├── .gitattributes ├── settings.gradle ├── lombok.config ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── com │ │ │ └── pet │ │ │ ├── domains │ │ │ ├── account │ │ │ │ ├── domain │ │ │ │ │ ├── AccountGroup.java │ │ │ │ │ ├── LoginAccount.java │ │ │ │ │ ├── Provider.java │ │ │ │ │ ├── Email.java │ │ │ │ │ ├── SignEmail.java │ │ │ │ │ └── Notification.java │ │ │ │ ├── repository │ │ │ │ │ ├── EmailRepository.java │ │ │ │ │ ├── SignEmailRepository.java │ │ │ │ │ ├── NotificationRepository.java │ │ │ │ │ └── AccountRepository.java │ │ │ │ ├── dto │ │ │ │ │ ├── request │ │ │ │ │ │ ├── NotificationUpdateParam.java │ │ │ │ │ │ ├── AccountLonginParam.java │ │ │ │ │ │ ├── AccountEmailParam.java │ │ │ │ │ │ ├── AccountUpdateParam.java │ │ │ │ │ │ ├── AccountEmailCheck.java │ │ │ │ │ │ ├── AccountAreaUpdateParam.java │ │ │ │ │ │ └── AccountSignUpParam.java │ │ │ │ │ └── response │ │ │ │ │ │ ├── AccountLoginResult.java │ │ │ │ │ │ ├── AccountCreateResult.java │ │ │ │ │ │ ├── AccountReadResult.java │ │ │ │ │ │ ├── AccountAreaReadResults.java │ │ │ │ │ │ ├── NotificationReadResults.java │ │ │ │ │ │ └── AccountBookmarkPostPageResults.java │ │ │ │ ├── mapper │ │ │ │ │ ├── NotificationMapper.java │ │ │ │ │ └── AccountMapper.java │ │ │ │ └── service │ │ │ │ │ ├── NotificationAsyncService.java │ │ │ │ │ └── NotificationService.java │ │ │ ├── EnumType.java │ │ │ ├── statistics │ │ │ │ ├── repository │ │ │ │ │ ├── PostCountByStatus.java │ │ │ │ │ └── PostStatisticsRepository.java │ │ │ │ ├── dto │ │ │ │ │ └── response │ │ │ │ │ │ └── PostStatisticsReadResult.java │ │ │ │ ├── controller │ │ │ │ │ └── PostStatisticsController.java │ │ │ │ ├── mapper │ │ │ │ │ └── PostStatisticsMapper.java │ │ │ │ ├── service │ │ │ │ │ └── PostStatisticsService.java │ │ │ │ └── domain │ │ │ │ │ └── PostStatistics.java │ │ │ ├── image │ │ │ │ ├── repository │ │ │ │ │ ├── ImageRepository.java │ │ │ │ │ └── PostImageRepository.java │ │ │ │ ├── service │ │ │ │ │ └── ImageService.java │ │ │ │ └── domain │ │ │ │ │ ├── Image.java │ │ │ │ │ └── PostImage.java │ │ │ ├── auth │ │ │ │ ├── repository │ │ │ │ │ ├── GroupPermissionRepository.java │ │ │ │ │ ├── GroupRepository.java │ │ │ │ │ └── RefreshTokenRepository.java │ │ │ │ ├── oauth2 │ │ │ │ │ ├── Oauth2User.java │ │ │ │ │ ├── RefreshToken.java │ │ │ │ │ ├── GoogleUser.java │ │ │ │ │ ├── ProviderType.java │ │ │ │ │ ├── NaverUser.java │ │ │ │ │ └── KakaoUser.java │ │ │ │ ├── service │ │ │ │ │ └── AuthenticationService.java │ │ │ │ ├── controller │ │ │ │ │ └── AuthController.java │ │ │ │ ├── domain │ │ │ │ │ ├── Permission.java │ │ │ │ │ ├── Group.java │ │ │ │ │ └── GroupPermission.java │ │ │ │ └── HandlerMethodResolverArgumentConfig.java │ │ │ ├── tag │ │ │ │ ├── repository │ │ │ │ │ ├── TagRepository.java │ │ │ │ │ └── PostTagRepository.java │ │ │ │ ├── service │ │ │ │ │ └── TagService.java │ │ │ │ └── domain │ │ │ │ │ ├── Tag.java │ │ │ │ │ └── PostTag.java │ │ │ ├── DeletableEntity.java │ │ │ ├── HealthCheckController.java │ │ │ ├── post │ │ │ │ ├── repository │ │ │ │ │ ├── ShelterPostBookmarkRepository.java │ │ │ │ │ ├── MissingPostBookmarkRepository.java │ │ │ │ │ ├── ShelterPostRepository.java │ │ │ │ │ ├── ShelterPostCustomRepository.java │ │ │ │ │ ├── projection │ │ │ │ │ │ ├── MissingPostWithFetch.java │ │ │ │ │ │ ├── ShelterPostWithFetch.java │ │ │ │ │ │ ├── MissingPostWithIsBookmark.java │ │ │ │ │ │ └── ShelterPostWithIsBookmark.java │ │ │ │ │ ├── MissingPostCustomRepository.java │ │ │ │ │ └── MissingPostRepository.java │ │ │ │ ├── domain │ │ │ │ │ ├── NeuteredType.java │ │ │ │ │ ├── Status.java │ │ │ │ │ ├── SexType.java │ │ │ │ │ ├── MissingPostBookmark.java │ │ │ │ │ └── ShelterPostBookmark.java │ │ │ │ ├── dto │ │ │ │ │ ├── response │ │ │ │ │ │ └── ShelterApiPageResult.java │ │ │ │ │ └── serach │ │ │ │ │ │ └── PostSearchParam.java │ │ │ │ └── service │ │ │ │ │ ├── ShelterPostBookmarkService.java │ │ │ │ │ └── MissingPostBookmarkService.java │ │ │ ├── animal │ │ │ │ ├── repository │ │ │ │ │ ├── AnimalKindRepository.java │ │ │ │ │ └── AnimalRepository.java │ │ │ │ ├── mapper │ │ │ │ │ └── AnimalMapper.java │ │ │ │ ├── dto │ │ │ │ │ ├── request │ │ │ │ │ │ └── AnimalKindCreateParams.java │ │ │ │ │ └── response │ │ │ │ │ │ ├── AnimalKindApiPageResults.java │ │ │ │ │ │ └── AnimalReadResults.java │ │ │ │ ├── controller │ │ │ │ │ └── AnimalController.java │ │ │ │ ├── service │ │ │ │ │ └── AnimalService.java │ │ │ │ └── domain │ │ │ │ │ ├── Animal.java │ │ │ │ │ └── AnimalKind.java │ │ │ ├── area │ │ │ │ ├── repository │ │ │ │ │ ├── TownRepository.java │ │ │ │ │ ├── CityRepository.java │ │ │ │ │ └── InterestAreaRepository.java │ │ │ │ ├── mapper │ │ │ │ │ ├── CityMapper.java │ │ │ │ │ └── InterestAreaMapper.java │ │ │ │ ├── dto │ │ │ │ │ ├── request │ │ │ │ │ │ ├── TownCreateParams.java │ │ │ │ │ │ └── CityCreateParams.java │ │ │ │ │ └── response │ │ │ │ │ │ ├── CityApiPageResults.java │ │ │ │ │ │ ├── TownApiPageResults.java │ │ │ │ │ │ └── CityReadResults.java │ │ │ │ ├── controller │ │ │ │ │ └── CityController.java │ │ │ │ ├── domain │ │ │ │ │ ├── City.java │ │ │ │ │ ├── Town.java │ │ │ │ │ └── InterestArea.java │ │ │ │ └── service │ │ │ │ │ └── CityService.java │ │ │ ├── comment │ │ │ │ ├── dto │ │ │ │ │ ├── request │ │ │ │ │ │ ├── CommentUpdateParam.java │ │ │ │ │ │ └── CommentCreateParam.java │ │ │ │ │ └── response │ │ │ │ │ │ ├── CommentWriteResult.java │ │ │ │ │ │ └── CommentPageResults.java │ │ │ │ ├── repository │ │ │ │ │ └── CommentRepository.java │ │ │ │ └── mapper │ │ │ │ │ └── CommentMapper.java │ │ │ ├── EntryExceptionController.java │ │ │ └── BaseEntity.java │ │ │ ├── infra │ │ │ ├── MailSender.java │ │ │ ├── ConsoleEmailSender.java │ │ │ ├── EmailMessage.java │ │ │ └── HtmlMailSender.java │ │ │ ├── common │ │ │ ├── exception │ │ │ │ ├── httpexception │ │ │ │ │ ├── ConflictException.java │ │ │ │ │ ├── NotFoundException.java │ │ │ │ │ ├── BadRequestException.java │ │ │ │ │ ├── ForbiddenException.java │ │ │ │ │ ├── AuthenticationException.java │ │ │ │ │ ├── InternalServerException.java │ │ │ │ │ └── BaseHttpException.java │ │ │ │ ├── CustomAsyncExceptionHandler.java │ │ │ │ ├── CustomAuthenticationEntryPoint.java │ │ │ │ └── controller │ │ │ │ │ └── ExceptionController.java │ │ │ ├── config │ │ │ │ ├── JpaAuditingConfig.java │ │ │ │ ├── QuerydslConfig.java │ │ │ │ ├── CustomPageableConfiguration.java │ │ │ │ ├── AsyncConfig.java │ │ │ │ ├── RedisConfig.java │ │ │ │ ├── RedisCachingConfig.java │ │ │ │ └── CookieUtil.java │ │ │ ├── s3 │ │ │ │ ├── service │ │ │ │ │ ├── UploadService.java │ │ │ │ │ ├── S3UploadService.java │ │ │ │ │ └── FileUploadService.java │ │ │ │ ├── validator │ │ │ │ │ ├── ValidImage.java │ │ │ │ │ ├── ImageSizeValidator.java │ │ │ │ │ ├── ValidImageSize.java │ │ │ │ │ └── ImageContentTypeValidator.java │ │ │ │ └── controller │ │ │ │ │ └── FileUploadController.java │ │ │ ├── util │ │ │ │ ├── Random.java │ │ │ │ ├── PasswordUtil.java │ │ │ │ ├── Assertions.java │ │ │ │ ├── EmailValidator.java │ │ │ │ └── OptimisticLockingHandlingUtils.java │ │ │ ├── response │ │ │ │ ├── ErrorResponse.java │ │ │ │ └── ApiResponse.java │ │ │ ├── jwt │ │ │ │ ├── JwtAuthentication.java │ │ │ │ └── JwtAuthenticationToken.java │ │ │ ├── property │ │ │ │ ├── JwtProperty.java │ │ │ │ ├── RefreshJwtProperty.java │ │ │ │ ├── RedisProperties.java │ │ │ │ └── ShelterProperties.java │ │ │ ├── filter │ │ │ │ ├── CookieAttributeFilter.java │ │ │ │ └── CorsFilter.java │ │ │ └── aop │ │ │ │ └── RequestLoggingAspect.java │ │ │ └── Application.java │ └── resources │ │ ├── application-dev.yml │ │ ├── application-prod.yml │ │ ├── application-rds.yml │ │ ├── data-h2.sql │ │ └── banner.txt └── test │ ├── resources │ └── org │ │ └── springframework │ │ └── restdocs │ │ └── templates │ │ ├── custom-response-fields.snippet │ │ └── request-fields.snippet │ └── java │ └── com │ └── pet │ ├── ApplicationTests.java │ ├── domains │ ├── account │ │ ├── WithAccount.java │ │ ├── domain │ │ │ ├── ProviderTest.java │ │ │ └── AccountTest.java │ │ ├── dto │ │ │ └── AccountValidationTest.java │ │ └── repository │ │ │ └── SignEmailRepositoryTest.java │ ├── docs │ │ ├── dto │ │ │ └── CommonDocumentationResults.java │ │ ├── CustomResponseFieldsSnippet.java │ │ ├── utils │ │ │ └── ApiDocumentUtils.java │ │ └── controller │ │ │ └── CommonDocumentationController.java │ ├── tag │ │ └── repository │ │ │ └── TagRepositoryTest.java │ └── animal │ │ ├── service │ │ └── AnimalServiceTest.java │ │ └── repository │ │ └── AnimalKindRepositoryTest.java │ └── common │ └── response │ └── ApiResponseTest.java ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── gradle.yml │ ├── sonar.yml │ ├── deploy-prod.yml │ └── deploy-dev.yml ├── appspec.yml ├── deploy-dev.sh └── deploy-prod.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'compet' 2 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prgrms-web-devcourse/Team_i6_comepet_BE/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/domain/AccountGroup.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | public enum AccountGroup { 4 | USER_GROUP 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/pet/infra/MailSender.java: -------------------------------------------------------------------------------- 1 | package com.pet.infra; 2 | 3 | public interface MailSender { 4 | 5 | void send(EmailMessage emailMessage); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/EnumType.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains; 2 | 3 | public interface EnumType { 4 | 5 | String getName(); 6 | 7 | String getText(); 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## ISSUE 종류 2 | 3 | - [ ] Feature 4 | - [ ] Bug Fix 5 | - [ ] Refactoring 6 | - [ ] Chore 7 | - [ ] Init 8 | 9 | ## 내용 10 | - 설명 : 11 | 12 | ## 기타 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR 종류 2 | 3 | - [ ] Feature 4 | - [ ] Bug Fix 5 | - [ ] Refactoring 6 | - [ ] Chore 7 | - [ ] Init 8 | 9 | close # 10 | 11 | ## 내용 12 | - 설명 : 13 | 14 | ## 추가 및 변경 로직 15 | - change 16 | 17 | ## 참고 및 기타 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/custom-response-fields.snippet: -------------------------------------------------------------------------------- 1 | {{title}} 2 | |=== 3 | |타입|타입명 4 | 5 | {{#fields}} 6 | |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 7 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 8 | 9 | {{/fields}} 10 | |=== 11 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/repository/PostCountByStatus.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.repository; 2 | 3 | import com.pet.domains.post.domain.Status; 4 | 5 | public interface PostCountByStatus { 6 | 7 | Status getPostStatus(); 8 | 9 | long getCount(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/pet/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.pet; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/image/repository/ImageRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.image.repository; 2 | 3 | import com.pet.domains.image.domain.Image; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface ImageRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/ConflictException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | public class ConflictException extends BaseHttpException { 4 | 5 | public ConflictException(String message, int code) { 6 | super(message, code); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | public class NotFoundException extends BaseHttpException { 4 | 5 | public NotFoundException(String message, int code) { 6 | super(message, code); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/repository/EmailRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.repository; 2 | 3 | import com.pet.domains.account.domain.Email; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface EmailRepository extends CrudRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | public class BadRequestException extends BaseHttpException { 4 | 5 | public BadRequestException(String message, int code) { 6 | super(message, code); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/ForbiddenException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | public class ForbiddenException extends BaseHttpException { 4 | 5 | public ForbiddenException(String message, int code) { 6 | super(message, code); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/JpaAuditingConfig.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @EnableJpaAuditing 7 | @Configuration 8 | public class JpaAuditingConfig { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | public class AuthenticationException extends BaseHttpException { 4 | 5 | public AuthenticationException(String message, int code) { 6 | super(message, code); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/InternalServerException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | public class InternalServerException extends BaseHttpException { 4 | 5 | public InternalServerException(String message, int code) { 6 | super(message, code); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/repository/GroupPermissionRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.repository; 2 | 3 | import com.pet.domains.auth.domain.GroupPermission; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface GroupPermissionRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:mysql://${DEV_DB_HOSTNAME}:${DEV_DB_PORT}/${DEV_DB_DATABASE}?zeroDateTimeBehavior=convertToNull 4 | username: ${DB_USER} 5 | password: ${DB_PASSWORD} 6 | jpa: 7 | show-sql: false 8 | hibernate: 9 | ddl-auto: update 10 | open-in-view: false 11 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:mysql://${DEV_DB_HOSTNAME}:${DEV_DB_PORT}/${PROD_DB_DATABASE}?zeroDateTimeBehavior=convertToNull 4 | username: ${DB_USER} 5 | password: ${DB_PASSWORD} 6 | jpa: 7 | show-sql: false 8 | hibernate: 9 | ddl-auto: none 10 | open-in-view: false 11 | -------------------------------------------------------------------------------- /src/main/resources/application-rds.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:mysql://${DEV_DB_HOSTNAME}:${DEV_DB_PORT}/${DEV_DB_DATABASE}?zeroDateTimeBehavior=convertToNull 4 | username: ${DB_USER} 5 | password: ${DB_PASSWORD} 6 | jpa: 7 | show-sql: false 8 | hibernate: 9 | ddl-auto: update 10 | open-in-view: false 11 | -------------------------------------------------------------------------------- /appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: / 5 | destination: /home/ubuntu/app/ 6 | overwrite: yes 7 | 8 | permissions: 9 | - object: / 10 | pattern: "**" 11 | owner: ubuntu 12 | group: ubuntu 13 | mode: 755 14 | 15 | hooks: 16 | AfterInstall: 17 | - location: deploy.sh 18 | timeout: 300 19 | runas: ubuntu 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/oauth2/Oauth2User.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.oauth2; 2 | 3 | import java.util.Map; 4 | 5 | public interface Oauth2User { 6 | 7 | String getNickname(Map attributes); 8 | 9 | String getEmail(Map attributes); 10 | 11 | String getProfileImage(Map attributes); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/data-h2.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO animal(created_at, updated_at, code, name) 2 | VALUES (NOW(), NOW(), '417000', '개'), 3 | (NOW(), NOW(), '422400', '고양이'), 4 | (NOW(), NOW(), '429900', '기타') 5 | ; 6 | 7 | INSERT INTO town (created_at, updated_at, code, name, city_id) 8 | SELECT NOW(), NOW(), '0000000', '전체', id 9 | FROM city 10 | WHERE code = '5690000'; 11 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/tag/repository/TagRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.tag.repository; 2 | 3 | import com.pet.domains.tag.domain.Tag; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface TagRepository extends JpaRepository { 8 | 9 | Optional findTagByName(String name); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/service/UploadService.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.service; 2 | 3 | import com.amazonaws.services.s3.model.ObjectMetadata; 4 | import java.io.InputStream; 5 | 6 | public interface UploadService { 7 | 8 | void uploadFile(InputStream inputStream, ObjectMetadata objectMetadata, String fileName); 9 | 10 | String getFileUrl(String fileName); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/repository/GroupRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.repository; 2 | 3 | import com.pet.domains.auth.domain.Group; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface GroupRepository extends JpaRepository { 8 | 9 | Optional findByName(String name); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/httpexception/BaseHttpException.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.httpexception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class BaseHttpException extends RuntimeException { 7 | 8 | private int code; 9 | 10 | public BaseHttpException(String message, int code) { 11 | super(message); 12 | this.code = code; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/domain/LoginAccount.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 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.PARAMETER) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface LoginAccount { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/util/Random.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.util; 2 | 3 | import java.util.UUID; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 8 | public class Random { 9 | 10 | public static String randomNewPassword() { 11 | return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 10); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/DeletableEntity.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.MappedSuperclass; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @MappedSuperclass 9 | public abstract class DeletableEntity extends BaseEntity { 10 | 11 | @Column(name = "deleted", nullable = false, columnDefinition = "boolean default false") 12 | private boolean deleted; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/util/PasswordUtil.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.util; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public class PasswordUtil { 8 | 9 | public static boolean match(String password) { 10 | return password.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\d~!@#$%^&*()+|=]{8,20}$"); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/account/WithAccount.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | import org.springframework.security.test.context.support.WithSecurityContext; 6 | 7 | @Retention(RetentionPolicy.RUNTIME) 8 | @WithSecurityContext(factory = WithAccountSecurityContextFactory.class) 9 | public @interface WithAccount { 10 | 11 | String value() default "tester"; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/image/repository/PostImageRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.image.repository; 2 | 3 | import com.pet.domains.image.domain.PostImage; 4 | import java.util.List; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface PostImageRepository extends JpaRepository { 8 | 9 | void deleteAllByMissingPostId(Long postId); 10 | 11 | List findAllByMissingPostId(Long postId); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/HealthCheckController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RequestMapping("/api") 8 | @RestController 9 | public class HealthCheckController { 10 | @GetMapping 11 | public String healthCheck() { 12 | return "OK"; 13 | } 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/ShelterPostBookmarkRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.post.domain.ShelterPostBookmark; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface ShelterPostBookmarkRepository extends JpaRepository { 8 | 9 | Long deleteByShelterPostIdAndAccount(Long postId, Account account); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/NotificationUpdateParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 9 | public class NotificationUpdateParam { 10 | 11 | private boolean checked; 12 | 13 | public NotificationUpdateParam(boolean checked) { 14 | this.checked = checked; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/MissingPostBookmarkRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.post.domain.MissingPostBookmark; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface MissingPostBookmarkRepository extends JpaRepository { 8 | 9 | Long deleteByAccountAndMissingPostId(Account account, Long missingPostId); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/repository/AnimalKindRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.repository; 2 | 3 | import com.pet.domains.animal.domain.AnimalKind; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface AnimalKindRepository extends JpaRepository { 8 | 9 | Optional findByName(String name); 10 | 11 | Optional findByNameAndAnimalId(String name, Long animalId); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/request-fields.snippet: -------------------------------------------------------------------------------- 1 | ===== Request Fields 2 | |=== 3 | |필드명|타입|필수값|양식|설명 4 | 5 | {{#fields}} 6 | |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 7 | |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} 8 | |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} 9 | |{{#tableCellContent}}{{#format}}{{.}}{{/format}}{{/tableCellContent}} 10 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 11 | 12 | {{/fields}} 13 | |=== 14 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/util/Assertions.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.util; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 8 | @Slf4j 9 | public class Assertions { 10 | 11 | public static void assertThrow(boolean condition, RuntimeException exception) { 12 | if (condition) { 13 | log.debug(exception.getMessage()); 14 | throw exception; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/response/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.response; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class ErrorResponse { 7 | 8 | private int code; 9 | 10 | private final Object message; 11 | 12 | public ErrorResponse(int code, Object message) { 13 | this.code = code; 14 | this.message = message; 15 | } 16 | 17 | public static ErrorResponse error(int code, Object message) { 18 | return new ErrorResponse(code, message); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/repository/SignEmailRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.repository; 2 | 3 | import com.pet.domains.account.domain.SignEmail; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface SignEmailRepository extends JpaRepository { 8 | 9 | Optional findByEmailAndVerifyKey(String email, String verifyKey); 10 | 11 | Optional findByEmailAndIsCheckedTrue(String email); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/repository/TownRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.repository; 2 | 3 | import com.pet.domains.area.domain.City; 4 | import com.pet.domains.area.domain.Town; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | public interface TownRepository extends JpaRepository { 9 | 10 | Optional findByNameAndCity(String name, City city); 11 | 12 | Optional findByNameContainingAndCity(String name, City city); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.repository; 2 | 3 | import com.pet.domains.auth.oauth2.RefreshToken; 4 | import java.util.Optional; 5 | import java.util.stream.DoubleStream; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.repository.CrudRepository; 8 | 9 | public interface RefreshTokenRepository extends JpaRepository { 10 | 11 | Optional findByToken(String refreshToken); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/AccountLonginParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 9 | public class AccountLonginParam { 10 | 11 | private String email; 12 | 13 | private String password; 14 | 15 | public AccountLonginParam(String email, String password) { 16 | this.email = email; 17 | this.password = password; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/response/AccountLoginResult.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.response; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class AccountLoginResult { 7 | 8 | private final Long id; 9 | 10 | private final String token; 11 | 12 | public AccountLoginResult(Long id, String token) { 13 | this.id = id; 14 | this.token = token; 15 | } 16 | 17 | public static AccountLoginResult of(Long id, String token) { 18 | return new AccountLoginResult(id, token); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/response/AccountCreateResult.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.response; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class AccountCreateResult { 7 | 8 | private final Long id; 9 | 10 | private final String token; 11 | 12 | public AccountCreateResult(Long id, String token) { 13 | this.id = id; 14 | this.token = token; 15 | } 16 | 17 | public static AccountCreateResult of(Long id, String token) { 18 | return new AccountCreateResult(id, token); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/domain/NeuteredType.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.domain; 2 | 3 | import com.pet.domains.EnumType; 4 | 5 | public enum NeuteredType implements EnumType { 6 | Y("Yes"), 7 | N("No"), 8 | U("Unknown"); 9 | 10 | private final String text; 11 | 12 | NeuteredType(String text) { 13 | this.text = text; 14 | } 15 | 16 | 17 | @Override 18 | public String getName() { 19 | return this.name(); 20 | } 21 | 22 | @Override 23 | public String getText() { 24 | return text; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/jwt/JwtAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.jwt; 2 | 3 | import lombok.Getter; 4 | import org.apache.commons.lang3.Validate; 5 | 6 | @Getter 7 | public class JwtAuthentication { 8 | 9 | private final String token; 10 | private final Long accountId; 11 | 12 | public JwtAuthentication(String token, Long accountId) { 13 | Validate.notBlank(token, "token must be provided."); 14 | Validate.notNull(accountId, "username must be provided."); 15 | this.token = token; 16 | this.accountId = accountId; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/domain/Status.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.domain; 2 | 3 | import com.pet.domains.EnumType; 4 | 5 | public enum Status implements EnumType { 6 | MISSING("실종"), 7 | DETECTION("목격"), 8 | PROTECTION("보호"), 9 | COMPLETION("완료"); 10 | 11 | private final String text; 12 | 13 | Status(String text) { 14 | this.text = text; 15 | } 16 | 17 | 18 | @Override 19 | public String getName() { 20 | return this.name(); 21 | } 22 | 23 | @Override 24 | public String getText() { 25 | return text; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/response/AccountReadResult.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.response; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class AccountReadResult { 7 | 8 | private final Long id; 9 | 10 | private final String nickname; 11 | 12 | private final String email; 13 | 14 | private final String image; 15 | 16 | public AccountReadResult(Long id, String nickname, String email, String image) { 17 | this.id = id; 18 | this.nickname = nickname; 19 | this.email = email; 20 | this.image = image; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/comment/dto/request/CommentUpdateParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.comment.dto.request; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import lombok.AccessLevel; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class CommentUpdateParam { 12 | 13 | @NotBlank(message = "댓글 내용을 입력해주세요.") 14 | private String content; 15 | 16 | @Builder 17 | private CommentUpdateParam(String content) { 18 | this.content = content; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/CustomAsyncExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception; 2 | 3 | import java.lang.reflect.Method; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; 6 | 7 | @Slf4j 8 | public class CustomAsyncExceptionHandler 9 | implements AsyncUncaughtExceptionHandler { 10 | 11 | @Override 12 | public void handleUncaughtException(Throwable ex, Method method, Object... params) { 13 | log.warn("Exception message - {}", ex.getMessage()); 14 | log.warn("Method name - {}", method.getName()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/QuerydslConfig.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import javax.persistence.EntityManager; 5 | import javax.persistence.PersistenceContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class QuerydslConfig { 11 | 12 | @PersistenceContext 13 | private EntityManager entityManager; 14 | 15 | @Bean 16 | public JPAQueryFactory jpaQueryFactory() { 17 | return new JPAQueryFactory(entityManager); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/repository/CityRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.repository; 2 | 3 | import com.pet.domains.area.domain.City; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | 9 | public interface CityRepository extends JpaRepository { 10 | 11 | Optional findByCode(String code); 12 | 13 | Optional findByName(String name); 14 | 15 | @Query("select distinct c from City as c join fetch c.towns") 16 | List findAll(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/property/JwtProperty.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.property; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.ConstructorBinding; 7 | 8 | @ConstructorBinding 9 | @ConfigurationProperties(prefix = "jwt") 10 | @Getter 11 | @RequiredArgsConstructor 12 | public class JwtProperty { 13 | 14 | private final String header; 15 | 16 | private final String issuer; 17 | 18 | private final String clientSecret; 19 | 20 | private final int expirySeconds; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/tag/repository/PostTagRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.tag.repository; 2 | 3 | import com.pet.domains.post.domain.MissingPost; 4 | import com.pet.domains.tag.domain.PostTag; 5 | import com.pet.domains.tag.domain.Tag; 6 | import java.util.List; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | 9 | public interface PostTagRepository extends JpaRepository { 10 | 11 | List getPostTagsByMissingPostId(Long postId); 12 | 13 | void deleteByMissingPost(MissingPost missingPost); 14 | 15 | PostTag findByMissingPostAndTag(MissingPost missingPost, Tag tag); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/util/EmailValidator.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.util; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.apache.commons.lang3.Validate; 6 | 7 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 8 | public class EmailValidator { 9 | private static final String EMAIL_REGEX = "\\b[\\w.-]+@[\\w.-]+\\.\\w{2,4}\\b"; 10 | 11 | public static void validate(String email) { 12 | Validate.notBlank(email, "email must not be null"); 13 | if (!email.matches(EMAIL_REGEX)) { 14 | throw new IllegalArgumentException("invalid email : " + email); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/pet/infra/ConsoleEmailSender.java: -------------------------------------------------------------------------------- 1 | package com.pet.infra; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Slf4j 8 | @Component 9 | @Profile({"local", "default", "rds"}) 10 | public class ConsoleEmailSender implements MailSender { 11 | 12 | @Override 13 | public void send(EmailMessage emailMessage) { 14 | log.info(" to : {}, subject : {}, message : {}", 15 | emailMessage.getTo(), 16 | emailMessage.getSubject(), 17 | emailMessage.getMessage() 18 | ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/property/RefreshJwtProperty.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.property; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.ConstructorBinding; 7 | 8 | @ConstructorBinding 9 | @ConfigurationProperties(prefix = "refresh") 10 | @Getter 11 | @RequiredArgsConstructor 12 | public class RefreshJwtProperty { 13 | 14 | private final String header; 15 | 16 | private final String issuer; 17 | 18 | private final String clientSecret; 19 | 20 | private final int expirySeconds; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/mapper/CityMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.mapper; 2 | 3 | 4 | import com.pet.domains.area.domain.City; 5 | import com.pet.domains.area.domain.Town; 6 | import com.pet.domains.area.dto.request.CityCreateParams; 7 | import com.pet.domains.area.dto.response.CityReadResults; 8 | import java.util.List; 9 | import org.mapstruct.Mapper; 10 | 11 | @Mapper(componentModel = "spring") 12 | public interface CityMapper { 13 | 14 | City toEntity(CityCreateParams.City cityCreateParam); 15 | 16 | CityReadResults.City.Town toTownDto(Town town); 17 | 18 | CityReadResults.City toCityDto(City city, List collect); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/CustomPageableConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.domain.PageRequest; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer; 8 | 9 | @Configuration 10 | public class CustomPageableConfiguration { 11 | 12 | @Bean 13 | public PageableHandlerMethodArgumentResolverCustomizer customize() { 14 | return p -> p.setFallbackPageable(PageRequest.of(0, 20, Sort.by("id").descending())); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ____ 2 | / \__ ____ U ___ u __ __ U _____ u ____ U _____ u _____ 3 | |\ / @ \ U /"___| \/"_ \/U|' \/ '|u\| ___"|/U| _"\ u\| ___"|/ |_ " _| 4 | \ \_______| \ .:|> \| | u | | | |\| |\/| |/ | _|" \| |_) |/ | _|" | | 5 | \ ##| | \__/ | |/__.-,_| |_| | | | | | | |___ | __/ | |___ /| |\ 6 | | ####\__/ \ \____|\_)-\___/ |_| |_| |_____| |_| |_____| u |_|U 7 | / / ## \| _// \\ \\ <<,-,,-. << >> ||>>_ << >> _// \\_ 8 | / /__________\ \ (__)(__) (__) (./ \.) (__) (__)(__)__) (__) (__)(__) (__) 9 | L_JJ \__JJ -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/validator/ValidImage.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.validator; 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 | import javax.validation.Constraint; 8 | import javax.validation.Payload; 9 | 10 | @Target(ElementType.PARAMETER) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Constraint(validatedBy = {ImageContentTypeValidator.class}) 13 | public @interface ValidImage { 14 | 15 | String message() default "Invalid image file"; 16 | 17 | Class[] groups() default {}; 18 | 19 | Class[] payload() default {}; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/AccountEmailParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Pattern; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class AccountEmailParam { 12 | 13 | @NotBlank(message = "올바른 이메일 형식이 아닙니다.") 14 | @Pattern(regexp = "\\b[\\w.-]+@[\\w.-]+\\.\\w{2,4}\\b", message = "올바른 이메일 형식이 아닙니다.") 15 | private String email; 16 | 17 | public AccountEmailParam(String email) { 18 | this.email = email; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/AccountUpdateParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 9 | public class AccountUpdateParam { 10 | 11 | private String nickname; 12 | 13 | private String newPassword; 14 | 15 | private String newPasswordCheck; 16 | 17 | public AccountUpdateParam(String nickname, String newPassword, String newPasswordCheck) { 18 | this.nickname = nickname; 19 | this.newPassword = newPassword; 20 | this.newPasswordCheck = newPasswordCheck; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/property/RedisProperties.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.property; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.ConstructorBinding; 7 | 8 | @Getter 9 | @RequiredArgsConstructor 10 | @ConstructorBinding 11 | @ConfigurationProperties("spring") 12 | public final class RedisProperties { 13 | 14 | private final Redis redis; 15 | 16 | @Getter 17 | @RequiredArgsConstructor 18 | public static final class Redis { 19 | 20 | private final String host; 21 | 22 | private final Integer port; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/EntryExceptionController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import lombok.RequiredArgsConstructor; 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 | @RequiredArgsConstructor 10 | @RestController 11 | @RequestMapping(value = "/exception") 12 | public class EntryExceptionController { 13 | 14 | @GetMapping(value = "/entrypoint") 15 | public void entrypointException() { 16 | throw ExceptionMessage.NOT_FOUND_JWT.getException(); 17 | } 18 | 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/validator/ImageSizeValidator.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.validator; 2 | 3 | import java.util.List; 4 | import javax.validation.ConstraintValidator; 5 | import javax.validation.ConstraintValidatorContext; 6 | import org.springframework.util.CollectionUtils; 7 | import org.springframework.web.multipart.MultipartFile; 8 | 9 | public class ImageSizeValidator implements ConstraintValidator> { 10 | 11 | @Override 12 | public boolean isValid(List images, ConstraintValidatorContext context) { 13 | if (CollectionUtils.isEmpty(images)) { 14 | return true; 15 | } 16 | return images.size() <= 3; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/validator/ValidImageSize.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.validator; 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 | import javax.validation.Constraint; 8 | import javax.validation.Payload; 9 | 10 | @Target(ElementType.PARAMETER) 11 | @Constraint(validatedBy = ImageSizeValidator.class) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface ValidImageSize { 14 | 15 | String message() default "The input files cannot contain more than 3 images."; 16 | 17 | Class[] groups() default {}; 18 | 19 | Class[] payload() default {}; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/repository/PostStatisticsRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.repository; 2 | 3 | import com.pet.domains.statistics.domain.PostStatistics; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | 9 | public interface PostStatisticsRepository extends JpaRepository { 10 | 11 | @Query("SELECT mp.status AS postStatus, count(mp) AS count FROM" 12 | + " MissingPost AS mp WHERE mp.deleted=false GROUP BY mp.status") 13 | List findGroupByStatus(); 14 | 15 | Optional findFirstByOrderByIdDesc(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/docs/dto/CommonDocumentationResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.docs.dto; 2 | 3 | import java.util.Map; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class CommonDocumentationResults { 9 | 10 | private final Map sexTypes; 11 | private final Map neuteredTypes; 12 | private final Map status; 13 | 14 | @Builder 15 | public CommonDocumentationResults( 16 | Map sexTypes, 17 | Map neuteredTypes, 18 | Map status 19 | ) { 20 | this.sexTypes = sexTypes; 21 | this.neuteredTypes = neuteredTypes; 22 | this.status = status; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/domain/Provider.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import java.util.Arrays; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @Getter 9 | @RequiredArgsConstructor 10 | public enum Provider { 11 | LOCAL("local"), 12 | GOOGLE("google"), 13 | KAKAO("kakao"), 14 | NAVER("naver"); 15 | 16 | private final String type; 17 | 18 | public static Provider findByType(String type) { 19 | return Arrays.stream(Provider.values()) 20 | .filter(provider -> provider.type.equals(type)) 21 | .findAny() 22 | .orElseThrow(ExceptionMessage.NOT_FOUND_PROVIDER::getException); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/repository/InterestAreaRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.repository; 2 | 3 | import com.pet.domains.area.domain.InterestArea; 4 | import java.util.List; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | 9 | public interface InterestAreaRepository extends JpaRepository { 10 | 11 | @Query("select i from InterestArea i " 12 | + "join fetch i.town t " 13 | + "left join fetch t.city c " 14 | + "where i.account.id = :accountId") 15 | List findByAccountId(@Param("accountId") Long accountId); 16 | 17 | void deleteAllByAccountId(Long id); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/CustomAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception; 2 | 3 | import java.io.IOException; 4 | import javax.servlet.http.HttpServletRequest; 5 | import javax.servlet.http.HttpServletResponse; 6 | import org.springframework.security.core.AuthenticationException; 7 | import org.springframework.security.web.AuthenticationEntryPoint; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { 12 | 13 | @Override 14 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx) 15 | throws IOException { 16 | response.sendRedirect("/exception/entrypoint"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/response/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.response; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import org.apache.commons.lang3.ObjectUtils; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 10 | @Getter 11 | public class ApiResponse { 12 | 13 | private T data; 14 | 15 | private LocalDateTime serverDateTime; 16 | 17 | public ApiResponse(final T data) { 18 | ObjectUtils.requireNonEmpty(data, "data must be not null"); 19 | this.data = data; 20 | this.serverDateTime = LocalDateTime.now(); 21 | } 22 | 23 | public static ApiResponse ok(final T data) { 24 | return new ApiResponse<>(data); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.service; 2 | 3 | import com.pet.common.jwt.JwtAuthentication; 4 | import com.pet.common.jwt.JwtAuthenticationToken; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class AuthenticationService { 12 | 13 | private final AuthenticationManager authenticationManager; 14 | 15 | public JwtAuthentication authenticate(String principal, String credentials) { 16 | return (JwtAuthentication)authenticationManager 17 | .authenticate(new JwtAuthenticationToken(principal, credentials)).getPrincipal(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/mapper/AnimalMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.mapper; 2 | 3 | import com.pet.domains.animal.domain.Animal; 4 | import com.pet.domains.animal.dto.response.AnimalReadResults; 5 | import java.util.List; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | 9 | @Mapper(componentModel = "spring") 10 | public interface AnimalMapper { 11 | 12 | @Mapping(source = "animalKinds", target = "kinds") 13 | AnimalReadResults.Animal toAnimalOfAnimalReadResults(Animal animalEntity); 14 | 15 | List toAnimalReadResult(List animalEntities); 16 | 17 | default AnimalReadResults toAnimalReadResults(List animalReadResult) { 18 | return AnimalReadResults.of(animalReadResult); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/AccountEmailCheck.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Pattern; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class AccountEmailCheck { 12 | 13 | @NotBlank(message = "올바른 이메일 형식이 아닙니다.") 14 | @Pattern(regexp = "\\b[\\w.-]+@[\\w.-]+\\.\\w{2,4}\\b", message = "올바른 이메일 형식이 아닙니다.") 15 | private String email; 16 | 17 | @NotBlank(message = "인증번호를 입력해주세요.") 18 | private String key; 19 | 20 | public AccountEmailCheck(String email, String key) { 21 | this.email = email; 22 | this.key = key; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.controller; 2 | 3 | import com.pet.common.response.ApiResponse; 4 | import com.pet.domains.account.domain.Account; 5 | import com.pet.domains.account.domain.LoginAccount; 6 | import java.util.Map; 7 | import lombok.extern.slf4j.Slf4j; 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 | @RestController 13 | @RequestMapping("/api/v1/auth-user") 14 | @Slf4j 15 | public class AuthController { 16 | 17 | @GetMapping 18 | public ApiResponse> checkToken(@LoginAccount Account account) { 19 | return ApiResponse.ok(Map.of("id", account.getId())); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | pull_request: 8 | branches: [ develop ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 14 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '14' 21 | distribution: 'adopt' 22 | cache: gradle 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew clean build 27 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/ShelterPostRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository; 2 | 3 | import com.pet.domains.post.domain.ShelterPost; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.EntityGraph; 6 | import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | 10 | public interface ShelterPostRepository extends JpaRepository, ShelterPostCustomRepository { 11 | 12 | @EntityGraph(attributePaths = {"animalKind", "animalKind.animal", "town", "town.city"}, type = EntityGraphType.LOAD) 13 | @Query("select sp from ShelterPost sp where sp.id = :postId") 14 | Optional findByIdWithFetch(Long postId); 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/repository/AnimalRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.repository; 2 | 3 | import com.pet.domains.animal.domain.Animal; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.EntityGraph; 7 | import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; 8 | import org.springframework.data.jpa.repository.JpaRepository; 9 | import org.springframework.data.jpa.repository.Query; 10 | 11 | public interface AnimalRepository extends JpaRepository { 12 | 13 | @EntityGraph(attributePaths = "animalKinds", type = EntityGraphType.LOAD) 14 | @Query("select distinct a from Animal a") 15 | List findAllWithAnimalKinds(); 16 | 17 | Optional findByCode(String code); 18 | 19 | Optional findByName(String name); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/dto/response/PostStatisticsReadResult.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.dto.response; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class PostStatisticsReadResult { 9 | 10 | private final Long missing; 11 | 12 | private final Long detection; 13 | 14 | private final Long protection; 15 | 16 | private final Long completion; 17 | 18 | private final LocalDateTime date; 19 | 20 | @Builder 21 | private PostStatisticsReadResult(Long missing, Long detection, Long protection, Long completion, 22 | LocalDateTime date) { 23 | this.missing = missing; 24 | this.detection = detection; 25 | this.protection = protection; 26 | this.completion = completion; 27 | this.date = date; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/tag/service/TagService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.tag.service; 2 | 3 | import com.pet.domains.tag.domain.Tag; 4 | import com.pet.domains.tag.repository.TagRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | @RequiredArgsConstructor 10 | @Transactional(readOnly = true) 11 | @Service 12 | public class TagService { 13 | 14 | private final TagRepository tagRepository; 15 | 16 | @Transactional 17 | public Tag getOrCreateByTagName(String tagName) { 18 | return tagRepository.findTagByName(tagName) 19 | .orElseGet(() -> tagRepository.save( 20 | Tag.builder() 21 | .name(tagName) 22 | .build()) 23 | ); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/controller/FileUploadController.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.controller; 2 | 3 | import com.pet.common.s3.service.FileUploadService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | @RequiredArgsConstructor 12 | @RequestMapping("/api/v1/upload") 13 | @RestController 14 | public class FileUploadController { 15 | 16 | private final FileUploadService fileUploadService; 17 | 18 | @PostMapping() 19 | public String uploadImage(@RequestParam MultipartFile file) { 20 | return fileUploadService.uploadImage(file); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/comment/dto/request/CommentCreateParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.comment.dto.request; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | import lombok.AccessLevel; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 12 | public class CommentCreateParam { 13 | 14 | @NotNull(message = "게시글 아이디는 필수입니다.") 15 | private Long postId; 16 | 17 | @NotBlank(message = "댓글 내용을 입력해주세요.") 18 | private String content; 19 | 20 | private Long parentCommentId; 21 | 22 | @Builder 23 | public CommentCreateParam(Long postId, String content, Long parentCommentId) { 24 | this.postId = postId; 25 | this.content = content; 26 | this.parentCommentId = parentCommentId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/mapper/NotificationMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.mapper; 2 | 3 | import com.pet.domains.account.domain.Notification; 4 | import com.pet.domains.account.dto.response.NotificationReadResults; 5 | import org.mapstruct.Mapper; 6 | import org.mapstruct.Mapping; 7 | 8 | @Mapper(componentModel = "spring") 9 | public interface NotificationMapper { 10 | 11 | 12 | @Mapping(target = "nickname", source = "account.nickname") 13 | @Mapping(target = "image", source = "missingPost.thumbnail") 14 | @Mapping(target = "postId", source = "missingPost.id") 15 | @Mapping(target = "status", source = "missingPost.status") 16 | @Mapping(target = "animalKindName", source = "missingPost.animalKind.name") 17 | @Mapping(target = "town", source = "missingPost.town.name") 18 | NotificationReadResults.Notification toNotificationDto(Notification notification); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/dto/request/TownCreateParams.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.dto.request; 2 | 3 | import java.util.List; 4 | import javax.xml.bind.annotation.XmlAccessType; 5 | import javax.xml.bind.annotation.XmlAccessorType; 6 | import javax.xml.bind.annotation.XmlElement; 7 | import javax.xml.bind.annotation.XmlRootElement; 8 | import lombok.Getter; 9 | 10 | @Getter 11 | @XmlRootElement(name = "items") 12 | @XmlAccessorType(XmlAccessType.NONE) 13 | public class TownCreateParams { 14 | 15 | @XmlElement(name = "item") 16 | private List towns; 17 | 18 | @Getter 19 | @XmlRootElement(name = "item") 20 | @XmlAccessorType(XmlAccessType.NONE) 21 | public static class Town { 22 | 23 | @XmlElement(name = "orgCd") 24 | private String code; 25 | 26 | @XmlElement(name = "orgdownNm") 27 | private String name; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/domain/Permission.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @Entity 16 | @Table(name = "permission") 17 | public class Permission { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | @Column(name = "id", updatable = false) 22 | private Long id; 23 | 24 | @Column(name = "name", length = 20, nullable = false) 25 | private String name; 26 | 27 | public Permission(String name) { 28 | this.name = name; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/dto/request/CityCreateParams.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.dto.request; 2 | 3 | import java.util.List; 4 | import javax.xml.bind.annotation.XmlAccessType; 5 | import javax.xml.bind.annotation.XmlAccessorType; 6 | import javax.xml.bind.annotation.XmlElement; 7 | import javax.xml.bind.annotation.XmlRootElement; 8 | import lombok.Getter; 9 | 10 | @Getter 11 | @XmlRootElement(name = "items") 12 | @XmlAccessorType(XmlAccessType.NONE) 13 | public class CityCreateParams { 14 | 15 | @XmlElement(name = "item") 16 | private List cities; 17 | 18 | @Getter 19 | @XmlRootElement(name = "item") 20 | @XmlAccessorType(XmlAccessType.NONE) 21 | public static class City { 22 | 23 | @XmlElement(name = "orgCd") 24 | private String code; 25 | 26 | @XmlElement(name = "orgdownNm") 27 | private String name; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/image/service/ImageService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.image.service; 2 | 3 | import com.pet.common.s3.service.FileUploadService; 4 | import com.pet.domains.image.domain.Image; 5 | import com.pet.domains.image.repository.ImageRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | @RequiredArgsConstructor 12 | @Transactional(readOnly = true) 13 | @Service 14 | public class ImageService { 15 | 16 | private final FileUploadService fileUploadService; 17 | private final ImageRepository imageRepository; 18 | 19 | @Transactional 20 | public Image createImage(MultipartFile imageFile) { 21 | return imageRepository.save(new Image(fileUploadService.uploadImage(imageFile))); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.EntityListeners; 6 | import javax.persistence.MappedSuperclass; 7 | import lombok.Getter; 8 | import org.springframework.data.annotation.CreatedDate; 9 | import org.springframework.data.annotation.LastModifiedDate; 10 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 11 | 12 | @Getter 13 | @EntityListeners(AuditingEntityListener.class) 14 | @MappedSuperclass 15 | public abstract class BaseEntity { 16 | 17 | @CreatedDate 18 | @Column(name = "created_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMP") 19 | private LocalDateTime createdAt; 20 | 21 | @LastModifiedDate 22 | @Column(name = "updated_at", nullable = false, columnDefinition = "TIMESTAMP") 23 | private LocalDateTime updatedAt; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/image/domain/Image.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.image.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import lombok.AccessLevel; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | 14 | @Getter 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @Entity 17 | @Table(name = "image") 18 | public class Image extends BaseEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "id", updatable = false) 23 | private Long id; 24 | 25 | @Column(name = "name", nullable = false) 26 | private String name; 27 | 28 | public Image(String name) { 29 | this.name = name; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/pet/infra/EmailMessage.java: -------------------------------------------------------------------------------- 1 | package com.pet.infra; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class EmailMessage { 7 | 8 | private static final String COMPET_SUBJECT = "컴팻 이메일 인증 요청입니다."; 9 | private static final String COMPET_PASSWORD = "컴팻 임시 비밀번호입니다."; 10 | 11 | private String to; 12 | 13 | private String subject; 14 | 15 | private String message; 16 | 17 | public EmailMessage(String to, String message, String subject) { 18 | this.to = to; 19 | this.message = message; 20 | this.subject = subject; 21 | } 22 | 23 | public static EmailMessage crateVerifyEmailMessage(String to, String message) { 24 | return new EmailMessage(to, message, COMPET_SUBJECT); 25 | } 26 | 27 | public static EmailMessage crateNewPasswordEmailMessage(String to, String message) { 28 | return new EmailMessage(to, message, COMPET_PASSWORD); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/dto/request/AnimalKindCreateParams.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.dto.request; 2 | 3 | import java.util.List; 4 | import javax.xml.bind.annotation.XmlAccessType; 5 | import javax.xml.bind.annotation.XmlAccessorType; 6 | import javax.xml.bind.annotation.XmlElement; 7 | import javax.xml.bind.annotation.XmlRootElement; 8 | import lombok.Getter; 9 | 10 | 11 | @Getter 12 | @XmlRootElement(name = "items") 13 | @XmlAccessorType(XmlAccessType.NONE) 14 | public class AnimalKindCreateParams { 15 | 16 | @XmlElement(name = "item") 17 | private List animalKinds; 18 | 19 | @Getter 20 | @XmlRootElement(name = "item") 21 | @XmlAccessorType(XmlAccessType.NONE) 22 | public static class AnimalKind { 23 | 24 | @XmlElement(name = "KNm") 25 | private String name; 26 | 27 | @XmlElement(name = "kindCd") 28 | private String code; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/oauth2/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.oauth2; 2 | 3 | import java.time.LocalDateTime; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.Id; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | @Entity 11 | @Getter 12 | public class RefreshToken { 13 | 14 | @Id 15 | @GeneratedValue 16 | private Long id; 17 | 18 | private String token; 19 | 20 | private String email; 21 | 22 | private LocalDateTime createdAt; 23 | 24 | public RefreshToken() { 25 | 26 | } 27 | 28 | @Builder 29 | public RefreshToken(String token, String email) { 30 | this.token = token; 31 | this.email = email; 32 | createdAt = LocalDateTime.now(); 33 | } 34 | 35 | public boolean isVerifyTokenByBefore30Days() { 36 | return createdAt.isAfter(LocalDateTime.now().plusDays(30)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/property/ShelterProperties.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.property; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.ConstructorBinding; 7 | 8 | @Getter 9 | @RequiredArgsConstructor 10 | @ConstructorBinding 11 | @ConfigurationProperties(prefix = "shelter") 12 | public class ShelterProperties { 13 | 14 | private final String name; 15 | 16 | private final String description; 17 | 18 | private final ShelterProperties.Api api; 19 | 20 | @RequiredArgsConstructor 21 | @Getter 22 | public static class Api { 23 | 24 | private final String url; 25 | private final String key; 26 | } 27 | 28 | public String getUrl() { 29 | return api.getUrl(); 30 | } 31 | 32 | public String getKey() { 33 | return api.getKey(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/oauth2/GoogleUser.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.oauth2; 2 | 3 | import java.util.Map; 4 | 5 | public class GoogleUser implements Oauth2User { 6 | 7 | private final String nickname; 8 | 9 | private final String email; 10 | 11 | private final String profileImage; 12 | 13 | public GoogleUser(String nickname, String email, String profileImage) { 14 | this.nickname = nickname; 15 | this.email = email; 16 | this.profileImage = profileImage; 17 | } 18 | 19 | @Override 20 | public String getNickname(Map attributes) { 21 | return (String) attributes.get(nickname); 22 | } 23 | 24 | @Override 25 | public String getEmail(Map attributes) { 26 | return (String) attributes.get(email); 27 | } 28 | 29 | @Override 30 | public String getProfileImage(Map attributes) { 31 | return (String) attributes.get(profileImage); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/exception/controller/ExceptionController.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.exception.controller; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import com.pet.common.response.ErrorResponse; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 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 | @RestController 13 | @RequestMapping("/api/error-info") 14 | public class ExceptionController { 15 | 16 | /** 17 | * 개발 단계에서 프론트에게 동적으로 에러 메세지를 전달하기 위해 생성 18 | */ 19 | @GetMapping 20 | public List getExceptionMessage() { 21 | return Arrays.stream(ExceptionMessage.values()) 22 | .map(message -> ErrorResponse.error(ExceptionMessage.getCode(message), message.getException().getMessage())) 23 | .collect(Collectors.toList()); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/oauth2/ProviderType.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.oauth2; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import java.util.Arrays; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @Getter 9 | @RequiredArgsConstructor 10 | public enum ProviderType { 11 | GOOGLE("google", new GoogleUser("sub", "email", "picture")), 12 | NAVER("naver", new NaverUser("nickname", "email", "profile_image")), 13 | KAKAO("kakao", new KakaoUser("nickname", "email", "profile_image")); 14 | 15 | private final String type; 16 | 17 | private final Oauth2User oauth2User; 18 | 19 | public static Oauth2User findProvider(String provider) { 20 | return Arrays.stream(ProviderType.values()) 21 | .filter(providerType -> providerType.type.equals(provider)) 22 | .findAny() 23 | .map(providerType -> providerType.oauth2User) 24 | .orElseThrow(ExceptionMessage.NOT_FOUND_PROVIDER::getException); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /deploy-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BUILD_JAR=$(ls /home/ubuntu/app/compet-0.0.1.jar) 3 | DEPLOY_PATH=/home/ubuntu/app/ 4 | JAR_NAME=$(basename $BUILD_JAR) 5 | echo "> dev deploy" 6 | echo "> build 파일명: $JAR_NAME" >> /home/ubuntu/app/log/deploy_dev.log 7 | 8 | echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ubuntu/app/deploy_dev.log 9 | CURRENT_PID=$(pgrep -f $JAR_NAME) 10 | 11 | if [ -z $CURRENT_PID ] 12 | then 13 | echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ubuntu/app/deploy_dev.log 14 | else 15 | echo "> kill -15 $CURRENT_PID" 16 | kill -15 $CURRENT_PID 17 | sleep 5 18 | fi 19 | 20 | source ~/.env 21 | ENVS=$(printenv) 22 | echo "> 환경변수 :$ENVS" >> /home/ubuntu/app/deploy_dev.log 23 | 24 | DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME 25 | echo "> DEPLOY_JAR 이름: $DEPLOY_JAR" >> /home/ubuntu/app/deploy_dev.log 26 | chmod 755 $DEPLOY_JAR 27 | echo "> DEPLOY_JAR 배포" >> /home/ubuntu/app/deploy_dev.log 28 | nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=dev $DEPLOY_JAR >> /home/ubuntu/app/deploy_dev.log 2>/home/ubuntu/app/deploy_dev_err.log & 29 | -------------------------------------------------------------------------------- /deploy-prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BUILD_JAR=$(ls /home/ubuntu/app/compet-0.0.1.jar) 3 | DEPLOY_PATH=/home/ubuntu/app/ 4 | JAR_NAME=$(basename $BUILD_JAR) 5 | echo "> prod deploy" 6 | echo "> build 파일명: $JAR_NAME" >> /home/ubuntu/app/log/deploy_prod.log 7 | 8 | echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ubuntu/app/deploy_prod.log 9 | CURRENT_PID=$(pgrep -f $JAR_NAME) 10 | 11 | if [ -z $CURRENT_PID ] 12 | then 13 | echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ubuntu/app/deploy_prod.log 14 | else 15 | echo "> kill -15 $CURRENT_PID" 16 | kill -15 $CURRENT_PID 17 | sleep 5 18 | fi 19 | 20 | source ~/.env 21 | ENVS=$(printenv) 22 | echo "> 환경변수 :$ENVS" >> /home/ubuntu/app/deploy_prod.log 23 | 24 | DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME 25 | echo "> DEPLOY_JAR 이름: $DEPLOY_JAR" >> /home/ubuntu/app/deploy_prod.log 26 | chmod 755 $DEPLOY_JAR 27 | echo "> DEPLOY_JAR 배포" >> /home/ubuntu/app/deploy_prod.log 28 | nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=prod $DEPLOY_JAR >> /home/ubuntu/app/deploy_prod.log 2>/home/ubuntu/app/deploy_prod_err.log & 29 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/repository/NotificationRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.account.domain.Notification; 5 | import java.util.Optional; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.jpa.repository.EntityGraph; 9 | import org.springframework.data.jpa.repository.JpaRepository; 10 | 11 | public interface NotificationRepository extends JpaRepository { 12 | 13 | void deleteByIdAndAccount(Long noticeId, Account account); 14 | 15 | Optional findByIdAndAccount(Long noticeId, Account account); 16 | 17 | @EntityGraph( 18 | attributePaths = {"account", "missingPost", "missingPost.animalKind", "missingPost.town"}, 19 | type = EntityGraph.EntityGraphType.LOAD 20 | ) 21 | Page findByAccount(Account account, Pageable pageable); 22 | 23 | void deleteAllInBatchByAccount(Account account); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/dto/response/CityApiPageResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.dto.response; 2 | 3 | import com.pet.domains.area.dto.request.CityCreateParams; 4 | import java.util.Objects; 5 | import javax.xml.bind.annotation.XmlAccessType; 6 | import javax.xml.bind.annotation.XmlAccessorType; 7 | import javax.xml.bind.annotation.XmlElement; 8 | import javax.xml.bind.annotation.XmlRootElement; 9 | import lombok.Getter; 10 | 11 | @Getter 12 | @XmlRootElement(name = "response") 13 | @XmlAccessorType(XmlAccessType.NONE) 14 | public class CityApiPageResults { 15 | 16 | @XmlElement(name = "body") 17 | private CityApiPageResults.Body body; 18 | 19 | @Getter 20 | @XmlRootElement(name = "body") 21 | @XmlAccessorType(XmlAccessType.NONE) 22 | public static class Body { 23 | 24 | @XmlElement(name = "items") 25 | private CityCreateParams items; 26 | } 27 | 28 | public CityCreateParams getBodyItems() { 29 | Objects.requireNonNull(body, "시도 조회 api 응답 바디가 널입니다."); 30 | return body.getItems(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/dto/response/TownApiPageResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.dto.response; 2 | 3 | import com.pet.domains.area.dto.request.TownCreateParams; 4 | import java.util.Objects; 5 | import javax.xml.bind.annotation.XmlAccessType; 6 | import javax.xml.bind.annotation.XmlAccessorType; 7 | import javax.xml.bind.annotation.XmlElement; 8 | import javax.xml.bind.annotation.XmlRootElement; 9 | import lombok.Getter; 10 | 11 | @Getter 12 | @XmlRootElement(name = "response") 13 | @XmlAccessorType(XmlAccessType.NONE) 14 | public class TownApiPageResults { 15 | 16 | @XmlElement(name = "body") 17 | private TownApiPageResults.Body body; 18 | 19 | @Getter 20 | @XmlRootElement(name = "body") 21 | @XmlAccessorType(XmlAccessType.NONE) 22 | public static class Body { 23 | 24 | @XmlElement(name = "items") 25 | private TownCreateParams items; 26 | } 27 | 28 | public TownCreateParams getBodyItems() { 29 | Objects.requireNonNull(body, "시군구 조회 api 응답 바디가 널입니다."); 30 | return body.getItems(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pet/Application.java: -------------------------------------------------------------------------------- 1 | package com.pet; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.TimeZone; 5 | import javax.annotation.PostConstruct; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 10 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 11 | import org.springframework.scheduling.annotation.EnableScheduling; 12 | 13 | @Slf4j 14 | @EnableScheduling 15 | @SpringBootApplication 16 | @ConfigurationPropertiesScan("com.pet.common.property") 17 | @EnableAspectJAutoProxy 18 | public class Application { 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(Application.class, args); 22 | } 23 | 24 | @PostConstruct 25 | public void started() { 26 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); 27 | log.info("server time: {}", LocalDateTime.now()); 28 | } 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/ShelterPostCustomRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.post.domain.ShelterPost; 5 | import com.pet.domains.post.dto.serach.PostSearchParam; 6 | import com.pet.domains.post.repository.projection.ShelterPostWithFetch; 7 | import com.pet.domains.post.repository.projection.ShelterPostWithIsBookmark; 8 | import java.util.Optional; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | 12 | public interface ShelterPostCustomRepository { 13 | 14 | Page findAllWithFetch(Pageable pageable, PostSearchParam postSearchParam); 15 | 16 | Page findAllByAccountBookmarkWithFetch(Account account, Pageable pageable); 17 | 18 | Page findAllWithIsBookmark(Account account, Pageable pageable, 19 | PostSearchParam postSearchParam); 20 | 21 | Optional findByIdWithIsBookmark(Account account, Long postId); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/AccountAreaUpdateParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import java.util.List; 4 | import javax.validation.constraints.NotBlank; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class AccountAreaUpdateParam { 12 | 13 | private List areas; 14 | 15 | private boolean notification; 16 | 17 | public AccountAreaUpdateParam(List areas, boolean notification) { 18 | this.areas = areas; 19 | this.notification = notification; 20 | } 21 | 22 | @Getter 23 | public static class Area { 24 | 25 | @NotBlank(message = "'시/군/구'를 입력해주세요.") 26 | private Long townId; 27 | 28 | private boolean defaultArea; 29 | 30 | public Area(Long townId, boolean defaultArea) { 31 | this.townId = townId; 32 | this.defaultArea = defaultArea; 33 | } 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/controller/CityController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.controller; 2 | 3 | import com.pet.common.response.ApiResponse; 4 | import com.pet.domains.area.dto.response.CityReadResults; 5 | import com.pet.domains.area.service.CityService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RestController 15 | @RequestMapping("/api/v1/cities") 16 | @RequiredArgsConstructor 17 | public class CityController { 18 | 19 | private final CityService cityService; 20 | 21 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 22 | @ResponseStatus(HttpStatus.OK) 23 | public ApiResponse getCities() { 24 | return ApiResponse.ok(cityService.getAllTownAndCity()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/controller/AnimalController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.controller; 2 | 3 | import com.pet.common.response.ApiResponse; 4 | import com.pet.domains.animal.dto.response.AnimalReadResults; 5 | import com.pet.domains.animal.service.AnimalService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RequestMapping("/api/v1/animals") 15 | @RequiredArgsConstructor 16 | @RestController 17 | public class AnimalController { 18 | 19 | private final AnimalService animalService; 20 | 21 | @ResponseStatus(HttpStatus.OK) 22 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 23 | public ApiResponse getAnimals() { 24 | return ApiResponse.ok(animalService.getAnimals()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/dto/response/AnimalKindApiPageResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.dto.response; 2 | 3 | import com.pet.domains.animal.dto.request.AnimalKindCreateParams; 4 | import java.util.Objects; 5 | import javax.xml.bind.annotation.XmlAccessType; 6 | import javax.xml.bind.annotation.XmlAccessorType; 7 | import javax.xml.bind.annotation.XmlElement; 8 | import javax.xml.bind.annotation.XmlRootElement; 9 | import lombok.Getter; 10 | 11 | @Getter 12 | @XmlRootElement(name = "response") 13 | @XmlAccessorType(XmlAccessType.NONE) 14 | public class AnimalKindApiPageResults { 15 | 16 | @XmlElement(name = "body") 17 | private AnimalKindApiPageResults.Body body; 18 | 19 | @Getter 20 | @XmlRootElement(name = "body") 21 | @XmlAccessorType(XmlAccessType.NONE) 22 | public static class Body { 23 | 24 | @XmlElement(name = "items") 25 | private AnimalKindCreateParams items; 26 | 27 | } 28 | 29 | public AnimalKindCreateParams getBodyItems() { 30 | Objects.requireNonNull(body, "품종 조회 api 응답 바디가 널입니다."); 31 | return body.getItems(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/projection/MissingPostWithFetch.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository.projection; 2 | 3 | 4 | import com.pet.domains.animal.domain.Animal; 5 | import com.pet.domains.animal.domain.AnimalKind; 6 | import com.pet.domains.area.domain.City; 7 | import com.pet.domains.area.domain.Town; 8 | import com.pet.domains.post.domain.MissingPost; 9 | import com.querydsl.core.annotations.QueryProjection; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor 15 | public class MissingPostWithFetch { 16 | 17 | private MissingPost missingPost; 18 | 19 | private Animal animal; 20 | 21 | private AnimalKind animalKind; 22 | 23 | private City city; 24 | 25 | private Town town; 26 | 27 | @QueryProjection 28 | public MissingPostWithFetch(MissingPost missingPost, Animal animal, 29 | AnimalKind animalKind, City city, Town town) { 30 | this.missingPost = missingPost; 31 | this.animal = animal; 32 | this.animalKind = animalKind; 33 | this.city = city; 34 | this.town = town; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/projection/ShelterPostWithFetch.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository.projection; 2 | 3 | 4 | import com.pet.domains.animal.domain.Animal; 5 | import com.pet.domains.animal.domain.AnimalKind; 6 | import com.pet.domains.area.domain.City; 7 | import com.pet.domains.area.domain.Town; 8 | import com.pet.domains.post.domain.ShelterPost; 9 | import com.querydsl.core.annotations.QueryProjection; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor 15 | public class ShelterPostWithFetch { 16 | 17 | private ShelterPost shelterPost; 18 | 19 | private Animal animal; 20 | 21 | private AnimalKind animalKind; 22 | 23 | private City city; 24 | 25 | private Town town; 26 | 27 | @QueryProjection 28 | public ShelterPostWithFetch(ShelterPost shelterPost, Animal animal, 29 | AnimalKind animalKind, City city, Town town) { 30 | this.shelterPost = shelterPost; 31 | this.animal = animal; 32 | this.animalKind = animalKind; 33 | this.city = city; 34 | this.town = town; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/MissingPostCustomRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.post.domain.MissingPost; 5 | import com.pet.domains.post.dto.serach.PostSearchParam; 6 | import com.pet.domains.post.repository.projection.MissingPostWithFetch; 7 | import com.pet.domains.post.repository.projection.MissingPostWithIsBookmark; 8 | import java.util.Optional; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | 12 | public interface MissingPostCustomRepository { 13 | 14 | Page findMissingPostAllWithFetch(Pageable pageable, PostSearchParam postSearchParam); 15 | 16 | Page findMissingPostAllByAccountBookmarkWithFetch(Account account, Pageable pageable); 17 | 18 | Page findMissingPostAllWithIsBookmark( 19 | Account account, 20 | Pageable pageable, 21 | PostSearchParam postSearchParam 22 | ); 23 | 24 | Optional findMissingPostByIdWithIsBookmark(Account account, Long postId); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/HandlerMethodResolverArgumentConfig.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth; 2 | 3 | import com.pet.domains.account.service.LoginService; 4 | import com.pet.domains.auth.controller.argumentresolver.JwtAuthenticationArgumentResolver; 5 | import java.util.List; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | 12 | @Configuration 13 | @RequiredArgsConstructor 14 | public class HandlerMethodResolverArgumentConfig implements WebMvcConfigurer { 15 | 16 | private final LoginService loginService; 17 | 18 | @Override 19 | public void addArgumentResolvers(List argumentResolvers) { 20 | argumentResolvers.add(jwtAuthenticationArgumentResolver()); 21 | } 22 | 23 | @Bean 24 | public JwtAuthenticationArgumentResolver jwtAuthenticationArgumentResolver() { 25 | return new JwtAuthenticationArgumentResolver(loginService); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/controller/PostStatisticsController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.controller; 2 | 3 | import com.pet.common.response.ApiResponse; 4 | import com.pet.domains.statistics.dto.response.PostStatisticsReadResult; 5 | import com.pet.domains.statistics.service.PostStatisticsService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RequiredArgsConstructor 15 | @RequestMapping("/api/v1/statistics") 16 | @RestController 17 | public class PostStatisticsController { 18 | 19 | private final PostStatisticsService postStatisticsService; 20 | 21 | @ResponseStatus(HttpStatus.OK) 22 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 23 | public ApiResponse getPostStatistics() { 24 | return ApiResponse.ok(postStatisticsService.getPostStatistics()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | 9 | public interface AccountRepository extends JpaRepository { 10 | 11 | @Query("select a " 12 | + "from Account a" 13 | + " join fetch a.group g left join fetch g.permissions gp join fetch gp.permission " 14 | + "where a.email = :email") 15 | Optional findByEmail(String email); 16 | 17 | boolean existsByEmail(String email); 18 | 19 | @Query("select a from Account a left join fetch a.image where a.id = :id") 20 | Optional findByIdAndImage(Long id); 21 | 22 | @Query("select a from Account a inner join InterestArea ia" 23 | + " on a.id = ia.account.id and ia.selected = true and ia.town.id = :townId" 24 | + " where a.notification = true and a.id != :publisherAccountId") 25 | List findAllByNotificationSubscribers(Long townId, Long publisherAccountId); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/service/AnimalService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.service; 2 | 3 | import com.pet.domains.animal.dto.response.AnimalReadResults; 4 | import com.pet.domains.animal.mapper.AnimalMapper; 5 | import com.pet.domains.animal.repository.AnimalRepository; 6 | import java.util.List; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.cache.annotation.Cacheable; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @Slf4j 14 | @Transactional(readOnly = true) 15 | @RequiredArgsConstructor 16 | @Service 17 | public class AnimalService { 18 | 19 | private final AnimalRepository animalRepository; 20 | 21 | private final AnimalMapper animalMapper; 22 | 23 | @Cacheable( 24 | cacheNames = "animals", 25 | unless = "#result == null || #result.getAnimals().isEmpty()" 26 | ) 27 | public AnimalReadResults getAnimals() { 28 | List result = 29 | animalMapper.toAnimalReadResult(animalRepository.findAllWithAnimalKinds()); 30 | return animalMapper.toAnimalReadResults(result); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/docs/CustomResponseFieldsSnippet.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.docs; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.restdocs.operation.Operation; 7 | import org.springframework.restdocs.payload.AbstractFieldsSnippet; 8 | import org.springframework.restdocs.payload.FieldDescriptor; 9 | import org.springframework.restdocs.payload.PayloadSubsectionExtractor; 10 | 11 | public class CustomResponseFieldsSnippet extends AbstractFieldsSnippet { 12 | 13 | public CustomResponseFieldsSnippet(String type, PayloadSubsectionExtractor subsectionExtractor, 14 | List descriptors, Map attributes, 15 | boolean ignoreUndocumentedFields) { 16 | super(type, descriptors, attributes, ignoreUndocumentedFields, 17 | subsectionExtractor); 18 | } 19 | 20 | 21 | @Override 22 | protected MediaType getContentType(Operation operation) { 23 | return operation.getResponse().getHeaders().getContentType(); 24 | } 25 | 26 | @Override 27 | protected byte[] getContent(Operation operation) { 28 | return operation.getResponse().getContent(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/projection/MissingPostWithIsBookmark.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository.projection; 2 | 3 | 4 | import com.pet.domains.animal.domain.Animal; 5 | import com.pet.domains.animal.domain.AnimalKind; 6 | import com.pet.domains.area.domain.City; 7 | import com.pet.domains.area.domain.Town; 8 | import com.pet.domains.post.domain.MissingPost; 9 | import com.querydsl.core.annotations.QueryProjection; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor 15 | public class MissingPostWithIsBookmark { 16 | 17 | private MissingPost missingPost; 18 | 19 | private Animal animal; 20 | 21 | private AnimalKind animalKind; 22 | 23 | private City city; 24 | 25 | private Town town; 26 | 27 | private boolean isBookmark; 28 | 29 | @QueryProjection 30 | public MissingPostWithIsBookmark(MissingPost missingPost, Animal animal, 31 | AnimalKind animalKind, City city, Town town, boolean isBookmark) { 32 | this.missingPost = missingPost; 33 | this.animal = animal; 34 | this.animalKind = animalKind; 35 | this.city = city; 36 | this.town = town; 37 | this.isBookmark = isBookmark; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/projection/ShelterPostWithIsBookmark.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository.projection; 2 | 3 | 4 | import com.pet.domains.animal.domain.Animal; 5 | import com.pet.domains.animal.domain.AnimalKind; 6 | import com.pet.domains.area.domain.City; 7 | import com.pet.domains.area.domain.Town; 8 | import com.pet.domains.post.domain.ShelterPost; 9 | import com.querydsl.core.annotations.QueryProjection; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor 15 | public class ShelterPostWithIsBookmark { 16 | 17 | private ShelterPost shelterPost; 18 | 19 | private Animal animal; 20 | 21 | private AnimalKind animalKind; 22 | 23 | private City city; 24 | 25 | private Town town; 26 | 27 | private boolean isBookmark; 28 | 29 | @QueryProjection 30 | public ShelterPostWithIsBookmark(ShelterPost shelterPost, Animal animal, 31 | AnimalKind animalKind, City city, Town town, boolean isBookmark) { 32 | this.shelterPost = shelterPost; 33 | this.animal = animal; 34 | this.animalKind = animalKind; 35 | this.city = city; 36 | this.town = town; 37 | this.isBookmark = isBookmark; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar Build 2 | on: 3 | push: 4 | branches: [main, develop] 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | jobs: 8 | build: 9 | name: sonar analysis 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 15 | - name: Set up JDK 14 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 14 19 | - name: Cache SonarCloud packages 20 | uses: actions/cache@v1 21 | with: 22 | path: ~/.sonar/cache 23 | key: ${{ runner.os }}-sonar 24 | restore-keys: ${{ runner.os }}-sonar 25 | - name: Cache Gradle packages 26 | uses: actions/cache@v1 27 | with: 28 | path: ~/.gradle/caches 29 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 30 | restore-keys: ${{ runner.os }}-gradle 31 | - name: Build and analyze 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 34 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 35 | run: ./gradlew build sonarqube --info 36 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/service/S3UploadService.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.service; 2 | 3 | import com.amazonaws.services.s3.AmazonS3Client; 4 | import com.amazonaws.services.s3.model.CannedAccessControlList; 5 | import com.amazonaws.services.s3.model.ObjectMetadata; 6 | import com.amazonaws.services.s3.model.PutObjectRequest; 7 | import java.io.InputStream; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.stereotype.Component; 11 | 12 | @RequiredArgsConstructor 13 | @Component 14 | public class S3UploadService implements UploadService { 15 | 16 | private final AmazonS3Client amazonS3Client; 17 | 18 | @Value("${cloud.aws.s3.bucket}") 19 | private String bucket; 20 | 21 | @Override 22 | public void uploadFile(InputStream inputStream, ObjectMetadata objectMetadata, String fileName) { 23 | amazonS3Client.putObject( 24 | new PutObjectRequest(bucket, fileName, inputStream, objectMetadata).withCannedAcl( 25 | CannedAccessControlList.PublicRead)); 26 | } 27 | 28 | @Override 29 | public String getFileUrl(String fileName) { 30 | return amazonS3Client.getUrl(bucket, fileName).toString(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/account/domain/ProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import static org.assertj.core.api.Assertions.*; 6 | 7 | @DisplayName("로그인 Provider 테스트") 8 | class ProviderTest { 9 | 10 | @Test 11 | @DisplayName("local provider 조회 테스트") 12 | void findLocalProviderTest() { 13 | Provider provider = Provider.findByType("local"); 14 | assertThat(provider.getType()).isEqualTo("local"); 15 | } 16 | 17 | @Test 18 | @DisplayName("google provider 조회 테스트") 19 | void findGoogleProviderTest() { 20 | Provider provider = Provider.findByType("google"); 21 | assertThat(provider.getType()).isEqualTo("google"); 22 | } 23 | 24 | @Test 25 | @DisplayName("kakao provider 조회 테스트") 26 | void findKakaoProviderTest() { 27 | Provider provider = Provider.findByType("kakao"); 28 | assertThat(provider.getType()).isEqualTo("kakao"); 29 | } 30 | 31 | @Test 32 | @DisplayName("naver provider 조회 테스트") 33 | void findNaverProviderTest() { 34 | Provider provider = Provider.findByType("naver"); 35 | assertThat(provider.getType()).isEqualTo("naver"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/oauth2/NaverUser.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.oauth2; 2 | 3 | import java.util.Map; 4 | 5 | public class NaverUser implements Oauth2User { 6 | 7 | private static final String NAVER_OAUTH = "response"; 8 | 9 | private final String nickname; 10 | 11 | private final String email; 12 | 13 | private final String profileImage; 14 | 15 | public NaverUser(String nickname, String email, String profileImage) { 16 | this.nickname = nickname; 17 | this.email = email; 18 | this.profileImage = profileImage; 19 | } 20 | 21 | @Override 22 | public String getNickname(Map attributes) { 23 | return (String)getNaverAttributes(attributes).get(nickname); 24 | } 25 | 26 | @Override 27 | public String getEmail(Map attributes) { 28 | return (String)getNaverAttributes(attributes).get(email); 29 | } 30 | 31 | @Override 32 | public String getProfileImage(Map attributes) { 33 | return (String)getNaverAttributes(attributes).get(profileImage); 34 | } 35 | 36 | private Map getNaverAttributes(Map attributes) { 37 | return (Map)attributes.get(NAVER_OAUTH); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import com.pet.common.exception.CustomAsyncExceptionHandler; 4 | import java.util.concurrent.Executor; 5 | import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.scheduling.annotation.AsyncConfigurerSupport; 8 | import org.springframework.scheduling.annotation.EnableAsync; 9 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 10 | 11 | 12 | @EnableAsync 13 | @Configuration 14 | public class AsyncConfig extends AsyncConfigurerSupport { 15 | 16 | @Override 17 | public Executor getAsyncExecutor() { 18 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 19 | executor.setCorePoolSize(10); // 쓰레드풀의 디폴드 갯수 (첫 요청이 들어온 순간 생성) 20 | executor.setMaxPoolSize(20); // 큐가 꽉찰 경우에 max 만큼 늘린다. 21 | executor.setQueueCapacity(100); 22 | executor.setThreadNamePrefix("my thread-"); 23 | executor.initialize(); 24 | return executor; 25 | } 26 | 27 | @Override 28 | public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { 29 | return new CustomAsyncExceptionHandler(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/oauth2/KakaoUser.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.oauth2; 2 | 3 | import java.util.Map; 4 | 5 | public class KakaoUser implements Oauth2User { 6 | 7 | private static final String KAKAO_OAUTH = "properties"; 8 | 9 | private final String nickname; 10 | 11 | private final String email; 12 | 13 | private final String profileImage; 14 | 15 | public KakaoUser(String nickname, String email, String profileImage) { 16 | this.nickname = nickname; 17 | this.email = email; 18 | this.profileImage = profileImage; 19 | } 20 | 21 | @Override 22 | public String getNickname(Map attributes) { 23 | return (String)getKakaoAttributes(attributes).get(nickname); 24 | } 25 | 26 | @Override 27 | public String getEmail(Map attributes) { 28 | return (String)getKakaoAttributes(attributes).get(email); 29 | } 30 | 31 | @Override 32 | public String getProfileImage(Map attributes) { 33 | return (String)getKakaoAttributes(attributes).get(profileImage); 34 | } 35 | 36 | private Map getKakaoAttributes(Map attributes) { 37 | return (Map)attributes.get(KAKAO_OAUTH); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/repository/MissingPostRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.post.domain.MissingPost; 5 | import java.util.Optional; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.jpa.repository.EntityGraph; 9 | import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; 10 | import org.springframework.data.jpa.repository.JpaRepository; 11 | import org.springframework.data.jpa.repository.Query; 12 | 13 | public interface MissingPostRepository extends JpaRepository, MissingPostCustomRepository { 14 | 15 | @EntityGraph( 16 | attributePaths = {"animalKind", "animalKind.animal", "town", "town.city", "account"}, 17 | type = EntityGraphType.LOAD 18 | ) 19 | @Query("SELECT mp FROM MissingPost mp WHERE mp.id = :postId") 20 | Optional findByMissingPostId(Long postId); 21 | 22 | 23 | @EntityGraph(attributePaths = {"animalKind", "town", "town.city"}, 24 | type = EntityGraphType.LOAD) 25 | Page findByAccountId(Long accountId, Pageable pageable); 26 | 27 | void deleteAllByAccount(Account account); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/docs/utils/ApiDocumentUtils.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.docs.utils; 2 | 3 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; 4 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; 5 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; 6 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; 7 | import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; 8 | import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; 9 | 10 | public class ApiDocumentUtils { 11 | 12 | private ApiDocumentUtils() { 13 | throw new AssertionError("유틸 클래스입니다."); 14 | } 15 | 16 | public static OperationRequestPreprocessor getDocumentRequest() { 17 | return preprocessRequest( 18 | modifyUris() 19 | .scheme("http") 20 | .host("ec2-3-35-254-102.ap-northeast-2.compute.amazonaws.com") 21 | .port(26134), 22 | prettyPrint()); 23 | } 24 | 25 | public static OperationResponsePreprocessor getDocumentResponse() { 26 | return preprocessResponse(prettyPrint()); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/domain/SexType.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.domain; 2 | 3 | import com.pet.domains.EnumType; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | import lombok.Getter; 9 | 10 | @Getter 11 | public enum SexType implements EnumType { 12 | MALE("수컷", "M"), 13 | FEMALE("암컷", "F"), 14 | UNKNOWN("모름", "Q"); 15 | 16 | private static final Map sexTypeByAbbr = Stream.of(SexType.values()) 17 | .collect(Collectors.toUnmodifiableMap(SexType::getAbbreviation, value -> value)); 18 | 19 | 20 | private final String text; 21 | 22 | private final String abbreviation; 23 | 24 | SexType(String text, String abbreviation) { 25 | this.text = text; 26 | this.abbreviation = abbreviation; 27 | } 28 | 29 | @Override 30 | public String getName() { 31 | return this.name(); 32 | } 33 | 34 | @Override 35 | public String getText() { 36 | return text; 37 | } 38 | 39 | 40 | public static SexType findSexType(String abbreviation) { 41 | return Optional.ofNullable(sexTypeByAbbr.get(abbreviation)) 42 | .orElseThrow(() -> new IllegalArgumentException("No match sex type")); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/mapper/InterestAreaMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.mapper; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.account.dto.request.AccountAreaUpdateParam; 5 | import com.pet.domains.account.dto.response.AccountAreaReadResults; 6 | import com.pet.domains.area.domain.InterestArea; 7 | import com.pet.domains.area.domain.Town; 8 | import org.mapstruct.Mapper; 9 | import org.mapstruct.Mapping; 10 | import org.mapstruct.Mappings; 11 | 12 | @Mapper(componentModel = "spring") 13 | public interface InterestAreaMapper { 14 | 15 | @Mappings({ 16 | @Mapping(target = "id", source = "interestArea.id"), 17 | @Mapping(target = "cityId", source = "interestArea.town.city.id"), 18 | @Mapping(target = "cityName", source = "interestArea.town.city.name"), 19 | @Mapping(target = "townId", source = "interestArea.town.id"), 20 | @Mapping(target = "townName", source = "interestArea.town.name"), 21 | @Mapping(target = "defaultArea", source = "interestArea.selected"), 22 | }) 23 | AccountAreaReadResults.Area toAreaResult(InterestArea interestArea, boolean checked); 24 | 25 | @Mapping(target = "selected", source = "area.defaultArea") 26 | InterestArea toEntity(Account account, AccountAreaUpdateParam.Area area, Town town); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/dto/response/ShelterApiPageResult.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.dto.response; 2 | 3 | import com.pet.domains.post.dto.request.ShelterPostCreateParams; 4 | import java.util.Objects; 5 | import javax.xml.bind.annotation.XmlAccessType; 6 | import javax.xml.bind.annotation.XmlAccessorType; 7 | import javax.xml.bind.annotation.XmlElement; 8 | import javax.xml.bind.annotation.XmlRootElement; 9 | import lombok.Getter; 10 | 11 | 12 | @Getter 13 | @XmlRootElement(name = "response") 14 | @XmlAccessorType(XmlAccessType.NONE) 15 | public class ShelterApiPageResult { 16 | 17 | @XmlElement(name = "body") 18 | private ShelterApiPageResult.Body body; 19 | 20 | @Getter 21 | @XmlRootElement(name = "body") 22 | @XmlAccessorType(XmlAccessType.NONE) 23 | public static class Body { 24 | 25 | @XmlElement(name = "numOfRows") 26 | private Long numOfRows; 27 | 28 | @XmlElement(name = "pageNo") 29 | private Long pageNo; 30 | 31 | @XmlElement(name = "totalCount") 32 | private Long totalCount; 33 | 34 | @XmlElement(name = "items") 35 | private ShelterPostCreateParams items; 36 | } 37 | 38 | public ShelterPostCreateParams getBodyItems() { 39 | Objects.requireNonNull(body, "보호소 동물 조회 api 응답 바디가 널입니다."); 40 | return body.getItems(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/tag/domain/Tag.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.tag.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.Index; 10 | import javax.persistence.Table; 11 | import lombok.AccessLevel; 12 | import lombok.Builder; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | import org.apache.commons.lang3.ObjectUtils; 16 | import org.hibernate.annotations.Formula; 17 | 18 | @Getter 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @Entity 21 | @Table(name = "tag", indexes = @Index(name = "idx_name", columnList = "name")) 22 | public class Tag extends BaseEntity { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | @Column(name = "id", updatable = false) 27 | private Long id; 28 | 29 | @Column(name = "name", nullable = false, unique = true) 30 | private String name; 31 | 32 | @Formula("(select count(*) from post_tag pt where pt.tag_id = id)") 33 | private long count; 34 | 35 | @Builder 36 | public Tag(String name) { 37 | ObjectUtils.requireNonEmpty(name, "name must not be null"); 38 | 39 | this.name = name; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/mapper/PostStatisticsMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.mapper; 2 | 3 | import com.pet.domains.post.domain.Status; 4 | import com.pet.domains.statistics.domain.PostStatistics; 5 | import com.pet.domains.statistics.dto.response.PostStatisticsReadResult; 6 | import com.pet.domains.statistics.repository.PostCountByStatus; 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | import org.mapstruct.Mapper; 12 | 13 | @Mapper(componentModel = "spring") 14 | public interface PostStatisticsMapper { 15 | 16 | default PostStatistics toEntity(List postCountByStatuses, LocalDateTime now) { 17 | Map statusCountMap = postCountByStatuses.stream() 18 | .collect(Collectors.toMap(PostCountByStatus::getPostStatus, PostCountByStatus::getCount)); 19 | 20 | return PostStatistics.builder() 21 | .missing(statusCountMap.getOrDefault(Status.MISSING, 0L)) 22 | .protection(statusCountMap.getOrDefault(Status.PROTECTION, 0L)) 23 | .detection(statusCountMap.getOrDefault(Status.DETECTION, 0L)) 24 | .completion(statusCountMap.getOrDefault(Status.COMPLETION, 0L)) 25 | .date(now) 26 | .build(); 27 | } 28 | 29 | PostStatisticsReadResult toReadResult(PostStatistics postStatisticsEntity); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/validator/ImageContentTypeValidator.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.validator; 2 | 3 | import java.util.Objects; 4 | import javax.validation.ConstraintValidator; 5 | import javax.validation.ConstraintValidatorContext; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | public class ImageContentTypeValidator implements ConstraintValidator { 9 | 10 | @Override 11 | public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) { 12 | boolean result = true; 13 | 14 | String contentType = multipartFile.getContentType(); 15 | if (!isSupportedContentType(Objects.requireNonNull(contentType))) { 16 | context.disableDefaultConstraintViolation(); 17 | context.buildConstraintViolationWithTemplate( 18 | "Only PNG or JPG or JPEG or BMP or GIF or TIFF images are allowed.") 19 | .addConstraintViolation(); 20 | result = false; 21 | } 22 | return result; 23 | } 24 | 25 | private boolean isSupportedContentType(String contentType) { 26 | return contentType.equals("image/png") 27 | || contentType.equals("image/jpg") 28 | || contentType.equals("image/jpeg") 29 | || contentType.equals("image/bmp") 30 | || contentType.equals("image/gif") 31 | || contentType.equals("image/tiff"); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import com.pet.common.property.RedisProperties; 4 | import com.pet.common.property.RedisProperties.Redis; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.data.redis.connection.RedisConnectionFactory; 11 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | 14 | @RequiredArgsConstructor 15 | @Configuration 16 | @Profile({"dev", "prod"}) 17 | public class RedisConfig { 18 | 19 | private final RedisProperties redisProperties; 20 | 21 | @Bean 22 | @ConditionalOnMissingBean(RedisConnectionFactory.class) 23 | public RedisConnectionFactory redisConnectionFactory() { 24 | Redis redis = redisProperties.getRedis(); 25 | return new LettuceConnectionFactory(redis.getHost(), redis.getPort()); 26 | } 27 | 28 | @Bean 29 | public RedisTemplate redisTemplate() { 30 | RedisTemplate redisTemplate = new RedisTemplate<>(); 31 | redisTemplate.setConnectionFactory(redisConnectionFactory()); 32 | return redisTemplate; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/dto/serach/PostSearchParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.dto.serach; 2 | 3 | import com.pet.domains.post.domain.SexType; 4 | import com.pet.domains.post.domain.Status; 5 | import java.time.LocalDate; 6 | import javax.validation.constraints.PastOrPresent; 7 | import javax.validation.constraints.Positive; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import org.springframework.format.annotation.DateTimeFormat; 12 | 13 | @Getter 14 | @Setter 15 | public class PostSearchParam { 16 | 17 | @Positive 18 | private Long city; 19 | 20 | @Positive 21 | private Long town; 22 | 23 | @Positive 24 | private Long animal; 25 | 26 | @Positive 27 | private Long animalKind; 28 | 29 | private SexType sex; 30 | 31 | @DateTimeFormat(pattern = "yyyy-MM-dd") 32 | @PastOrPresent 33 | private LocalDate start; 34 | 35 | @DateTimeFormat(pattern = "yyyy-MM-dd") 36 | private LocalDate end; 37 | 38 | private Status status; 39 | 40 | @Builder 41 | public PostSearchParam(Long city, Long town, Long animal, Long animalKind, SexType sex, LocalDate start, 42 | LocalDate end, Status status) { 43 | this.city = city; 44 | this.town = town; 45 | this.animal = animal; 46 | this.animalKind = animalKind; 47 | this.sex = sex; 48 | this.start = start; 49 | this.end = end; 50 | this.status = status; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/pet/common/response/ApiResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.response; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 5 | import static org.assertj.core.api.Assertions.assertThatNullPointerException; 6 | import static org.junit.jupiter.api.Assertions.assertNotNull; 7 | import java.util.Collections; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | 11 | @DisplayName("Api Response 테스트") 12 | class ApiResponseTest { 13 | 14 | @Test 15 | @DisplayName("ApiResponse 생성 테스트") 16 | void newApiResponseTest() { 17 | ApiResponse testResponse = ApiResponse.ok("Test"); 18 | 19 | assertThat(testResponse.getData()).isEqualTo("Test"); 20 | assertNotNull(testResponse.getServerDateTime()); 21 | } 22 | 23 | @Test 24 | @DisplayName("ApiResponse null 주입 테스트") 25 | void apiResponseIsNullTest() { 26 | assertThatNullPointerException() 27 | .isThrownBy(() -> new ApiResponse<>(null)) 28 | .withMessage("data must be not null"); 29 | } 30 | 31 | @Test 32 | @DisplayName("ApiResponse empty 주입 테스트") 33 | void apiResponseIsEmptyDataTest() { 34 | assertThatIllegalArgumentException() 35 | .isThrownBy(() -> new ApiResponse(Collections.emptyList())) 36 | .withMessage("data must be not null"); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/domain/City.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.OneToMany; 12 | import javax.persistence.Table; 13 | import lombok.AccessLevel; 14 | import lombok.Builder; 15 | import lombok.Getter; 16 | import lombok.NoArgsConstructor; 17 | import org.apache.commons.lang3.Validate; 18 | 19 | @Getter 20 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 21 | @Entity 22 | @Table(name = "city") 23 | public class City extends BaseEntity { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | @Column(name = "id", updatable = false) 28 | private Long id; 29 | 30 | @Column(name = "code", nullable = false, length = 10) 31 | private String code; 32 | 33 | @Column(name = "name", nullable = false, length = 200, unique = true) 34 | private String name; 35 | 36 | @OneToMany(mappedBy = "city") 37 | private List towns = new ArrayList<>(); 38 | 39 | @Builder 40 | public City(String code, String name) { 41 | Validate.notBlank(code, "code must not be blank"); 42 | Validate.notBlank(name, "code must not be blank"); 43 | 44 | this.code = code; 45 | this.name = name; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/domain/Email.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import java.io.Serializable; 5 | import java.time.LocalDateTime; 6 | import lombok.Getter; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.redis.core.RedisHash; 10 | 11 | @Getter 12 | @RedisHash("email") 13 | public class Email implements Serializable { 14 | 15 | private static final Long EXPIRATION = 3L; 16 | 17 | @Id 18 | private String email; 19 | 20 | private String verifyKey; 21 | 22 | private boolean checked; 23 | 24 | private LocalDateTime createdAt; 25 | 26 | public Email(String email, String verifyKey) { 27 | this.email = email; 28 | this.verifyKey = verifyKey; 29 | this.createdAt = LocalDateTime.now(); 30 | } 31 | 32 | public boolean isVerifyTime(LocalDateTime requestTime) { 33 | return this.createdAt.plusMinutes(EXPIRATION).isAfter(requestTime); 34 | } 35 | 36 | public void successVerified() { 37 | this.checked = true; 38 | } 39 | 40 | public boolean isVerifyEmail(String email) { 41 | if (StringUtils.equals(this.email, email) && checked) { 42 | return true; 43 | } 44 | throw ExceptionMessage.INVALID_SIGN_UP.getException(); 45 | } 46 | 47 | public boolean verify(String key) { 48 | return verifyKey.equals(key); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/pet/infra/HtmlMailSender.java: -------------------------------------------------------------------------------- 1 | package com.pet.infra; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import javax.mail.MessagingException; 5 | import javax.mail.internet.MimeMessage; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.mail.javamail.JavaMailSender; 10 | import org.springframework.mail.javamail.MimeMessageHelper; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | @Profile({"dev", "prod"}) 17 | public class HtmlMailSender implements MailSender { 18 | 19 | private final JavaMailSender javaMailSender; 20 | 21 | @Override 22 | public void send(EmailMessage emailMessage) { 23 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 24 | try { 25 | MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); 26 | mimeMessageHelper.setTo(emailMessage.getTo()); 27 | mimeMessageHelper.setSubject(emailMessage.getSubject()); 28 | mimeMessageHelper.setText(emailMessage.getMessage(), false); 29 | javaMailSender.send(mimeMessage); 30 | 31 | log.debug("이메일을 정상적으로 전송했습니다.: {}", emailMessage.getTo()); 32 | } catch (MessagingException e) { 33 | log.warn("메일 전송에 실패했습니다."); 34 | throw ExceptionMessage.FAIL_TO_EMAIL.getException(); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/domain/Group.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.OneToMany; 11 | import javax.persistence.Table; 12 | import lombok.AccessLevel; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | import org.springframework.security.core.GrantedAuthority; 16 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 17 | import static java.util.stream.Collectors.*; 18 | 19 | @Getter 20 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 21 | @Entity 22 | @Table(name = "user_group") 23 | public class Group { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | @Column(name = "id", updatable = false) 28 | private Long id; 29 | 30 | @Column(name = "name", length = 20, nullable = false) 31 | private String name; 32 | 33 | @OneToMany(mappedBy = "group") 34 | private List permissions = new ArrayList<>(); 35 | 36 | public Group(String name) { 37 | this.name = name; 38 | } 39 | 40 | public List getAuthorities() { 41 | return permissions.stream() 42 | .map(permission -> new SimpleGrantedAuthority(permission.getPermission().getName())) 43 | .collect(toList()); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/comment/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.comment.repository; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.comment.domain.Comment; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.data.jpa.repository.EntityGraph; 10 | import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; 11 | import org.springframework.data.jpa.repository.JpaRepository; 12 | import org.springframework.data.jpa.repository.Query; 13 | 14 | public interface CommentRepository extends JpaRepository { 15 | 16 | @EntityGraph(attributePaths = {"account", "missingPost"}, type = EntityGraphType.LOAD) 17 | @Query("select c from Comment c where c.id = :commentId and c.deleted = :deleted") 18 | Optional findByIdAndDeletedWithFetch(Long commentId, boolean deleted); 19 | 20 | void deleteAllByMissingPostId(Long postId); 21 | 22 | void deleteByIdAndAccount(Long commentId, Account account); 23 | 24 | List findAllByMissingPostId(Long postId); 25 | 26 | @EntityGraph( 27 | attributePaths = {"account", "account.image", "missingPost", "parentComment"}, 28 | type = EntityGraphType.LOAD 29 | ) 30 | @Query("select c from Comment c where c.missingPost.id = :postId and c.parentComment.id is null") 31 | Page findAllByMissingPostId(Long postId, Pageable pageable); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/domain/Animal.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.FetchType; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.OneToMany; 13 | import javax.persistence.Table; 14 | import lombok.AccessLevel; 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import org.apache.commons.lang3.ObjectUtils; 19 | 20 | @Getter 21 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 22 | @Entity 23 | @Table(name = "animal") 24 | public class Animal extends BaseEntity { 25 | 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | private Long id; 29 | 30 | @Column(name = "name", length = 50, nullable = false) 31 | private String name; 32 | 33 | @Column(name = "code", length = 30, nullable = false) 34 | private String code; 35 | 36 | @OneToMany(fetch = FetchType.LAZY, mappedBy = "animal") 37 | private List animalKinds = new ArrayList<>(); 38 | 39 | @Builder 40 | public Animal(String name, String code) { 41 | ObjectUtils.requireNonEmpty(name, "name must not be null"); 42 | ObjectUtils.requireNonEmpty(code, "code must not be null"); 43 | 44 | this.name = name; 45 | this.code = code; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/comment/mapper/CommentMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.comment.mapper; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.comment.domain.Comment; 5 | import com.pet.domains.comment.dto.response.CommentPageResults; 6 | import com.pet.domains.comment.dto.response.CommentWriteResult; 7 | import java.util.List; 8 | import org.mapstruct.Mapper; 9 | import org.mapstruct.Mapping; 10 | import org.springframework.data.domain.Page; 11 | 12 | @Mapper(componentModel = "spring") 13 | public interface CommentMapper { 14 | 15 | @Mapping(target = "image", source = "account.image.name") 16 | CommentPageResults.Comment.Account toAccountReadResult(Account account); 17 | 18 | List toChildCommentReadResults(List childComments); 19 | 20 | List toCommentResult(List comments); 21 | 22 | default CommentPageResults toCommentPageResults(Page commentPage) { 23 | return new CommentPageResults( 24 | toCommentResult(commentPage.getContent()), 25 | commentPage.getTotalElements(), 26 | commentPage.isLast(), 27 | commentPage.getSize() 28 | ); 29 | } 30 | 31 | List toChildCommentWriteResults(List childComments); 32 | 33 | @Mapping(target = "image", source = "account.image.name") 34 | CommentWriteResult.Account toAccountWriteResult(Account account); 35 | 36 | CommentWriteResult toCommentWriteResult(Comment comment); 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/service/ShelterPostBookmarkService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.service; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import com.pet.domains.account.domain.Account; 5 | import com.pet.domains.post.domain.ShelterPost; 6 | import com.pet.domains.post.domain.ShelterPostBookmark; 7 | import com.pet.domains.post.repository.ShelterPostBookmarkRepository; 8 | import com.pet.domains.post.repository.ShelterPostRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @Transactional(readOnly = true) 14 | @RequiredArgsConstructor 15 | @Service 16 | public class ShelterPostBookmarkService { 17 | 18 | private final ShelterPostBookmarkRepository shelterPostBookmarkRepository; 19 | 20 | private final ShelterPostRepository shelterPostRepository; 21 | 22 | @Transactional 23 | public void createPostBookmark(Long postId, Account account) { 24 | ShelterPost foundPost = shelterPostRepository.findById(postId) 25 | .orElseThrow(ExceptionMessage.NOT_FOUND_SHELTER_POST::getException); 26 | shelterPostBookmarkRepository.save( 27 | ShelterPostBookmark.builder() 28 | .shelterPost(foundPost) 29 | .account(account) 30 | .build() 31 | ); 32 | } 33 | 34 | @Transactional 35 | public void deletePostBookmark(Long postId, Account account) { 36 | shelterPostBookmarkRepository.deleteByShelterPostIdAndAccount(postId, account); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/service/MissingPostBookmarkService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.service; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import com.pet.domains.account.domain.Account; 5 | import com.pet.domains.post.domain.MissingPost; 6 | import com.pet.domains.post.domain.MissingPostBookmark; 7 | import com.pet.domains.post.repository.MissingPostBookmarkRepository; 8 | import com.pet.domains.post.repository.MissingPostRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @RequiredArgsConstructor 14 | @Transactional(readOnly = true) 15 | @Service 16 | public class MissingPostBookmarkService { 17 | 18 | private final MissingPostRepository missingPostRepository; 19 | 20 | private final MissingPostBookmarkRepository missingPostBookmarkRepository; 21 | 22 | @Transactional 23 | public void createMissingPostBookmark(Long postId, Account account) { 24 | MissingPost getMissingPost = missingPostRepository.findById(postId) 25 | .orElseThrow(ExceptionMessage.NOT_FOUND_MISSING_POST::getException); 26 | 27 | missingPostBookmarkRepository.save( 28 | MissingPostBookmark.builder() 29 | .account(account) 30 | .missingPost(getMissingPost) 31 | .build() 32 | ); 33 | } 34 | 35 | @Transactional 36 | public void deleteMissingPostBookmark(Long postId, Account account) { 37 | missingPostBookmarkRepository.deleteByAccountAndMissingPostId(account, postId); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/account/domain/AccountTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | import com.pet.domains.auth.domain.Group; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import static org.junit.jupiter.api.Assertions.*; 8 | import static org.mockito.Mockito.*; 9 | 10 | @DisplayName("회원 테스트") 11 | class AccountTest { 12 | 13 | @Test 14 | @DisplayName("비밀번호 일치 테스트") 15 | void checkPasswordTest() { 16 | BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); 17 | Account account = Account.builder() 18 | .email("tester@email.com") 19 | .nickname("tester") 20 | .group(new Group("group")) 21 | .password(passwordEncoder.encode("123123a!")) 22 | .build(); 23 | 24 | assertTrue(account.isMatchPassword(passwordEncoder, "123123a!")); 25 | } 26 | 27 | @Test 28 | @DisplayName("올바른 이메일 형식으로 회원 객체 생성 테스트") 29 | void validateEmail() { 30 | Account.builder() 31 | .email("tester@email.com") 32 | .nickname("tester") 33 | .group(new Group("group")) 34 | .build(); 35 | } 36 | 37 | @Test 38 | @DisplayName("올바르지 않은 이메일 형식으로 회원 객체 생성 테스트") 39 | void invalidEmailTest() { 40 | assertThrows(IllegalArgumentException.class, 41 | () -> Account.builder() 42 | .email("email.com") 43 | .nickname("tester") 44 | .group(new Group("group")) 45 | .build()); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/util/OptimisticLockingHandlingUtils.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.util; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import java.util.Optional; 5 | import java.util.function.Supplier; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.orm.ObjectOptimisticLockingFailureException; 8 | 9 | @Slf4j 10 | public class OptimisticLockingHandlingUtils { 11 | 12 | private OptimisticLockingHandlingUtils() { 13 | throw new AssertionError("유틸 클래스입니다."); 14 | } 15 | 16 | public static void handling(Runnable runnable, int handlingCount, String description) { 17 | boolean isFailure = true; 18 | for (int i = 0; i < handlingCount; i++) { 19 | try { 20 | runnable.run(); 21 | isFailure = false; 22 | break; 23 | } catch (ObjectOptimisticLockingFailureException ex) { 24 | log.warn("#{}: locking failure occurred when try {}", i, description); 25 | } 26 | } 27 | if (isFailure) { 28 | throw ExceptionMessage.SERVICE_UNAVAILABLE.getException(); 29 | } 30 | } 31 | 32 | public static Optional handling(Supplier supplier, int handlingCount, String description) { 33 | for (int i = 0; i < handlingCount; i++) { 34 | try { 35 | return Optional.ofNullable(supplier.get()); 36 | } catch (ObjectOptimisticLockingFailureException ex) { 37 | log.warn("#{}: locking failure occurred when try {}", i, description); 38 | } 39 | } 40 | return Optional.empty(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/dto/response/AnimalReadResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.dto.response; 2 | 3 | import java.util.List; 4 | import lombok.AccessLevel; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class AnimalReadResults { 12 | 13 | private List animals; 14 | 15 | private AnimalReadResults(List animals) { 16 | this.animals = animals; 17 | } 18 | 19 | public static AnimalReadResults of(List animals) { 20 | return new AnimalReadResults(animals); 21 | } 22 | 23 | @Getter 24 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 25 | public static class Animal { 26 | 27 | private Long id; 28 | 29 | private String name; 30 | 31 | private List kinds; 32 | 33 | @Builder 34 | public Animal(Long id, String name, List kinds) { 35 | this.id = id; 36 | this.name = name; 37 | this.kinds = kinds; 38 | } 39 | 40 | @Getter 41 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 42 | public static class AnimalKind { 43 | 44 | private Long id; 45 | 46 | private String name; 47 | 48 | @Builder 49 | public AnimalKind(Long id, String name) { 50 | this.id = id; 51 | this.name = name; 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "animals"; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/docs/controller/CommonDocumentationController.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.docs.controller; 2 | 3 | 4 | import com.pet.common.response.ApiResponse; 5 | import com.pet.domains.EnumType; 6 | import com.pet.domains.docs.dto.CommonDocumentationResults; 7 | import com.pet.domains.post.domain.NeuteredType; 8 | import com.pet.domains.post.domain.SexType; 9 | import com.pet.domains.post.domain.Status; 10 | import java.util.Arrays; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.ResponseStatus; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | public class CommonDocumentationController { 20 | 21 | @ResponseStatus(HttpStatus.OK) 22 | @GetMapping("/common") 23 | public ApiResponse findAll() { 24 | 25 | Map sexTypes = getDocs(SexType.values()); 26 | Map neuteredTypes = getDocs(NeuteredType.values()); 27 | Map status = getDocs(Status.values()); 28 | 29 | return ApiResponse.ok( 30 | CommonDocumentationResults.builder() 31 | .sexTypes(sexTypes) 32 | .neuteredTypes(neuteredTypes) 33 | .status(status) 34 | .build() 35 | ); 36 | } 37 | 38 | private Map getDocs(EnumType[] enumTypes) { 39 | return Arrays.stream(enumTypes) 40 | .collect(Collectors.toMap(EnumType::getName, EnumType::getText)); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/auth/domain/GroupPermission.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.auth.domain; 2 | 3 | import javax.persistence.CascadeType; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.ForeignKey; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.ManyToOne; 12 | import javax.persistence.Table; 13 | import lombok.AccessLevel; 14 | import lombok.Getter; 15 | import lombok.NoArgsConstructor; 16 | import org.springframework.web.bind.annotation.CrossOrigin; 17 | 18 | @Getter 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @Entity 21 | @Table(name = "group_permission") 22 | public class GroupPermission { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | @Column(name = "id", updatable = false) 27 | private Long id; 28 | 29 | @ManyToOne(cascade = CascadeType.ALL) 30 | @JoinColumn(name = "group_id", 31 | referencedColumnName = "id", 32 | foreignKey = @ForeignKey(name = "fk_group_to_group_permission"), 33 | nullable = false 34 | ) 35 | private Group group; 36 | 37 | @ManyToOne(cascade = CascadeType.ALL) 38 | @JoinColumn(name = "permission_id", 39 | referencedColumnName = "id", 40 | foreignKey = @ForeignKey(name = "fk_permission_to_group_permission"), 41 | nullable = false 42 | ) 43 | private Permission permission; 44 | 45 | public GroupPermission(Group group, Permission permission) { 46 | this.group = group; 47 | this.permission = permission; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/response/AccountAreaReadResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.response; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class AccountAreaReadResults { 8 | 9 | private final List areas; 10 | 11 | private final boolean checked; 12 | 13 | public AccountAreaReadResults(List areas, boolean checked) { 14 | this.areas = areas; 15 | this.checked = checked; 16 | } 17 | 18 | public static AccountAreaReadResults of(List areas, boolean checked) { 19 | return new AccountAreaReadResults(areas, checked); 20 | } 21 | 22 | @Getter 23 | public static class Area { 24 | 25 | private final Long id; 26 | 27 | private final Long cityId; 28 | 29 | private final String cityName; 30 | 31 | private final Long townId; 32 | 33 | private final String townName; 34 | 35 | private final boolean defaultArea; 36 | 37 | public Area(Long id, Long cityId, String cityName, Long townId, String townName, boolean defaultArea) { 38 | this.id = id; 39 | this.cityId = cityId; 40 | this.cityName = cityName; 41 | this.townId = townId; 42 | this.townName = townName; 43 | this.defaultArea = defaultArea; 44 | } 45 | 46 | public static Area of( 47 | Long id, Long cityId, String cityName, Long townId, String townName, boolean defaultArea 48 | ) { 49 | return new Area(id, cityId, cityName, townId, townName, defaultArea); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/request/AccountSignUpParam.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.request; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | import javax.validation.constraints.Pattern; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 12 | public class AccountSignUpParam { 13 | 14 | @NotBlank 15 | @Pattern(regexp = "^[가-힣a-zA-Z]{2,10}$", message = "올바른 닉네임 형식이 아닙니다.") 16 | private String nickname; 17 | 18 | @NotBlank(message = "올바른 이메일 형식이 아닙니다.") 19 | @Pattern(regexp = "\\b[\\w.-]+@[\\w.-]+\\.\\w{2,4}\\b", message = "올바른 이메일 형식이 아닙니다.") 20 | private String email; 21 | 22 | @NotBlank(message = "올바른 비밀번호 형식이 아닙니다.") 23 | @Pattern( 24 | regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\d~!@#$%^&*()+|=]{8,20}$", 25 | message = "올바른 비밀번호 형식이 아닙니다.") 26 | private String password; 27 | 28 | @NotBlank(message = "올바른 비밀번호 형식이 아닙니다.") 29 | @Pattern( 30 | regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\d~!@#$%^&*()+|=]{8,20}$", 31 | message = "올바른 비밀번호 형식이 아닙니다.") 32 | private String passwordCheck; 33 | 34 | @NotNull(message = "올바른 요청이 아닙니다.") 35 | private Long verifiedId; 36 | 37 | public AccountSignUpParam(String nickname, String email, String password, String passwordCheck, Long verifiedId) { 38 | this.nickname = nickname; 39 | this.email = email; 40 | this.password = password; 41 | this.passwordCheck = passwordCheck; 42 | this.verifiedId = verifiedId; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/mapper/AccountMapper.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.mapper; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.account.dto.response.AccountMissingPostPageResults; 5 | import com.pet.domains.account.dto.response.AccountReadResult; 6 | import com.pet.domains.post.domain.MissingPost; 7 | import com.pet.domains.tag.domain.PostTag; 8 | import com.pet.domains.tag.domain.Tag; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import org.mapstruct.Mapper; 12 | import org.mapstruct.Mapping; 13 | import org.mapstruct.Mappings; 14 | 15 | @Mapper(componentModel = "spring") 16 | public interface AccountMapper { 17 | 18 | @Mapping(target = "image", source = "account.image.name") 19 | AccountReadResult toReadResult(Account account); 20 | 21 | @Mappings({ 22 | @Mapping(target = "town", source = "missingPost.town.name"), 23 | @Mapping(target = "city", source = "missingPost.town.city.name"), 24 | @Mapping(target = "animalKind", source = "missingPost.animalKind.name"), 25 | @Mapping(target = "sex", source = "missingPost.sexType"), 26 | @Mapping(target = "postTags", expression = "java(toMissingPostTagResults(missingPost.getPostTags()))"), 27 | }) 28 | AccountMissingPostPageResults.Post toAccountMissingPostPageResults(MissingPost missingPost); 29 | 30 | AccountMissingPostPageResults.Post.Tag toTagDto(Tag tag); 31 | 32 | default List toMissingPostTagResults(List postTags) { 33 | return postTags.stream() 34 | .map(postTag -> toTagDto(postTag.getTag())) 35 | .collect(Collectors.toList()); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/domain/Town.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.FetchType; 7 | import javax.persistence.ForeignKey; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.JoinColumn; 12 | import javax.persistence.ManyToOne; 13 | import javax.persistence.Table; 14 | import lombok.AccessLevel; 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import org.apache.commons.lang3.Validate; 19 | 20 | 21 | @Getter 22 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 23 | @Entity 24 | @Table(name = "town") 25 | public class Town extends BaseEntity { 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | @Column(name = "id", updatable = false) 30 | private Long id; 31 | 32 | @Column(name = "code", length = 10) 33 | private String code; 34 | 35 | @Column(name = "name", nullable = false, length = 200) 36 | private String name; 37 | 38 | @ManyToOne(fetch = FetchType.LAZY) 39 | @JoinColumn( 40 | name = "city_id", 41 | nullable = false, 42 | foreignKey = @ForeignKey(name = "fk_town_to_city") 43 | ) 44 | private City city; 45 | 46 | @Builder 47 | public Town(String code, String name, City city) { 48 | Validate.notBlank(name, "name must not be null"); 49 | Validate.notNull(city, "city must not be null"); 50 | 51 | this.code = code; 52 | this.name = name; 53 | this.city = city; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/tag/repository/TagRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.tag.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import com.pet.common.config.JpaAuditingConfig; 5 | import com.pet.common.config.QuerydslConfig; 6 | import com.pet.domains.tag.domain.Tag; 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.autoconfigure.jdbc.AutoConfigureTestDatabase; 11 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 12 | import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; 13 | import org.springframework.context.annotation.ComponentScan.Filter; 14 | import org.springframework.context.annotation.FilterType; 15 | 16 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 17 | @DataJpaTest(includeFilters = @Filter( 18 | type = FilterType.ASSIGNABLE_TYPE, 19 | classes = {JpaAuditingConfig.class, QuerydslConfig.class}) 20 | ) 21 | @DisplayName("태그 리포지토리 테스트") 22 | class TagRepositoryTest { 23 | 24 | @Autowired 25 | private TestEntityManager entityManager; 26 | 27 | @Autowired 28 | private TagRepository tagRepository; 29 | 30 | @Test 31 | @DisplayName("이름으로 tag 조회 테스트") 32 | void findTagByNameTest() { 33 | // given 34 | Tag tag = Tag.builder() 35 | .name("웰시코기") 36 | .build(); 37 | entityManager.persist(tag); 38 | 39 | // when 40 | Tag foundTag = tagRepository.findTagByName("웰시코기").get(); 41 | 42 | // then 43 | assertThat(foundTag.getId()).isEqualTo(tag.getId()); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/filter/CookieAttributeFilter.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.filter; 2 | 3 | import java.io.IOException; 4 | import java.util.Collection; 5 | import javax.servlet.Filter; 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.FilterConfig; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.ServletRequest; 10 | import javax.servlet.ServletResponse; 11 | import javax.servlet.http.HttpServletResponse; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.stereotype.Component; 15 | 16 | @Slf4j 17 | @Component 18 | public class CookieAttributeFilter implements Filter { 19 | @Override 20 | public void init(FilterConfig filterConfig) throws ServletException { 21 | 22 | } 23 | 24 | @Override 25 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 26 | throws IOException, ServletException { 27 | HttpServletResponse httpServletResponse = (HttpServletResponse)response; 28 | chain.doFilter(request, response); 29 | log.info("CookieAttributeFilter"); 30 | 31 | Collection headers = httpServletResponse.getHeaders(HttpHeaders.SET_COOKIE); 32 | boolean firstHeader = true; 33 | 34 | for (String header : headers) { 35 | if (firstHeader) { 36 | httpServletResponse 37 | .setHeader(HttpHeaders.SET_COOKIE, String.format("%s;Secure;%s", header, "SameSite=" + "None")); 38 | firstHeader = false; 39 | continue; 40 | } 41 | httpServletResponse 42 | .addHeader(HttpHeaders.SET_COOKIE, String.format("%s;Secure;%s", header, "SameSite=" + "None")); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/animal/service/AnimalServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.service; 2 | 3 | import static org.mockito.ArgumentMatchers.anyList; 4 | import static org.mockito.BDDMockito.given; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | import com.pet.domains.animal.domain.Animal; 9 | import com.pet.domains.animal.dto.response.AnimalReadResults; 10 | import com.pet.domains.animal.mapper.AnimalMapper; 11 | import com.pet.domains.animal.repository.AnimalRepository; 12 | import java.util.List; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.mockito.InjectMocks; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | @DisplayName("동물 서비스 테스트") 22 | class AnimalServiceTest { 23 | 24 | @Mock 25 | private AnimalRepository animalRepository; 26 | 27 | @Mock 28 | private AnimalMapper animalMapper; 29 | 30 | @InjectMocks 31 | private AnimalService animalService; 32 | 33 | @Test 34 | @DisplayName("동물/품종 목록 조회 성공") 35 | void getAnimalsSuccessTest() { 36 | // given 37 | given(animalRepository.findAllWithAnimalKinds()) 38 | .willReturn(List.of(mock(Animal.class))); 39 | given(animalMapper.toAnimalReadResult(anyList())).willReturn(List.of(mock(AnimalReadResults.Animal.class))); 40 | given(animalMapper.toAnimalReadResults(anyList())).willReturn(mock(AnimalReadResults.class)); 41 | 42 | // when 43 | animalService.getAnimals(); 44 | 45 | // then 46 | verify(animalRepository, times(1)).findAllWithAnimalKinds(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/animal/domain/AnimalKind.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.FetchType; 7 | import javax.persistence.ForeignKey; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.JoinColumn; 12 | import javax.persistence.ManyToOne; 13 | import javax.persistence.Table; 14 | import lombok.AccessLevel; 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import org.apache.commons.lang3.ObjectUtils; 19 | 20 | @Getter 21 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 22 | @Entity 23 | @Table(name = "animal_kind") 24 | public class AnimalKind extends BaseEntity { 25 | 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | @Column(name = "id", updatable = false) 29 | private Long id; 30 | 31 | @Column(name = "name", length = 50, nullable = false) 32 | private String name; 33 | 34 | @Column(name = "code", length = 6) 35 | private String code; 36 | 37 | @ManyToOne(fetch = FetchType.LAZY) 38 | @JoinColumn( 39 | name = "animal_id", 40 | referencedColumnName = "id", 41 | foreignKey = @ForeignKey(name = "fk_animal_to_animal_kind"), 42 | nullable = false 43 | ) 44 | private Animal animal; 45 | 46 | @Builder 47 | public AnimalKind(String name, String code, Animal animal) { 48 | ObjectUtils.requireNonEmpty(name, "name must not be null"); 49 | ObjectUtils.requireNonEmpty(animal, "animal must not be null"); 50 | 51 | this.name = name; 52 | this.code = code; 53 | this.animal = animal; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/comment/dto/response/CommentWriteResult.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.comment.dto.response; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class CommentWriteResult { 9 | 10 | private long id; 11 | 12 | private String content; 13 | 14 | private LocalDateTime createdAt; 15 | 16 | private Account account; 17 | 18 | private List childComments; 19 | 20 | private boolean deleted; 21 | 22 | public CommentWriteResult(long id, String content, LocalDateTime createdAt, 23 | Account account, List childComments, boolean deleted) { 24 | this.id = id; 25 | this.content = content; 26 | this.createdAt = createdAt; 27 | this.account = account; 28 | this.childComments = childComments; 29 | this.deleted = deleted; 30 | } 31 | 32 | @Getter 33 | public static class Account { 34 | 35 | private long id; 36 | 37 | private String nickname; 38 | 39 | private String image; 40 | 41 | public Account(long id, String nickname, String image) { 42 | this.id = id; 43 | this.nickname = nickname; 44 | this.image = image; 45 | } 46 | } 47 | 48 | @Getter 49 | public static class ChildComment { 50 | 51 | private long id; 52 | 53 | private String content; 54 | 55 | private LocalDateTime createdAt; 56 | 57 | private Account account; 58 | 59 | public ChildComment(long id, String content, LocalDateTime createdAt, 60 | Account account) { 61 | this.id = id; 62 | this.content = content; 63 | this.createdAt = createdAt; 64 | this.account = account; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/dto/response/CityReadResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.dto.response; 2 | 3 | import java.util.List; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 10 | public class CityReadResults { 11 | 12 | private List cities; 13 | 14 | public CityReadResults(List cities) { 15 | this.cities = cities; 16 | } 17 | 18 | public static CityReadResults of(List cities) { 19 | return new CityReadResults(cities); 20 | } 21 | 22 | @Getter 23 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 24 | public static class City { 25 | 26 | private Long id; 27 | 28 | private String name; 29 | 30 | private List towns; 31 | 32 | public City(Long id, String name, List towns) { 33 | this.id = id; 34 | this.name = name; 35 | this.towns = towns; 36 | } 37 | 38 | public static City of(Long id, String name, List towns) { 39 | return new City(id, name, towns); 40 | } 41 | 42 | @Getter 43 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 44 | public static class Town { 45 | 46 | private Long id; 47 | 48 | private String name; 49 | 50 | public Town(Long id, String name) { 51 | this.id = id; 52 | this.name = name; 53 | } 54 | 55 | public static Town of(Long id, String name) { 56 | return new Town(id, name); 57 | } 58 | 59 | } 60 | 61 | } 62 | @Override 63 | public String toString() { 64 | return "cities"; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/service/CityService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.service; 2 | 3 | import com.pet.domains.area.domain.City; 4 | import com.pet.domains.area.dto.request.CityCreateParams; 5 | import com.pet.domains.area.dto.response.CityReadResults; 6 | import com.pet.domains.area.mapper.CityMapper; 7 | import com.pet.domains.area.repository.CityRepository; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.cache.annotation.CacheEvict; 12 | import org.springframework.cache.annotation.Cacheable; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | @Transactional(readOnly = true) 17 | @RequiredArgsConstructor 18 | @Service 19 | public class CityService { 20 | 21 | private final CityRepository cityRepository; 22 | 23 | private final CityMapper cityMapper; 24 | 25 | @CacheEvict(cacheNames = "cities", allEntries = true) 26 | @Transactional 27 | public void createCites(CityCreateParams cityCreateParams) { 28 | List cities = cityCreateParams.getCities().stream() 29 | .map(cityMapper::toEntity) 30 | .collect(Collectors.toList()); 31 | cityRepository.saveAll(cities); 32 | } 33 | 34 | @Cacheable( 35 | cacheNames = "cities", 36 | unless = "#result == null || #result.getCities().isEmpty()" 37 | ) 38 | public CityReadResults getAllTownAndCity() { 39 | List result = cityRepository.findAll(); 40 | return CityReadResults.of(result.stream() 41 | .map(city -> cityMapper.toCityDto(city, city.getTowns().stream() 42 | .map(cityMapper::toTownDto) 43 | .collect(Collectors.toList()))) 44 | .collect(Collectors.toList())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/service/NotificationAsyncService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.service; 2 | 3 | import com.pet.domains.account.domain.Account; 4 | import com.pet.domains.account.domain.Notification; 5 | import com.pet.domains.account.repository.AccountRepository; 6 | import com.pet.domains.account.repository.NotificationRepository; 7 | import com.pet.domains.post.domain.MissingPost; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.scheduling.annotation.Async; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Propagation; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | @Service 20 | public class NotificationAsyncService { 21 | 22 | private final NotificationRepository notificationRepository; 23 | 24 | private final AccountRepository accountRepository; 25 | 26 | @Async 27 | @Transactional(propagation = Propagation.REQUIRES_NEW) 28 | public void createNotifications(MissingPost triggerMissingPost, Long publisherAccountId) { 29 | log.info("start async createNotifications task"); 30 | List subscribers = accountRepository.findAllByNotificationSubscribers( 31 | triggerMissingPost.getTown().getId(), 32 | publisherAccountId 33 | ); 34 | List notifications = subscribers.stream() 35 | .map(account -> Notification.builder() 36 | .account(account) 37 | .missingPost(triggerMissingPost) 38 | .build()) 39 | .collect(Collectors.toList()); 40 | 41 | notificationRepository.saveAll(notifications); 42 | log.info("complete async createNotifications task"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/service/PostStatisticsService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.service; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import com.pet.domains.statistics.domain.PostStatistics; 5 | import com.pet.domains.statistics.dto.response.PostStatisticsReadResult; 6 | import com.pet.domains.statistics.mapper.PostStatisticsMapper; 7 | import com.pet.domains.statistics.repository.PostCountByStatus; 8 | import com.pet.domains.statistics.repository.PostStatisticsRepository; 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.scheduling.annotation.Scheduled; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | @Slf4j 18 | @Transactional(readOnly = true) 19 | @RequiredArgsConstructor 20 | @Service 21 | public class PostStatisticsService { 22 | 23 | private final PostStatisticsRepository postStatisticsRepository; 24 | 25 | private final PostStatisticsMapper postStatisticsMapper; 26 | 27 | @Scheduled(cron = "0 0 4 * * *") 28 | @Transactional 29 | public void createPostStatistics() { 30 | LocalDateTime now = LocalDateTime.now(); 31 | List groupByStatus = postStatisticsRepository.findGroupByStatus(); 32 | PostStatistics postStatistics = postStatisticsRepository.save( 33 | postStatisticsMapper.toEntity(groupByStatus, now)); 34 | log.info("{} created at {}", postStatistics, postStatistics.getDate()); 35 | } 36 | 37 | public PostStatisticsReadResult getPostStatistics() { 38 | PostStatistics postStatistics = postStatisticsRepository.findFirstByOrderByIdDesc() 39 | .orElseThrow(ExceptionMessage.NOT_FOUND_POST_STATISTICS::getException); 40 | return postStatisticsMapper.toReadResult(postStatistics); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/s3/service/FileUploadService.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.s3.service; 2 | 3 | import com.amazonaws.services.s3.model.ObjectMetadata; 4 | import com.pet.common.exception.ExceptionMessage; 5 | import com.pet.common.s3.validator.ValidImage; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.UUID; 9 | import javax.validation.Valid; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.validation.annotation.Validated; 14 | import org.springframework.web.multipart.MultipartFile; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | @Component 19 | @Validated 20 | public class FileUploadService { 21 | 22 | private final UploadService uploadService; 23 | 24 | public String uploadImage(@ValidImage MultipartFile file) { 25 | String fileName = createFileName(file.getOriginalFilename()); 26 | ObjectMetadata objectMetadata = new ObjectMetadata(); 27 | objectMetadata.setContentLength(file.getSize()); 28 | objectMetadata.setContentType(file.getContentType()); 29 | try (InputStream inputStream = file.getInputStream()) { 30 | uploadService.uploadFile(inputStream, objectMetadata, fileName); 31 | } catch (IOException e) { 32 | throw ExceptionMessage.FAIL_CHANGE_IMAGE.getException(); 33 | } 34 | return uploadService.getFileUrl(fileName); 35 | } 36 | 37 | private String createFileName(String originalFileName) { 38 | return UUID.randomUUID().toString().concat(getFileExtension(originalFileName)); 39 | } 40 | 41 | private String getFileExtension(String fileName) { 42 | try { 43 | return fileName.substring(fileName.lastIndexOf(".")); 44 | } catch (StringIndexOutOfBoundsException e) { 45 | throw ExceptionMessage.INVALID_IMAGE_TYPE.getException(); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/domain/SignEmail.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import com.pet.domains.BaseEntity; 5 | import java.time.LocalDateTime; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | import lombok.AccessLevel; 13 | import lombok.Builder; 14 | import lombok.Getter; 15 | import lombok.NoArgsConstructor; 16 | import org.apache.commons.lang3.StringUtils; 17 | 18 | @Getter 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @Entity 21 | @Table(name = "sign_email") 22 | public class SignEmail extends BaseEntity { 23 | 24 | private static final Long EXPIRATION = 3L; 25 | 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | @Column(name = "id", updatable = false) 29 | private Long id; 30 | 31 | @Column(name = "email", nullable = false, length = 50) 32 | private String email; 33 | 34 | @Column(name = "verify_key", nullable = false, unique = true) 35 | private String verifyKey; 36 | 37 | @Column(name = "is_checked", columnDefinition = "boolean default false") 38 | private boolean isChecked; 39 | 40 | @Builder 41 | public SignEmail(String email, String verifyKey) { 42 | this.email = email; 43 | this.verifyKey = verifyKey; 44 | } 45 | 46 | public boolean isVerifyTime(LocalDateTime requestTime) { 47 | return getCreatedAt().plusMinutes(EXPIRATION).isAfter(requestTime); 48 | } 49 | 50 | public void successVerified() { 51 | this.isChecked = true; 52 | } 53 | 54 | public boolean isVerifyEmail(String email) { 55 | if (StringUtils.equals(this.email, email) && isChecked) { 56 | return true; 57 | } 58 | throw ExceptionMessage.INVALID_SIGN_UP.getException(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/filter/CorsFilter.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.filter; 2 | 3 | import java.io.IOException; 4 | import javax.servlet.Filter; 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.FilterConfig; 7 | import javax.servlet.ServletException; 8 | import javax.servlet.ServletRequest; 9 | import javax.servlet.ServletResponse; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import org.springframework.core.Ordered; 13 | import org.springframework.core.annotation.Order; 14 | import org.springframework.stereotype.Component; 15 | 16 | @Component 17 | @Order(Ordered.HIGHEST_PRECEDENCE) 18 | public class CorsFilter implements Filter { 19 | 20 | @Override 21 | public void init(FilterConfig filterConfig) throws ServletException { 22 | 23 | } 24 | 25 | @Override 26 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 27 | throws IOException, ServletException { 28 | HttpServletRequest request = (HttpServletRequest) req; 29 | HttpServletResponse response = (HttpServletResponse) res; 30 | 31 | response.setHeader("Access-Control-Allow-Origin", "https://comepet.netlify.app"); 32 | response.setHeader("Access-Control-Allow-Credentials", "true"); 33 | response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH"); 34 | response.setHeader("Access-Control-Max-Age", "3600"); 35 | response.setHeader("Access-Control-Allow-Headers", "authorization, content-type, xsrf-token"); 36 | response.addHeader("Access-Control-Expose-Headers", "xsrf-token"); 37 | response.setHeader("Access-Control-Allow-Credentials", "true"); 38 | 39 | if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { 40 | response.setStatus(HttpServletResponse.SC_OK); 41 | } else { 42 | chain.doFilter(req, res); 43 | } 44 | } 45 | 46 | @Override 47 | public void destroy() { 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Prod CD with Gradle 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | name: production deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Gradle build 13 | - uses: actions/checkout@v2 14 | - name: Set up JDK 14 15 | uses: actions/setup-java@v2 16 | with: 17 | java-version: '14' 18 | distribution: 'adopt' 19 | cache: gradle 20 | - name: Grant execute permission for gradlew 21 | run: chmod +x gradlew 22 | - name: Build with Gradle 23 | run: ./gradlew clean build 24 | 25 | # 디렉토리 생성 26 | - name: Make Directory 27 | run: mkdir -p deploy 28 | 29 | # Jar 파일 복사 30 | - name: Copy Jar 31 | run: cp ./build/libs/*.jar ./deploy 32 | 33 | # Appspec 파일 복사 34 | - name: Copy appspec 35 | run: cp ./appspec.yml ./deploy 36 | 37 | # Deploy Shell 파일 복사 38 | - name: Dev Copy deploy shell 39 | run: cp ./deploy-prod.sh ./deploy/deploy.sh 40 | 41 | # 파일 압축 42 | - name: Make zip file 43 | run: zip -r ./compet.zip ./deploy 44 | 45 | # AWS Credential 46 | - name: Dev AWS Credential 47 | uses: aws-actions/configure-aws-credentials@v1 48 | with: 49 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} 50 | aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} 51 | aws-region: ap-northeast-2 52 | 53 | - name: Upload to S3 54 | run: aws s3 cp --region ap-northeast-2 --acl private ./compet.zip s3://compet-dev-deploy/compet/ 55 | 56 | - name: code deploy 57 | run: | 58 | aws deploy create-deployment --application-name compet-dev-deploy \ 59 | --deployment-config-name CodeDeployDefault.AllAtOnce \ 60 | --deployment-group-name compet-dev-deploy-group \ 61 | --file-exists-behavior OVERWRITE \ 62 | --s3-location bucket=compet-dev-deploy,bundleType=zip,key=compet/compet.zip 63 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev CD with Gradle 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | 7 | jobs: 8 | build: 9 | name: development deploy 10 | runs-on: ubuntu-latest 11 | environment: development 12 | steps: 13 | # Gradle build 14 | - uses: actions/checkout@v2 15 | - name: Set up JDK 14 16 | uses: actions/setup-java@v2 17 | with: 18 | java-version: '14' 19 | distribution: 'adopt' 20 | cache: gradle 21 | - name: Grant execute permission for gradlew 22 | run: chmod +x gradlew 23 | - name: Build with Gradle 24 | run: ./gradlew clean build 25 | 26 | # 디렉토리 생성 27 | - name: Make Directory 28 | run: mkdir -p deploy 29 | 30 | # Jar 파일 복사 31 | - name: Copy Jar 32 | run: cp ./build/libs/*.jar ./deploy 33 | 34 | # Appspec 파일 복사 35 | - name: Copy appspec 36 | run: cp ./appspec.yml ./deploy 37 | 38 | # Deploy Shell 파일 복사 39 | - name: Dev Copy deploy shell 40 | run: cp ./deploy-dev.sh ./deploy/deploy.sh 41 | 42 | # 파일 압축 43 | - name: Make zip file 44 | run: zip -r ./compet.zip ./deploy 45 | 46 | # AWS Credential 47 | - name: Dev AWS Credential 48 | uses: aws-actions/configure-aws-credentials@v1 49 | with: 50 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} 51 | aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} 52 | aws-region: ap-northeast-2 53 | 54 | - name: Upload to S3 55 | run: aws s3 cp --region ap-northeast-2 --acl private ./compet.zip s3://compet-dev-deploy/compet/ 56 | 57 | - name: code deploy 58 | run: | 59 | aws deploy create-deployment --application-name compet-dev-deploy \ 60 | --deployment-config-name CodeDeployDefault.AllAtOnce \ 61 | --deployment-group-name compet-dev-deploy-group \ 62 | --file-exists-behavior OVERWRITE \ 63 | --s3-location bucket=compet-dev-deploy,bundleType=zip,key=compet/compet.zip 64 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/RedisCachingConfig.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import java.time.Duration; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.cache.CacheManager; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.data.redis.cache.RedisCacheConfiguration; 12 | import org.springframework.data.redis.cache.RedisCacheManager; 13 | import org.springframework.data.redis.connection.RedisConnectionFactory; 14 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 15 | import org.springframework.data.redis.serializer.RedisSerializationContext; 16 | import org.springframework.data.redis.serializer.StringRedisSerializer; 17 | 18 | @EnableCaching 19 | @RequiredArgsConstructor 20 | @Configuration 21 | @Profile({"dev", "prod"}) 22 | public class RedisCachingConfig { 23 | 24 | private final RedisConnectionFactory redisConnectionFactory; 25 | 26 | private final ObjectMapper objectMapper; 27 | 28 | @Bean 29 | public CacheManager redisCacheManager() { 30 | RedisCacheConfiguration redisCachingConfiguration = RedisCacheConfiguration 31 | .defaultCacheConfig() 32 | .serializeKeysWith( 33 | RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) 34 | ) 35 | .serializeValuesWith( 36 | RedisSerializationContext.SerializationPair.fromSerializer( 37 | new GenericJackson2JsonRedisSerializer() 38 | ) 39 | ) 40 | .entryTtl(Duration.ofHours(1)); 41 | 42 | return RedisCacheManager 43 | .RedisCacheManagerBuilder 44 | .fromConnectionFactory(redisConnectionFactory) 45 | .cacheDefaults(redisCachingConfiguration) 46 | .build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/animal/repository/AnimalKindRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.animal.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import com.pet.common.config.JpaAuditingConfig; 5 | import com.pet.common.config.QuerydslConfig; 6 | import com.pet.domains.animal.domain.Animal; 7 | import com.pet.domains.animal.domain.AnimalKind; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 12 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; 14 | import org.springframework.context.annotation.ComponentScan.Filter; 15 | import org.springframework.context.annotation.FilterType; 16 | 17 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 18 | @DataJpaTest(includeFilters = @Filter( 19 | type = FilterType.ASSIGNABLE_TYPE, 20 | classes = {JpaAuditingConfig.class, QuerydslConfig.class}) 21 | ) 22 | @DisplayName("품종 리포지토리 테스트") 23 | class AnimalKindRepositoryTest { 24 | 25 | @Autowired 26 | private TestEntityManager entityManager; 27 | 28 | @Autowired 29 | private AnimalKindRepository animalKindRepository; 30 | 31 | @Test 32 | @DisplayName("이름으로 AnimalKind 조회 테스트") 33 | void findAnimalKindByNameTest() { 34 | // given 35 | Animal animal = Animal.builder() 36 | .code("1234") 37 | .name("testAnimal") 38 | .build(); 39 | entityManager.persist(animal); 40 | 41 | AnimalKind animalKind = AnimalKind.builder() 42 | .name("animalKindName") 43 | .animal(animal) 44 | .build(); 45 | entityManager.persist(animalKind); 46 | 47 | // when 48 | AnimalKind foundAnimalKind = animalKindRepository.findByNameAndAnimalId("animalKindName", animal.getId()).get(); 49 | 50 | // then 51 | assertThat(foundAnimalKind.getId()).isEqualTo(animalKind.getId()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/config/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.config; 2 | 3 | import java.util.Base64; 4 | import java.util.Optional; 5 | import javax.servlet.http.Cookie; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import org.springframework.util.SerializationUtils; 9 | 10 | public class CookieUtil { 11 | 12 | public static Optional getCookie(HttpServletRequest request, String name) { 13 | Cookie[] cookies = request.getCookies(); 14 | 15 | if (cookies != null && cookies.length > 0) { 16 | for (Cookie cookie : cookies) { 17 | if (name.equals(cookie.getName())) { 18 | return Optional.of(cookie); 19 | } 20 | } 21 | } 22 | return Optional.empty(); 23 | } 24 | 25 | public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { 26 | Cookie cookie = new Cookie(name, value); 27 | cookie.setPath("/"); 28 | cookie.setHttpOnly(true); 29 | cookie.setMaxAge(maxAge); 30 | 31 | response.addCookie(cookie); 32 | } 33 | 34 | public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { 35 | Cookie[] cookies = request.getCookies(); 36 | 37 | if (cookies != null && cookies.length > 0) { 38 | for (Cookie cookie : cookies) { 39 | if (name.equals(cookie.getName())) { 40 | cookie.setValue(""); 41 | cookie.setPath("/"); 42 | cookie.setMaxAge(0); 43 | response.addCookie(cookie); 44 | } 45 | } 46 | } 47 | } 48 | 49 | public static String serialize(Object obj) { 50 | return Base64.getUrlEncoder() 51 | .encodeToString(SerializationUtils.serialize(obj)); 52 | } 53 | 54 | public static T deserialize(Cookie cookie, Class cls) { 55 | return cls.cast( 56 | SerializationUtils.deserialize( 57 | Base64.getUrlDecoder().decode(cookie.getValue()) 58 | ) 59 | ); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/domain/MissingPostBookmark.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import com.pet.domains.account.domain.Account; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.ForeignKey; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.JoinColumn; 13 | import javax.persistence.ManyToOne; 14 | import javax.persistence.Table; 15 | import javax.persistence.UniqueConstraint; 16 | import lombok.AccessLevel; 17 | import lombok.Builder; 18 | import lombok.Getter; 19 | import lombok.NoArgsConstructor; 20 | import org.apache.commons.lang3.Validate; 21 | 22 | @Getter 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | @Entity 25 | @Table( 26 | name = "missing_post_bookmark", 27 | uniqueConstraints = @UniqueConstraint( 28 | name = "uni_missing_post_and_account", 29 | columnNames = {"missing_post_id", "account_id"} 30 | ) 31 | ) 32 | public class MissingPostBookmark extends BaseEntity { 33 | 34 | @Id 35 | @GeneratedValue(strategy = GenerationType.IDENTITY) 36 | @Column(name = "id", updatable = false) 37 | private Long id; 38 | 39 | @ManyToOne(fetch = FetchType.LAZY) 40 | @JoinColumn(name = "account_id", 41 | referencedColumnName = "id", 42 | nullable = false, 43 | foreignKey = @ForeignKey(name = "fk_account_to_missing_book_mark")) 44 | private Account account; 45 | 46 | @ManyToOne(fetch = FetchType.LAZY) 47 | @JoinColumn(name = "missing_post_id", 48 | referencedColumnName = "id", 49 | nullable = false, 50 | foreignKey = @ForeignKey(name = "fk_missing_post_to_missing_book_mark")) 51 | private MissingPost missingPost; 52 | 53 | @Builder 54 | public MissingPostBookmark(Account account, MissingPost missingPost) { 55 | Validate.notNull(account, "account must not be null"); 56 | Validate.notNull(missingPost, "missingPost must not be null"); 57 | 58 | this.account = account; 59 | this.missingPost = missingPost; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/post/domain/ShelterPostBookmark.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.post.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import com.pet.domains.account.domain.Account; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.ForeignKey; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.JoinColumn; 13 | import javax.persistence.ManyToOne; 14 | import javax.persistence.Table; 15 | import javax.persistence.UniqueConstraint; 16 | import lombok.AccessLevel; 17 | import lombok.Builder; 18 | import lombok.Getter; 19 | import lombok.NoArgsConstructor; 20 | import org.apache.commons.lang3.ObjectUtils; 21 | 22 | @Getter 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | @Entity 25 | @Table( 26 | name = "shelter_post_bookmark", 27 | uniqueConstraints = @UniqueConstraint( 28 | name = "uni_shelter_post_and_account", 29 | columnNames = {"shelter_post_id", "account_id"} 30 | ) 31 | ) 32 | public class ShelterPostBookmark extends BaseEntity { 33 | 34 | @Id 35 | @GeneratedValue(strategy = GenerationType.IDENTITY) 36 | @Column(name = "id", updatable = false) 37 | private Long id; 38 | 39 | @ManyToOne(fetch = FetchType.LAZY) 40 | @JoinColumn(name = "account_id", 41 | referencedColumnName = "id", 42 | nullable = false, 43 | foreignKey = @ForeignKey(name = "fk_account_to_shelter_book_mark")) 44 | private Account account; 45 | 46 | @ManyToOne(fetch = FetchType.LAZY) 47 | @JoinColumn(name = "shelter_post_id", 48 | referencedColumnName = "id", 49 | nullable = false, 50 | foreignKey = @ForeignKey(name = "fk_shelter_post_to_shelter_book_mark")) 51 | private ShelterPost shelterPost; 52 | 53 | @Builder 54 | public ShelterPostBookmark(Account account, ShelterPost shelterPost) { 55 | ObjectUtils.requireNonEmpty(account, "account must not be null"); 56 | ObjectUtils.requireNonEmpty(shelterPost, "shelterPost must not be null"); 57 | 58 | this.account = account; 59 | this.shelterPost = shelterPost; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/area/domain/InterestArea.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.area.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import com.pet.domains.account.domain.Account; 5 | import java.util.Objects; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.FetchType; 9 | import javax.persistence.ForeignKey; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import javax.persistence.JoinColumn; 14 | import javax.persistence.ManyToOne; 15 | import javax.persistence.Table; 16 | import lombok.AccessLevel; 17 | import lombok.Builder; 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Getter; 20 | import lombok.NoArgsConstructor; 21 | import org.apache.commons.lang3.Validate; 22 | 23 | @Getter 24 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 25 | @Entity 26 | @Table(name = "interest_area") 27 | @EqualsAndHashCode(of = "id", callSuper = false) 28 | public class InterestArea extends BaseEntity { 29 | 30 | @Id 31 | @GeneratedValue(strategy = GenerationType.IDENTITY) 32 | @Column(name = "id", updatable = false) 33 | private Long id; 34 | 35 | @Column(name = "selected", nullable = false, columnDefinition = "boolean default false") 36 | private boolean selected; 37 | 38 | @ManyToOne(fetch = FetchType.LAZY) 39 | @JoinColumn( 40 | name = "account_id", 41 | nullable = false, 42 | foreignKey = @ForeignKey(name = "fk_interest_area_to_account") 43 | ) 44 | private Account account; 45 | 46 | @ManyToOne(fetch = FetchType.LAZY) 47 | @JoinColumn( 48 | name = "town_id", 49 | nullable = false, 50 | foreignKey = @ForeignKey(name = "fk_interest_area_to_town") 51 | ) 52 | private Town town; 53 | 54 | @Builder 55 | public InterestArea(boolean selected, Account account, Town town) { 56 | Validate.notNull(account, "account must not be null"); 57 | Validate.notNull(town, "town must not be null"); 58 | 59 | this.selected = selected; 60 | this.account = account; 61 | this.town = town; 62 | } 63 | 64 | public void checkSelect() { 65 | this.selected = true; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/jwt/JwtAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.jwt; 2 | 3 | import java.util.Collection; 4 | import java.util.Objects; 5 | import org.apache.commons.lang3.BooleanUtils; 6 | import org.apache.commons.lang3.ObjectUtils; 7 | import org.apache.commons.lang3.Validate; 8 | import org.apache.commons.lang3.builder.ToStringBuilder; 9 | import org.apache.commons.lang3.builder.ToStringStyle; 10 | import org.springframework.security.authentication.AbstractAuthenticationToken; 11 | import org.springframework.security.core.GrantedAuthority; 12 | 13 | public class JwtAuthenticationToken extends AbstractAuthenticationToken { 14 | 15 | private final Object principal; 16 | 17 | private String credentials; 18 | 19 | public JwtAuthenticationToken(String principal, String credentials) { 20 | super(null); 21 | super.setAuthenticated(false); 22 | 23 | this.principal = principal; 24 | this.credentials = credentials; 25 | } 26 | 27 | public JwtAuthenticationToken( 28 | Object principal, String credentials, Collection authorities 29 | ) { 30 | super(authorities); 31 | super.setAuthenticated(true); 32 | 33 | this.principal = principal; 34 | this.credentials = credentials; 35 | } 36 | 37 | @Override 38 | public Object getPrincipal() { 39 | return principal; 40 | } 41 | 42 | @Override 43 | public String getCredentials() { 44 | return credentials; 45 | } 46 | 47 | public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { 48 | Validate.isTrue(!isAuthenticated, 49 | "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); 50 | super.setAuthenticated(false); 51 | } 52 | 53 | @Override 54 | public void eraseCredentials() { 55 | super.eraseCredentials(); 56 | credentials = null; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 62 | .append("principal", principal) 63 | .append("credentials", "[PROTECTED]") 64 | .toString(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pet/common/aop/RequestLoggingAspect.java: -------------------------------------------------------------------------------- 1 | package com.pet.common.aop; 2 | 3 | import com.google.common.base.Joiner; 4 | import java.util.Map; 5 | import java.util.stream.Collectors; 6 | import javax.servlet.http.HttpServletRequest; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.aspectj.lang.annotation.Around; 10 | import org.aspectj.lang.annotation.Aspect; 11 | import org.aspectj.lang.annotation.Pointcut; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.web.context.request.RequestContextHolder; 16 | import org.springframework.web.context.request.ServletRequestAttributes; 17 | import static org.springframework.web.context.request.RequestContextHolder.*; 18 | 19 | @Component 20 | @Aspect 21 | @Slf4j 22 | public class RequestLoggingAspect { 23 | 24 | private String paramMapToString(Map paramMap) { 25 | return paramMap.entrySet().stream() 26 | .map(entry -> String.format("%s -> (%s)", 27 | entry.getKey(), Joiner.on(",").join(entry.getValue()))) 28 | .collect(Collectors.joining(", ")); 29 | } 30 | 31 | // 모든 컨트롤러 요청에 대한 처리 32 | @Pointcut("within(com.pet.*.*.controller..*)") 33 | public void onRequest() { 34 | 35 | } 36 | 37 | @Around("com.pet.common.aop.RequestLoggingAspect.onRequest()") 38 | public Object doLogging(ProceedingJoinPoint joinPoint) throws Throwable { 39 | HttpServletRequest request = ((ServletRequestAttributes) currentRequestAttributes()).getRequest(); 40 | Map paramMap = request.getParameterMap(); 41 | 42 | String params = ""; 43 | if (!paramMap.isEmpty()) { 44 | params = " [" + paramMapToString(paramMap) + "]"; 45 | } 46 | 47 | long start = System.currentTimeMillis(); 48 | try { 49 | return joinPoint.proceed(joinPoint.getArgs()); 50 | } finally { 51 | long end = System.currentTimeMillis(); 52 | log.info("Request: {} {}{} < {} ({}ms)", request.getMethod(), request.getRequestURI(), 53 | params, request.getRemoteHost(), end - start); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/response/NotificationReadResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.response; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class NotificationReadResults { 8 | 9 | private final List notifications; 10 | 11 | private final long totalElements; 12 | 13 | private final boolean last; 14 | 15 | private final long size; 16 | 17 | public NotificationReadResults(List notifications, long totalElements, boolean last, long size) { 18 | this.notifications = notifications; 19 | this.totalElements = totalElements; 20 | this.last = last; 21 | this.size = size; 22 | } 23 | 24 | public static NotificationReadResults of(List notifications, 25 | long totalElements, boolean last, long size 26 | ) { 27 | return new NotificationReadResults(notifications, totalElements, last, size); 28 | } 29 | 30 | @Getter 31 | public static class Notification { 32 | 33 | private final Long id; 34 | 35 | private final String nickname; 36 | 37 | private final String image; 38 | 39 | private final Long postId; 40 | 41 | private final String status; 42 | 43 | private final String animalKindName; 44 | 45 | private final String town; 46 | 47 | private final boolean checked; 48 | 49 | public Notification(Long id, String nickname, String image, Long postId, String status, String animalKindName, 50 | String town, boolean checked) { 51 | this.id = id; 52 | this.nickname = nickname; 53 | this.image = image; 54 | this.postId = postId; 55 | this.status = status; 56 | this.animalKindName = animalKindName; 57 | this.town = town; 58 | this.checked = checked; 59 | } 60 | 61 | public static Notification of(Long id, String nickname, String image, Long postId, 62 | String status, String animalKindName, String town, boolean checked) { 63 | return new Notification(id, nickname, image, postId, status, animalKindName, town, checked); 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/image/domain/PostImage.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.image.domain; 2 | 3 | import com.pet.domains.post.domain.MissingPost; 4 | import java.util.Objects; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.ForeignKey; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.JoinColumn; 13 | import javax.persistence.ManyToOne; 14 | import javax.persistence.OneToOne; 15 | import javax.persistence.Table; 16 | import lombok.AccessLevel; 17 | import lombok.Builder; 18 | import lombok.Getter; 19 | import lombok.NoArgsConstructor; 20 | import org.apache.commons.lang3.ObjectUtils; 21 | 22 | @Getter 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | @Entity 25 | @Table(name = "post_image") 26 | public class PostImage { 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | @Column(name = "id", updatable = false) 31 | private Long id; 32 | 33 | @ManyToOne(fetch = FetchType.LAZY) 34 | @JoinColumn( 35 | name = "missing_post_id", 36 | referencedColumnName = "id", 37 | foreignKey = @ForeignKey(name = "fk_missing_post_to_post_image"), 38 | nullable = false 39 | ) 40 | private MissingPost missingPost; 41 | 42 | @OneToOne(fetch = FetchType.EAGER) 43 | @JoinColumn( 44 | name = "image_id", 45 | referencedColumnName = "id", 46 | foreignKey = @ForeignKey(name = "fk_image_to_post_image"), 47 | nullable = false 48 | ) 49 | private Image image; 50 | 51 | @Builder 52 | public PostImage(MissingPost missingPost, Image image) { 53 | ObjectUtils.requireNonEmpty(missingPost, "missingPost must not be null"); 54 | ObjectUtils.requireNonEmpty(image, "image must not be null"); 55 | 56 | addMissingPost(missingPost); 57 | this.image = image; 58 | } 59 | 60 | private void addMissingPost(MissingPost missingPost) { 61 | if (Objects.nonNull(this.missingPost)) { 62 | missingPost.getPostImages().remove(this); 63 | } 64 | this.missingPost = missingPost; 65 | missingPost.getPostImages().add(this); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/account/dto/AccountValidationTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import com.pet.domains.account.dto.request.AccountEmailParam; 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | import javax.validation.ConstraintViolation; 8 | import javax.validation.Validation; 9 | import javax.validation.Validator; 10 | import javax.validation.ValidatorFactory; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.DisplayName; 13 | import org.junit.jupiter.api.Test; 14 | 15 | @DisplayName("회원 dto 유효성 테스트") 16 | class AccountValidationTest { 17 | 18 | private static Validator validator; 19 | 20 | @BeforeAll 21 | static void setUp() { 22 | ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); 23 | validator = factory.getValidator(); 24 | } 25 | 26 | @Test 27 | @DisplayName("이메일 정상 입력 테스트") 28 | void checkEmailTest() { 29 | AccountEmailParam emailCheck = new AccountEmailParam("test@test.com"); 30 | Set> validations = validator.validate(emailCheck); 31 | assertThat(validations).hasSize(0); 32 | } 33 | 34 | @Test 35 | @DisplayName("이메일 비정상 입력 테스트 - test@test") 36 | void checkEmailFailNotContainsDotTest() { 37 | AccountEmailParam emailCheck = new AccountEmailParam("test@test"); 38 | Set> validations = validator.validate(emailCheck); 39 | assertThat(validations).hasSize(1); 40 | 41 | assertThat(validations.stream() 42 | .map(ConstraintViolation::getMessage) 43 | .collect(Collectors.toList())).containsAnyOf("올바른 이메일 형식이 아닙니다."); 44 | } 45 | 46 | @Test 47 | @DisplayName("이메일 비정상 입력 테스트 - test.com") 48 | void checkEmailFailNotContainsAtTest() { 49 | AccountEmailParam emailCheck = new AccountEmailParam("test@test"); 50 | Set> validations = validator.validate(emailCheck); 51 | assertThat(validations).hasSize(1); 52 | 53 | assertThat(validations.stream() 54 | .map(ConstraintViolation::getMessage) 55 | .collect(Collectors.toList())).containsAnyOf("올바른 이메일 형식이 아닙니다."); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/tag/domain/PostTag.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.tag.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import com.pet.domains.post.domain.MissingPost; 5 | import java.util.Objects; 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.FetchType; 9 | import javax.persistence.ForeignKey; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import javax.persistence.JoinColumn; 14 | import javax.persistence.ManyToOne; 15 | import javax.persistence.Table; 16 | import lombok.AccessLevel; 17 | import lombok.Builder; 18 | import lombok.Getter; 19 | import lombok.NoArgsConstructor; 20 | import lombok.Setter; 21 | import org.apache.commons.lang3.ObjectUtils; 22 | 23 | @Getter 24 | @Setter(AccessLevel.PRIVATE) 25 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 26 | @Entity 27 | @Table(name = "post_tag") 28 | public class PostTag extends BaseEntity { 29 | 30 | @Id 31 | @GeneratedValue(strategy = GenerationType.IDENTITY) 32 | @Column(name = "id", updatable = false) 33 | private Long id; 34 | 35 | @ManyToOne(fetch = FetchType.LAZY) 36 | @JoinColumn( 37 | name = "missing_post_id", 38 | referencedColumnName = "id", 39 | foreignKey = @ForeignKey(name = "fk_missing_post_to_post_tag"), 40 | nullable = false 41 | ) 42 | private MissingPost missingPost; 43 | 44 | @ManyToOne(fetch = FetchType.EAGER) 45 | @JoinColumn( 46 | name = "tag_id", 47 | referencedColumnName = "id", 48 | foreignKey = @ForeignKey(name = "fk_tag_to_post_tag"), 49 | nullable = false 50 | ) 51 | private Tag tag; 52 | 53 | @Builder 54 | public PostTag(MissingPost missingPost, Tag tag) { 55 | ObjectUtils.requireNonEmpty(missingPost, "missingPost must not be null"); 56 | ObjectUtils.requireNonEmpty(tag, "tag must not be null"); 57 | 58 | addMissingPost(missingPost); 59 | this.tag = tag; 60 | } 61 | 62 | private void addMissingPost(MissingPost missingPost) { 63 | if (Objects.nonNull(this.missingPost)) { 64 | missingPost.getPostTags().remove(this); 65 | } 66 | this.missingPost = missingPost; 67 | missingPost.getPostTags().add(this); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/service/NotificationService.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.service; 2 | 3 | import com.pet.common.exception.ExceptionMessage; 4 | import com.pet.domains.account.domain.Account; 5 | import com.pet.domains.account.domain.Notification; 6 | import com.pet.domains.account.dto.response.NotificationReadResults; 7 | import com.pet.domains.account.mapper.NotificationMapper; 8 | import com.pet.domains.account.repository.NotificationRepository; 9 | import java.util.stream.Collectors; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.Pageable; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | @Service 17 | @RequiredArgsConstructor 18 | @Transactional(readOnly = true) 19 | public class NotificationService { 20 | 21 | private final NotificationRepository notificationRepository; 22 | 23 | private final NotificationMapper notificationMapper; 24 | 25 | @Transactional 26 | public void deleteNoticeById(Account account, Long notificationId) { 27 | notificationRepository.deleteByIdAndAccount(notificationId, account); 28 | } 29 | 30 | @Transactional 31 | public void checkNotification(Account account, Long noticeId, boolean checked) { 32 | if (checked) { 33 | Notification notification = notificationRepository.findByIdAndAccount(noticeId, account) 34 | .orElseThrow(ExceptionMessage.NOT_FOUND_NOTIFICATION::getException); 35 | notification.checkReadStatus(); 36 | } 37 | } 38 | 39 | public NotificationReadResults getByAccountId(Account account, Pageable pageable) { 40 | Page notifications = notificationRepository.findByAccount(account, pageable); 41 | return NotificationReadResults.of( 42 | notifications.stream() 43 | .map(notificationMapper::toNotificationDto) 44 | .collect(Collectors.toList()), 45 | notifications.getTotalElements(), 46 | notifications.isLast(), 47 | notifications.getSize() 48 | ); 49 | } 50 | 51 | @Transactional 52 | public void deleteAllByAccount(Account account) { 53 | notificationRepository.deleteAllInBatchByAccount(account); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/domain/Notification.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.domain; 2 | 3 | import com.pet.domains.DeletableEntity; 4 | import com.pet.domains.post.domain.MissingPost; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.ForeignKey; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.JoinColumn; 13 | import javax.persistence.ManyToOne; 14 | import javax.persistence.Table; 15 | import lombok.AccessLevel; 16 | import lombok.Builder; 17 | import lombok.Getter; 18 | import lombok.NoArgsConstructor; 19 | import org.apache.commons.lang3.Validate; 20 | import org.hibernate.annotations.SQLDelete; 21 | import org.hibernate.annotations.Where; 22 | 23 | @Getter 24 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 25 | @SQLDelete(sql = "UPDATE notification SET deleted = true WHERE id=?") 26 | @Where(clause = "deleted = false") 27 | @Entity 28 | @Table(name = "notification") 29 | public class Notification extends DeletableEntity { 30 | 31 | @Id 32 | @GeneratedValue(strategy = GenerationType.IDENTITY) 33 | @Column(name = "id", updatable = false) 34 | private Long id; 35 | 36 | @Column(name = "checked", nullable = false, columnDefinition = "boolean default false") 37 | private boolean checked; 38 | 39 | @ManyToOne(fetch = FetchType.LAZY) 40 | @JoinColumn( 41 | name = "account_id", 42 | referencedColumnName = "id", 43 | nullable = false, 44 | foreignKey = @ForeignKey(name = "fk_notification_to_account") 45 | ) 46 | private Account account; 47 | 48 | @ManyToOne(fetch = FetchType.LAZY) 49 | @JoinColumn( 50 | name = "missing_post_id", 51 | referencedColumnName = "id", 52 | nullable = false, 53 | foreignKey = @ForeignKey(name = "fk_notification_to_missing_post") 54 | ) 55 | private MissingPost missingPost; 56 | 57 | @Builder 58 | public Notification(Account account, MissingPost missingPost) { 59 | Validate.notNull(account, "account must not be null"); 60 | Validate.notNull(missingPost, "account must not be null"); 61 | 62 | this.account = account; 63 | this.missingPost = missingPost; 64 | } 65 | 66 | public void checkReadStatus() { 67 | this.checked = true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/pet/domains/account/repository/SignEmailRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import com.pet.common.config.JpaAuditingConfig; 5 | import com.pet.common.config.QuerydslConfig; 6 | import com.pet.common.exception.ExceptionMessage; 7 | import com.pet.domains.account.domain.SignEmail; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 12 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; 14 | import org.springframework.context.annotation.ComponentScan.Filter; 15 | import org.springframework.context.annotation.FilterType; 16 | 17 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 18 | @DataJpaTest(includeFilters = @Filter( 19 | type = FilterType.ASSIGNABLE_TYPE, 20 | classes = {JpaAuditingConfig.class, QuerydslConfig.class}) 21 | ) 22 | @DisplayName("인증 이메일 DB 테스트") 23 | class SignEmailRepositoryTest { 24 | 25 | @Autowired 26 | private SignEmailRepository signEmailRepository; 27 | 28 | @Autowired 29 | private TestEntityManager entityManager; 30 | 31 | @Test 32 | @DisplayName("인증된 이메일 하나만 조회") 33 | void getVerifyEmail() { 34 | String email = "test@email.com"; 35 | SignEmail s1 = SignEmail.builder().email(email).verifyKey("1234").build(); 36 | SignEmail s2 = SignEmail.builder().email(email).verifyKey("12").build(); 37 | SignEmail s3 = SignEmail.builder().email(email).verifyKey("4321").build(); 38 | SignEmail s4 = SignEmail.builder().email(email).verifyKey("54321").build(); 39 | s3.successVerified(); 40 | 41 | signEmailRepository.save(s1); 42 | signEmailRepository.save(s2); 43 | signEmailRepository.save(s3); 44 | signEmailRepository.save(s4); 45 | 46 | entityManager.flush(); 47 | entityManager.clear(); 48 | 49 | signEmailRepository.deleteById( 50 | signEmailRepository.findByEmailAndIsCheckedTrue(email) 51 | .orElseThrow(ExceptionMessage.INVALID_SIGN_UP::getException).getId()); 52 | 53 | assertThat(signEmailRepository.findAll().size()).isEqualTo(3); 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/account/dto/response/AccountBookmarkPostPageResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.account.dto.response; 2 | 3 | import com.pet.domains.post.domain.SexType; 4 | import java.time.LocalDate; 5 | import java.util.List; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class AccountBookmarkPostPageResults { 10 | 11 | private final List posts; 12 | 13 | private final long totalElements; 14 | 15 | private final boolean last; 16 | 17 | private final long size; 18 | 19 | public AccountBookmarkPostPageResults( 20 | List posts, long totalElements, boolean last, long size 21 | ) { 22 | this.posts = posts; 23 | this.totalElements = totalElements; 24 | this.last = last; 25 | this.size = size; 26 | } 27 | 28 | public static AccountBookmarkPostPageResults of( 29 | List shelters, long totalElements, boolean last, long size 30 | ) { 31 | return new AccountBookmarkPostPageResults(shelters, totalElements, last, size); 32 | } 33 | 34 | @Getter 35 | public static class Post { 36 | 37 | private final Long id; 38 | 39 | private final String animalKind; 40 | 41 | private final SexType sexType; 42 | 43 | private final String city; 44 | 45 | private final String town; 46 | 47 | private final LocalDate createdAt; 48 | 49 | private final String thumbnail; 50 | 51 | private final int bookmarkCount; 52 | 53 | public Post( 54 | Long id, String animalKind, SexType sexType, String city, String town, LocalDate createdAt, 55 | String thumbnail, int bookmarkCount 56 | ) { 57 | this.id = id; 58 | this.animalKind = animalKind; 59 | this.sexType = sexType; 60 | this.city = city; 61 | this.town = town; 62 | this.createdAt = createdAt; 63 | this.thumbnail = thumbnail; 64 | this.bookmarkCount = bookmarkCount; 65 | } 66 | 67 | public static Post of( 68 | Long id, String animalKind, SexType sexType, String city, String town, LocalDate createdAt, 69 | String thumbnail, int bookmarkCount 70 | ) { 71 | return new Post(id, animalKind, sexType, city, town, createdAt, thumbnail, bookmarkCount); 72 | } 73 | 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/comment/dto/response/CommentPageResults.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.comment.dto.response; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class CommentPageResults { 9 | 10 | private List comments; 11 | 12 | private long totalElements; 13 | 14 | private boolean last; 15 | 16 | private long size; 17 | 18 | public CommentPageResults(List comments, long totalElements, boolean last, long size) { 19 | this.comments = comments; 20 | this.totalElements = totalElements; 21 | this.last = last; 22 | this.size = size; 23 | } 24 | 25 | @Getter 26 | public static class Comment { 27 | 28 | private long id; 29 | 30 | private String content; 31 | 32 | private LocalDateTime createdAt; 33 | 34 | private Comment.Account account; 35 | 36 | private List childComments; 37 | 38 | private boolean deleted; 39 | 40 | public Comment(long id, String content, LocalDateTime createdAt, 41 | Account account, 42 | List childComments, boolean deleted) { 43 | this.id = id; 44 | this.content = content; 45 | this.createdAt = createdAt; 46 | this.account = account; 47 | this.childComments = childComments; 48 | this.deleted = deleted; 49 | } 50 | 51 | @Getter 52 | public static class Account { 53 | 54 | private long id; 55 | 56 | private String nickname; 57 | 58 | private String image; 59 | 60 | public Account(long id, String nickname, String image) { 61 | this.id = id; 62 | this.nickname = nickname; 63 | this.image = image; 64 | } 65 | } 66 | 67 | @Getter 68 | public static class ChildComment { 69 | 70 | private long id; 71 | 72 | private String content; 73 | 74 | private LocalDateTime createdAt; 75 | 76 | private Comment.Account account; 77 | 78 | public ChildComment(long id, String content, LocalDateTime createdAt, 79 | Account account) { 80 | this.id = id; 81 | this.content = content; 82 | this.createdAt = createdAt; 83 | this.account = account; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/pet/domains/statistics/domain/PostStatistics.java: -------------------------------------------------------------------------------- 1 | package com.pet.domains.statistics.domain; 2 | 3 | import com.pet.domains.BaseEntity; 4 | import java.time.LocalDateTime; 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import lombok.AccessLevel; 12 | import lombok.Builder; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | import org.apache.commons.lang3.Validate; 16 | import org.apache.commons.lang3.builder.ToStringBuilder; 17 | import org.apache.commons.lang3.builder.ToStringStyle; 18 | 19 | @Getter 20 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 21 | @Entity 22 | @Table(name = "post_statistics") 23 | public class PostStatistics extends BaseEntity { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | @Column(name = "id", updatable = false) 28 | private Long id; 29 | 30 | @Column(name = "missing", columnDefinition = "BIGINT default 0", nullable = false) 31 | private long missing; 32 | 33 | @Column(name = "detection", columnDefinition = "BIGINT default 0", nullable = false) 34 | private long detection; 35 | 36 | @Column(name = "protection", columnDefinition = "BIGINT default 0", nullable = false) 37 | private long protection; 38 | 39 | @Column(name = "completion", columnDefinition = "BIGINT default 0", nullable = false) 40 | private long completion; 41 | 42 | @Column(name = "date", nullable = false, updatable = false, columnDefinition = "TIMESTAMP") 43 | private LocalDateTime date; 44 | 45 | @Builder 46 | public PostStatistics(long missing, long detection, long protection, long completion, LocalDateTime date) { 47 | Validate.notNull(date, "date must not be null"); 48 | 49 | this.missing = missing; 50 | this.detection = detection; 51 | this.protection = protection; 52 | this.completion = completion; 53 | this.date = date; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 59 | .append("id", id) 60 | .append("missing", missing) 61 | .append("detection", detection) 62 | .append("protection", protection) 63 | .append("completion", completion) 64 | .append("date", date) 65 | .toString(); 66 | } 67 | } 68 | --------------------------------------------------------------------------------