├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── renovate.json ├── src ├── main │ ├── java │ │ └── bokjak │ │ │ └── bokjakserver │ │ │ ├── domain │ │ │ ├── user │ │ │ │ ├── model │ │ │ │ │ ├── Role.java │ │ │ │ │ ├── UserStatus.java │ │ │ │ │ ├── SocialType.java │ │ │ │ │ ├── BlackList.java │ │ │ │ │ ├── RevokeUser.java │ │ │ │ │ ├── UserBlockUser.java │ │ │ │ │ └── SleepingUser.java │ │ │ │ ├── repository │ │ │ │ │ ├── BlackListRepository.java │ │ │ │ │ ├── RevokeUserRepository.java │ │ │ │ │ ├── UserBlockUserRepository.java │ │ │ │ │ ├── SleepingUserRepository.java │ │ │ │ │ └── UserRepository.java │ │ │ │ ├── exeption │ │ │ │ │ ├── UserException.java │ │ │ │ │ └── AuthException.java │ │ │ │ ├── dto │ │ │ │ │ └── UserDto.java │ │ │ │ ├── service │ │ │ │ │ ├── SocialService.java │ │ │ │ │ └── KakaoService.java │ │ │ │ └── controller │ │ │ │ │ ├── AuthController.java │ │ │ │ │ └── UserController.java │ │ │ ├── ban │ │ │ │ ├── repository │ │ │ │ │ ├── BanRepositoryCustom.java │ │ │ │ │ ├── BanRepository.java │ │ │ │ │ └── BanRepositoryImpl.java │ │ │ │ ├── exception │ │ │ │ │ └── BanException.java │ │ │ │ ├── service │ │ │ │ │ └── BanService.java │ │ │ │ ├── model │ │ │ │ │ └── Ban.java │ │ │ │ ├── dto │ │ │ │ │ └── BanDto.java │ │ │ │ └── controller │ │ │ │ │ └── BanController.java │ │ │ ├── spot │ │ │ │ ├── repository │ │ │ │ │ ├── SpotImageRepository.java │ │ │ │ │ ├── SpotRepository.java │ │ │ │ │ └── SpotRepositoryCustom.java │ │ │ │ ├── exception │ │ │ │ │ └── SpotException.java │ │ │ │ └── model │ │ │ │ │ ├── SpotImage.java │ │ │ │ │ └── Spot.java │ │ │ ├── category │ │ │ │ ├── repository │ │ │ │ │ ├── SpotCategoryRepository.java │ │ │ │ │ └── LocationCategoryRepository.java │ │ │ │ ├── exception │ │ │ │ │ └── CategoryException.java │ │ │ │ ├── dto │ │ │ │ │ ├── CongestionLevelChoice.java │ │ │ │ │ ├── CongestionHistoricalDateChoice.java │ │ │ │ │ └── CategoryDto.java │ │ │ │ ├── model │ │ │ │ │ ├── SpotCategory.java │ │ │ │ │ └── LocationCategory.java │ │ │ │ ├── controller │ │ │ │ │ └── CategoryController.java │ │ │ │ └── service │ │ │ │ │ └── CategoryService.java │ │ │ ├── location │ │ │ │ ├── repository │ │ │ │ │ ├── LocationRepository.java │ │ │ │ │ └── LocationRepositoryCustom.java │ │ │ │ ├── exception │ │ │ │ │ └── LocationException.java │ │ │ │ ├── model │ │ │ │ │ └── Location.java │ │ │ │ └── dto │ │ │ │ │ └── LocationDto.java │ │ │ ├── comment │ │ │ │ ├── repository │ │ │ │ │ ├── CommentRepository.java │ │ │ │ │ └── CommentRepositoryCustom.java │ │ │ │ ├── exception │ │ │ │ │ └── CommentException.java │ │ │ │ ├── model │ │ │ │ │ └── Comment.java │ │ │ │ └── dto │ │ │ │ │ └── CommentDto.java │ │ │ ├── notification │ │ │ │ ├── repository │ │ │ │ │ ├── NotificationRepository.java │ │ │ │ │ ├── NotificationRepositoryCustom.java │ │ │ │ │ └── NotificationRepositoryImpl.java │ │ │ │ ├── exception │ │ │ │ │ └── NotificationException.java │ │ │ │ ├── dto │ │ │ │ │ └── FcmDto.java │ │ │ │ ├── model │ │ │ │ │ ├── NotificationType.java │ │ │ │ │ └── Notification.java │ │ │ │ ├── controller │ │ │ │ │ └── NotificationController.java │ │ │ │ └── service │ │ │ │ │ ├── NotificationService.java │ │ │ │ │ └── FcmService.java │ │ │ ├── report │ │ │ │ ├── exception │ │ │ │ │ └── ReportException.java │ │ │ │ ├── model │ │ │ │ │ ├── ReportTarget.java │ │ │ │ │ └── Report.java │ │ │ │ ├── repository │ │ │ │ │ └── ReportRepository.java │ │ │ │ ├── controller │ │ │ │ │ └── ReportController.java │ │ │ │ ├── dto │ │ │ │ │ └── ReportDto.java │ │ │ │ └── service │ │ │ │ │ └── ReportService.java │ │ │ ├── congestion │ │ │ │ ├── repository │ │ │ │ │ ├── WeeklyCongestionStatisticRepository.java │ │ │ │ │ ├── CongestionRepository.java │ │ │ │ │ └── DailyCongestionStatisticRepository.java │ │ │ │ ├── exception │ │ │ │ │ └── CongestionException.java │ │ │ │ ├── service │ │ │ │ │ └── CongestionService.java │ │ │ │ ├── model │ │ │ │ │ ├── WeeklyCongestionStatistic.java │ │ │ │ │ ├── Congestion.java │ │ │ │ │ ├── DailyCongestionStatistic.java │ │ │ │ │ └── CongestionLevel.java │ │ │ │ └── dto │ │ │ │ │ └── CongestionDto.java │ │ │ ├── bookmark │ │ │ │ ├── repository │ │ │ │ │ ├── SpotBookmarkRepository.java │ │ │ │ │ └── LocationBookmarkRepository.java │ │ │ │ └── model │ │ │ │ │ ├── SpotBookmark.java │ │ │ │ │ └── LocationBookmark.java │ │ │ ├── image │ │ │ │ ├── exception │ │ │ │ │ └── ImageException.java │ │ │ │ ├── S3SaveDir.java │ │ │ │ ├── dto │ │ │ │ │ └── ImageDto.java │ │ │ │ └── controller │ │ │ │ │ └── ImageController.java │ │ │ └── test │ │ │ │ └── TestController.java │ │ │ ├── util │ │ │ ├── enums │ │ │ │ ├── EnumModel.java │ │ │ │ ├── EnumValue.java │ │ │ │ └── EnumQueryValue.java │ │ │ ├── queries │ │ │ │ ├── OrderByNull.java │ │ │ │ └── SortOrder.java │ │ │ ├── CursorUtil.java │ │ │ ├── web │ │ │ │ └── PreventNullResponseUtils.java │ │ │ ├── CustomSliceExecutionUtils.java │ │ │ ├── client │ │ │ │ └── ClientIPAddressUtils.java │ │ │ ├── CustomDateUtils.java │ │ │ ├── image │ │ │ │ └── ImageFilePathUtils.java │ │ │ └── CustomEncryptUtil.java │ │ │ ├── common │ │ │ ├── constant │ │ │ │ ├── MessageConstants.java │ │ │ │ ├── ConstraintConstants.java │ │ │ │ └── GlobalConstants.java │ │ │ ├── dummy │ │ │ │ ├── DummySpotCategory.java │ │ │ │ ├── BuzzingDummy.java │ │ │ │ ├── DummyLocationCategory.java │ │ │ │ ├── CategoryDummy.java │ │ │ │ ├── CommentDummy.java │ │ │ │ ├── UserDummy.java │ │ │ │ ├── SpotDummy.java │ │ │ │ └── CongestionDummy.java │ │ │ ├── exception │ │ │ │ ├── BuzException.java │ │ │ │ └── StatusCode.java │ │ │ ├── model │ │ │ │ └── BaseEntity.java │ │ │ ├── HelloController.java │ │ │ └── dto │ │ │ │ ├── ApiResponse.java │ │ │ │ └── PageResponse.java │ │ │ ├── config │ │ │ ├── WebConfig.java │ │ │ ├── swagger │ │ │ │ ├── SwaggerController.java │ │ │ │ └── SwaggerConfig.java │ │ │ ├── querydsl │ │ │ │ └── QuerydslConfig.java │ │ │ ├── security │ │ │ │ ├── SecurityUtils.java │ │ │ │ ├── PrincipalDetailService.java │ │ │ │ ├── PrincipalDetails.java │ │ │ │ └── SecurityConfig.java │ │ │ ├── jwt │ │ │ │ ├── JwtDto.java │ │ │ │ ├── JwtAuthenticationFilter.java │ │ │ │ ├── JwtAuthenticationEntryPoint.java │ │ │ │ └── JwtAccessDeniedHandler.java │ │ │ ├── AsyncConfig.java │ │ │ ├── redis │ │ │ │ ├── RedisService.java │ │ │ │ └── RedisConfig.java │ │ │ └── AwsS3 │ │ │ │ └── AwsS3Config.java │ │ │ ├── web │ │ │ └── log │ │ │ │ ├── ReadableRequestBodyWrapperFilter.java │ │ │ │ └── LoggerAspect.java │ │ │ └── BokjakserverApplication.java │ └── resources │ │ ├── application.yml │ │ ├── templates │ │ └── convertSleepingMail.html │ │ └── logback-spring.xml └── test │ └── java │ └── bokjak │ └── bokjakserver │ ├── BokjakserverApplicationTests.java │ └── domain │ ├── location │ ├── LocationMockUtils.java │ └── LocationServiceTest.java │ └── user │ └── UserTemplate.java ├── Dockerfile ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── pr-test.yml │ └── deploy.yml ├── scripts └── health_check.sh ├── .gitignore ├── README.md └── gradlew.bat /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'bokjakserver' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akatsuki-USW/Buzzzzing-Server/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/Role.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | public enum Role { 4 | ROLE_USER, ROLE_ADMIN 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/enums/EnumModel.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.enums; 2 | 3 | public interface EnumModel { 4 | String getKey(); 5 | 6 | V getValue(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/UserStatus.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | public enum UserStatus { 4 | NORMAL,BANNED,BLACKLIST,SLEEP,DELETED 5 | } 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-jdk 2 | ENV APP_HOME=/home/ubuntu/Buzzzzing-Server 3 | WORKDIR $APP_HOME 4 | COPY build/libs/*.jar buzzing-server.jar 5 | EXPOSE 8080 6 | ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","buzzing-server.jar"] 7 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/constant/MessageConstants.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.constant; 2 | 3 | public class MessageConstants { 4 | public static final String S3_FILE_DELETE_SUCCESS = "file delete success."; 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR 요약 2 | 3 | ## 구현한 내용 또는 수정한 내용 4 | 5 | ## 추가로 알리고 싶은 내용 6 | 7 | ## 관련 이슈 8 | 9 | ## 체크리스트 10 | 11 | - [ ] 본인을 Assign해주시고, 본인을 제외한 백엔드 개발자를 Reviewer로 지정해주세요. 12 | - [ ] WBS 업데이트해주세요. (제목에 WBS 번호 달아주세요) 13 | - [ ] 라벨 체크해주세요. 14 | - [ ] .yml 파일 수정 내용이 있다면 공유해주세요! 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/repository/BanRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.repository; 2 | 3 | import bokjak.bokjakserver.domain.ban.model.Ban; 4 | 5 | import java.util.List; 6 | 7 | public interface BanRepositoryCustom { 8 | 9 | List findLimitCountAndSortByRecent(Long userId); 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/bokjak/bokjakserver/BokjakserverApplicationTests.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | //@SpringBootTest 7 | class BokjakserverApplicationTests { 8 | 9 | // @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/spot/repository/SpotImageRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.spot.repository; 2 | 3 | import bokjak.bokjakserver.domain.spot.model.SpotImage; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface SpotImageRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/spot/repository/SpotRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.spot.repository; 2 | 3 | import bokjak.bokjakserver.domain.spot.model.Spot; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface SpotRepository extends JpaRepository, SpotRepositoryCustom { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/DummySpotCategory.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum DummySpotCategory { 9 | CAFE("카페"), PLAY("놀거리"), RESTAURANT("맛집"); 10 | 11 | private final String name; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/repository/SpotCategoryRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.repository; 2 | 3 | import bokjak.bokjakserver.domain.category.model.SpotCategory; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface SpotCategoryRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/location/repository/LocationRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location.repository; 2 | 3 | import bokjak.bokjakserver.domain.location.model.Location; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface LocationRepository extends JpaRepository, LocationRepositoryCustom { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/repository/LocationCategoryRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.repository; 2 | 3 | import bokjak.bokjakserver.domain.category.model.LocationCategory; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface LocationCategoryRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/repository/BlackListRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.repository; 2 | 3 | import bokjak.bokjakserver.domain.user.model.BlackList; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface BlackListRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/comment/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.comment.repository; 2 | 3 | import bokjak.bokjakserver.domain.comment.model.Comment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | 7 | public interface CommentRepository extends JpaRepository, CommentRepositoryCustom{ 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/repository/NotificationRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.repository; 2 | 3 | import bokjak.bokjakserver.domain.notification.model.Notification; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface NotificationRepository extends JpaRepository, NotificationRepositoryCustom { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/exception/ReportException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class ReportException extends BuzException { 7 | public ReportException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/repository/WeeklyCongestionStatisticRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.repository; 2 | 3 | import bokjak.bokjakserver.domain.congestion.model.WeeklyCongestionStatistic; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface WeeklyCongestionStatisticRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config; 2 | 3 | import org.json.simple.parser.JSONParser; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | public class WebConfig { 9 | 10 | @Bean 11 | public JSONParser jsonParser() { 12 | return new JSONParser(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/exception/NotificationException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class NotificationException extends BuzException { 7 | 8 | public NotificationException(StatusCode statusCode) { 9 | super(statusCode); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/bookmark/repository/SpotBookmarkRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.bookmark.repository; 2 | 3 | import bokjak.bokjakserver.domain.bookmark.model.SpotBookmark; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface SpotBookmarkRepository extends JpaRepository { 9 | List findAllByUserId(Long userId); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/repository/RevokeUserRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.repository; 2 | 3 | import bokjak.bokjakserver.domain.user.model.RevokeUser; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RevokeUserRepository extends JpaRepository { 9 | 10 | Optional findBySocialEmail(String socialEmail); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/repository/CongestionRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.repository; 2 | 3 | import bokjak.bokjakserver.domain.congestion.model.Congestion; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface CongestionRepository extends JpaRepository { 9 | Optional findTopByLocationIdOrderByObservedAtDesc(Long locationId); 10 | } 11 | -------------------------------------------------------------------------------- /scripts/health_check.sh: -------------------------------------------------------------------------------- 1 | TARGET_URL=localhost PORT=8080 2 | 3 | for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10; do 4 | echo "> #${RETRY_COUNT} trying..." 5 | RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" $TARGET_URL:$PORT/hello) # HTTP status code 응답받기 6 | 7 | if [ "${RESPONSE_CODE}" -eq 200 ]; then 8 | echo "> New WAS successfully running" 9 | exit 0 10 | elif [ ${RETRY_COUNT} -eq 10 ]; then 11 | echo "> Health check failed." 12 | exit 1 13 | fi 14 | sleep 10 15 | done 16 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/repository/BanRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.repository; 2 | 3 | import bokjak.bokjakserver.domain.ban.model.Ban; 4 | import bokjak.bokjakserver.domain.user.model.User; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | 9 | public interface BanRepository extends JpaRepository, BanRepositoryCustom { 10 | 11 | Optional findByUserAndIsBannedIsTrue(User user); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/queries/OrderByNull.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.queries; 2 | 3 | import com.querydsl.core.types.NullExpression; 4 | import com.querydsl.core.types.Order; 5 | import com.querydsl.core.types.OrderSpecifier; 6 | 7 | public class OrderByNull extends OrderSpecifier { 8 | public static final OrderByNull DEFAULT = new OrderByNull(); 9 | 10 | private OrderByNull() { 11 | super(Order.ASC, NullExpression.DEFAULT, NullHandling.Default); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/exception/BuzException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.exception; 2 | 3 | public class BuzException extends RuntimeException { 4 | public StatusCode statusCode; 5 | public BuzException(StatusCode statusCode) { 6 | super(statusCode.getMessage()); 7 | this.statusCode=statusCode; 8 | } 9 | 10 | public BuzException(StatusCode statusCode, String message) { 11 | super(message); 12 | this.statusCode = statusCode; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/repository/NotificationRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.repository; 2 | 3 | import bokjak.bokjakserver.domain.notification.model.Notification; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public interface NotificationRepositoryCustom { 9 | List findLimitCountAndSortByRecent(Long userId); 10 | Optional findByNotificationIdAndUserId(Long notificationId, Long userId); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/exception/BanException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class BanException extends BuzException { 7 | public BanException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | 11 | public BanException(StatusCode statusCode, String message) { 12 | super(statusCode, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/exeption/UserException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.exeption; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class UserException extends BuzException { 7 | public UserException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | 11 | public UserException(StatusCode statusCode, String message) { 12 | super(statusCode, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/spot/exception/SpotException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.spot.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class SpotException extends BuzException { 7 | 8 | public SpotException(StatusCode statusCode) { 9 | super(statusCode); 10 | } 11 | 12 | public SpotException(StatusCode statusCode, String message) { 13 | super(statusCode, message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/repository/UserBlockUserRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.repository; 2 | 3 | import bokjak.bokjakserver.domain.user.model.User; 4 | import bokjak.bokjakserver.domain.user.model.UserBlockUser; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | 9 | public interface UserBlockUserRepository extends JpaRepository { 10 | boolean existsByBlockerUserAndBlockedUser(User blockerUser, User blockedUser); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/comment/exception/CommentException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.comment.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class CommentException extends BuzException { 7 | public CommentException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | 11 | public CommentException(StatusCode statusCode, String message) { 12 | super(statusCode, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/exception/CategoryException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class CategoryException extends BuzException { 7 | public CategoryException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | 11 | public CategoryException(StatusCode statusCode, String message) { 12 | super(statusCode, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/location/exception/LocationException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class LocationException extends BuzException { 7 | public LocationException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | 11 | public LocationException(StatusCode statusCode, String message) { 12 | super(statusCode, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/queries/SortOrder.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.queries; 2 | 3 | import bokjak.bokjakserver.util.enums.EnumModel; 4 | import lombok.AllArgsConstructor; 5 | 6 | @AllArgsConstructor 7 | public enum SortOrder implements EnumModel { 8 | ASC("ASC"), DESC("DESC"); 9 | 10 | private final String value; 11 | 12 | @Override 13 | public String getKey() { 14 | return name(); 15 | } 16 | 17 | @Override 18 | public String getValue() { 19 | return value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/exception/CongestionException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | public class CongestionException extends BuzException { 7 | public CongestionException(StatusCode statusCode) { 8 | super(statusCode); 9 | } 10 | 11 | public CongestionException(StatusCode statusCode, String message) { 12 | super(statusCode, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/swagger/SwaggerController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.swagger; 2 | 3 | import io.swagger.v3.oas.annotations.Hidden; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | 8 | @Controller 9 | @RequestMapping("/docs") 10 | @Hidden 11 | public class SwaggerController { 12 | @GetMapping 13 | public String redirect() { 14 | return "redirect:/swagger-ui/index.html"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/BuzzingDummy.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import org.springframework.context.annotation.Profile; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | 12 | @Target(ElementType.TYPE) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Transactional 15 | @Profile({"local"}) // 특정 프로파일에만 더미 파일 작동 16 | @interface BuzzingDummy { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/CursorUtil.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util; 2 | 3 | public class CursorUtil { 4 | public static final int TEN = 10; 5 | public static final int TWENTY = 20; 6 | 7 | public static Long getNextCursorId(Long currentCursorId, int amount, Long totalElements) { 8 | if (currentCursorId == null) { 9 | return (long) amount; 10 | } 11 | long nextCursorId = currentCursorId + amount; 12 | if (totalElements < nextCursorId) { 13 | return null; 14 | } 15 | return nextCursorId; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/repository/SleepingUserRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.repository; 2 | 3 | import bokjak.bokjakserver.domain.user.model.SleepingUser; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface SleepingUserRepository extends JpaRepository { 11 | 12 | Optional findBySocialEmail(String socialEmail); 13 | List findByLastLoginDateBefore(LocalDateTime time); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/model/ReportTarget.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.model; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.report.exception.ReportException; 5 | 6 | public enum ReportTarget { 7 | SPOT,COMMENT; 8 | 9 | public static ReportTarget toEnum(String reportTarget) { 10 | return switch (reportTarget.toUpperCase()) { 11 | case "SPOT" -> SPOT; 12 | case "COMMENT" -> COMMENT; 13 | default -> throw new ReportException(StatusCode.NOT_FOUND_REPORT_TARGET); 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/dto/CongestionLevelChoice.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.dto; 2 | 3 | import bokjak.bokjakserver.util.enums.EnumModel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public enum CongestionLevelChoice implements EnumModel { 10 | RELAX("여유순"), NORMAL("보통"), BUZZING("복쟉순"); 11 | 12 | private final String value; 13 | 14 | @Override 15 | public String getKey() { 16 | return name(); 17 | } 18 | 19 | @Override 20 | public String getValue() { 21 | return value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/querydsl/QuerydslConfig.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.querydsl; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.persistence.PersistenceContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class QuerydslConfig { 11 | @PersistenceContext 12 | private EntityManager entityManager; 13 | 14 | @Bean 15 | public JPAQueryFactory jpaQueryFactory(){ 16 | return new JPAQueryFactory(entityManager); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/repository/DailyCongestionStatisticRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.repository; 2 | 3 | import bokjak.bokjakserver.domain.congestion.model.DailyCongestionStatistic; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.Optional; 8 | 9 | public interface DailyCongestionStatisticRepository extends JpaRepository { 10 | Optional findTop1ByLocationIdAndCreatedAtBetweenOrderByCreatedAtDesc(Long locationId, LocalDateTime start, LocalDateTime end); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/image/exception/ImageException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.image.exception; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | @Getter 9 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 10 | public class ImageException extends RuntimeException{ 11 | StatusCode awsS3ErrorCode; 12 | 13 | public ImageException(StatusCode awsS3ErrorCode) { 14 | super(awsS3ErrorCode.getMessage()); 15 | this.awsS3ErrorCode = awsS3ErrorCode; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/web/PreventNullResponseUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.web; 2 | 3 | 4 | public class PreventNullResponseUtils { 5 | private static final String ALTERNATIVE_NICKNAME = "알 수 없음"; 6 | public static final String ALTERNATIVE_PROFILE_IMAGE_URL = ""; 7 | 8 | public static String resolveUserNicknameFromNullable(String nickname) { 9 | return nickname != null ? nickname : ALTERNATIVE_NICKNAME; 10 | } 11 | 12 | public static String resolveUserProfileImageUrlFromNullable(String profileImageUrl) { 13 | return profileImageUrl != null ? profileImageUrl : ALTERNATIVE_PROFILE_IMAGE_URL; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/repository/ReportRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.repository; 2 | 3 | import bokjak.bokjakserver.domain.report.model.Report; 4 | import bokjak.bokjakserver.domain.report.model.ReportTarget; 5 | import bokjak.bokjakserver.domain.user.model.User; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import java.util.List; 9 | 10 | public interface ReportRepository extends JpaRepository { 11 | 12 | boolean existsByReporterAndReportedUserAndReportTargetAndTargetIdAndIsCheckedFalse(User reporterUser, User reportedUser, ReportTarget reportTarget, Long TargetId); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/SocialType.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | import lombok.Getter; 4 | import org.springframework.http.HttpMethod; 5 | 6 | @Getter 7 | public enum SocialType { 8 | KAKAO( 9 | "kakao", 10 | "https://kapi.kakao.com/v2/user/me", 11 | HttpMethod.GET 12 | ); 13 | 14 | private String socialName; 15 | private String socialUrl; 16 | private HttpMethod httpMethod; 17 | 18 | SocialType(String socialName, String socialUrl, HttpMethod httpMethod) { 19 | this.socialName = socialName; 20 | this.socialUrl = socialUrl; 21 | this.httpMethod = httpMethod; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/dto/FcmDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.dto; 2 | 3 | public class FcmDto { 4 | public record FcmMessage( 5 | Boolean validate_only, 6 | Message message 7 | ) {} 8 | 9 | public record Message( 10 | String token, 11 | Notification data 12 | ) {} 13 | 14 | public record Notification( 15 | String title, 16 | String body, 17 | String redirectTargetId, 18 | String type 19 | ) {} 20 | 21 | public record PushMessage( 22 | Long receiverId, 23 | String title, 24 | String body 25 | ) {} 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/model/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.model; 2 | 3 | import jakarta.persistence.EntityListeners; 4 | import jakarta.persistence.MappedSuperclass; 5 | import lombok.Getter; 6 | import org.springframework.data.annotation.CreatedDate; 7 | import org.springframework.data.annotation.LastModifiedDate; 8 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | @Getter 13 | @EntityListeners(AuditingEntityListener.class) 14 | @MappedSuperclass 15 | public abstract class BaseEntity { 16 | @CreatedDate 17 | protected LocalDateTime createdAt; 18 | @LastModifiedDate 19 | protected LocalDateTime updatedAt; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/security/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.security; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.user.exeption.UserException; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | 8 | public class SecurityUtils { 9 | public static String getCurrentUserSocialEmail() { 10 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 11 | if (authentication == null || authentication.getName() == null) { 12 | throw new UserException(StatusCode.NOT_FOUND_USER); 13 | } 14 | return authentication.getName(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/BlackList.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import jakarta.persistence.*; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | @Entity 13 | @AllArgsConstructor 14 | @Builder 15 | @NoArgsConstructor 16 | @Getter 17 | public class BlackList extends BaseEntity { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | @Column(name = "black_list_id") 22 | private Long id; 23 | 24 | private String socialEmail; 25 | private LocalDateTime banStartedAt; 26 | private LocalDateTime banEndedAt; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/bookmark/repository/LocationBookmarkRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.bookmark.repository; 2 | 3 | import bokjak.bokjakserver.domain.bookmark.model.LocationBookmark; 4 | import bokjak.bokjakserver.domain.location.model.Location; 5 | import bokjak.bokjakserver.domain.user.model.User; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public interface LocationBookmarkRepository extends JpaRepository { 12 | Boolean existsByUserAndLocation(User user, Location location); 13 | 14 | Optional findByUserAndLocation(User user, Location location); 15 | 16 | List findAllByUser(User user); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/RevokeUser.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | @Entity 12 | @AllArgsConstructor 13 | @Builder 14 | @NoArgsConstructor 15 | @Getter 16 | public class RevokeUser { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | @Column(name = "revoke_user_id") 21 | private Long id; 22 | 23 | private String socialEmail; 24 | private LocalDateTime revokedAt; 25 | 26 | public void deleteRevokeUser() { 27 | this.socialEmail=null; 28 | this.revokedAt=null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/image/S3SaveDir.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.image; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.image.exception.ImageException; 5 | import lombok.AllArgsConstructor; 6 | 7 | @AllArgsConstructor 8 | public enum S3SaveDir { 9 | USER_PROFILE("/user/profile"), 10 | SPOT("/spot"), 11 | ETC("/etc"); 12 | 13 | public final String path; 14 | 15 | public static S3SaveDir toEnum(String stringParam) { 16 | return switch (stringParam) { 17 | case "profile" -> USER_PROFILE; 18 | case "spot" -> SPOT; 19 | case "etc" -> ETC; 20 | 21 | default -> throw new ImageException(StatusCode.AWS_S3_FILE_TYPE_INVALID); 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ## 커스텀 9 | 10 | # 로컬 환경 설정 정보 11 | application-local.yml 12 | application-dev.yml 13 | application-prod.yml 14 | /src/main/generated/ 15 | memo.md 16 | /src/main/resources/buzzzzing-firebase-private-key.json 17 | 18 | ### STS ### 19 | .apt_generated 20 | .classpath 21 | .factorypath 22 | .project 23 | .settings 24 | .springBeans 25 | .sts4-cache 26 | bin/ 27 | !**/src/main/**/bin/ 28 | !**/src/test/**/bin/ 29 | 30 | ### IntelliJ IDEA ### 31 | .idea 32 | *.iws 33 | *.iml 34 | *.ipr 35 | out/ 36 | !**/src/main/**/out/ 37 | !**/src/test/**/out/ 38 | 39 | ### NetBeans ### 40 | /nbproject/private/ 41 | /nbbuild/ 42 | /dist/ 43 | /nbdist/ 44 | /.nb-gradle/ 45 | 46 | ### VS Code ### 47 | .vscode/ 48 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/HelloController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common; 2 | 3 | import io.swagger.v3.oas.annotations.Hidden; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @Slf4j 12 | @RestController 13 | @RequestMapping("/hello") 14 | @RequiredArgsConstructor 15 | @Hidden 16 | public class HelloController { 17 | @Value("${server.port}") 18 | private String port; 19 | 20 | @GetMapping() 21 | public String sayHello() { 22 | return "✅ server listening on port " + port; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/exeption/AuthException.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.exeption; 2 | 3 | import bokjak.bokjakserver.common.exception.BuzException; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | 6 | import java.util.HashMap; 7 | 8 | public class AuthException extends BuzException { 9 | public Object data; 10 | 11 | public AuthException(StatusCode statusCode) { 12 | super(statusCode); 13 | this.data = new HashMap(); 14 | } 15 | public AuthException(StatusCode statusCode, String message) { 16 | super(statusCode, message); 17 | this.data= new HashMap(); 18 | } 19 | public AuthException(StatusCode statusCode, Object data) { 20 | super(statusCode); 21 | this.data = data; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/bokjak/bokjakserver/domain/location/LocationMockUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location; 2 | 3 | import bokjak.bokjakserver.domain.category.model.LocationCategory; 4 | import bokjak.bokjakserver.domain.location.model.Location; 5 | 6 | public class LocationMockUtils { 7 | public static Location makeDummyLocation() { 8 | LocationCategory category = LocationCategory.builder() 9 | .id(2L) 10 | .name("놀이공원") 11 | .iconImageUrl("https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/amusement.png") 12 | .build(); 13 | 14 | return Location.builder() 15 | .name("망원한강공원") 16 | .id(83L) 17 | .apiId(94) 18 | .locationCategory(category) 19 | .build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/jwt/JwtDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.jwt; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record JwtDto ( 6 | @Schema(description = "accessToken", example = "bear eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ2dkY3NER0VG9EREs2Q2NLOVFCYXpma0dOTWNNS29pdkJlNGcrbWRCVXgwPSIsInJvbGUiOiJST0xFX1VTRVIiLCJpYXQiOjE2NzI5ODUwNDYsImV4cCI6MTY3Mjk4ODA0Nn0.IBAJmsKYTQuHGnv4qt14kLY1mTRZK67Xk7iS_P4yGV-mUuiZla84ezgUdpDfdphotFb9tgc-Gzk4wIWXgMZX8w") 7 | String accessToken, 8 | @Schema(description = "refreshToken", example = "bear eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ2dkY3NER0VG9EREs2Q2NLOVFCYXpma0dOTWNNS29pdkJlNGcrbWRCVXgwPSIsImlhdCI6MTY3Mjk4NTA0NiwiZXhwIjoxNjcyOTkxMDQ2fQ.XrjxsDGAKsD6MSdYHAAt9cGgVLZd7Vlf627YHfgRPLoueOYUV9MV9ZjD6mvWxTeHGa85xGjwOPDCKAxtroySAQ") 9 | String refreshToken 10 | ) { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.repository; 2 | 3 | import bokjak.bokjakserver.domain.user.model.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface UserRepository extends JpaRepository { 11 | Optional findByEmail(String email); 12 | Optional findByNickname(String nickname); 13 | Optional findBySocialEmail(String socialEmail); 14 | boolean existsByEmail(String email); 15 | boolean existsByNickname(String nickname); 16 | 17 | List findByLastLoginDateBetween(LocalDateTime startTime, LocalDateTime endTime); 18 | 19 | List findByLastLoginDateBefore(LocalDateTime beforeTime); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/model/NotificationType.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.model; 2 | 3 | import bokjak.bokjakserver.domain.spot.model.Spot; 4 | import bokjak.bokjakserver.domain.user.model.User; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | @Getter 10 | public enum NotificationType { 11 | 12 | /** 13 | * 내 게시글에 답글 14 | */ 15 | CREATE_SPOT_COMMENT(ReceiverType.AUTHOR, Spot.class), 16 | 17 | /** 18 | * 내 답글에 답글(대댓글) 19 | */ 20 | CREATE_SPOT_COMMENT_COMMENT(ReceiverType.AUTHOR, Spot.class), 21 | 22 | TEST_USER_ITSELF(ReceiverType.USER, User.class); 23 | 24 | private enum ReceiverType { 25 | AUTHOR,USER 26 | } 27 | 28 | private final ReceiverType receiverType; 29 | private final Class redirectTargetClass; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/enums/EnumValue.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.enums; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | @Getter 10 | public class EnumValue {// Enum -> DTO 변환용 객체. https://jojoldu.tistory.com/122 11 | private final String key; 12 | private final V value; 13 | 14 | public EnumValue(EnumModel enumModel) { 15 | this.key = enumModel.getKey(); 16 | this.value = enumModel.getValue(); 17 | } 18 | 19 | // enum -> DTO 20 | public static List> toEnumValues(Class enumModelClass) { 21 | return Arrays 22 | .stream(enumModelClass.getEnumConstants()) 23 | .map(EnumValue::new) 24 | .collect(Collectors.toList()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 7 | 8 | import java.util.concurrent.Executor; 9 | 10 | @Configuration 11 | @EnableAsync 12 | public class AsyncConfig { 13 | 14 | @Bean(name = "AsyncBean") 15 | public Executor asyncThreadTaskExecutor() { 16 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 17 | executor.setCorePoolSize(50); // 기본 스레드 수 18 | executor.setMaxPoolSize(100); // 최대 스레드 수 19 | executor.setQueueCapacity(200); // Queue 사이즈 20 | executor.setThreadNamePrefix("AsyncThread"); 21 | return executor; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/service/CongestionService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.service; 2 | 3 | import bokjak.bokjakserver.domain.congestion.model.Congestion; 4 | import bokjak.bokjakserver.domain.congestion.repository.CongestionRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import java.util.Optional; 11 | 12 | 13 | @Slf4j 14 | @Service 15 | @Transactional(readOnly = true) 16 | @RequiredArgsConstructor 17 | public class CongestionService { 18 | CongestionRepository congestionRepository; 19 | 20 | public Optional getCurrentCongestionOfLocation(Long locationId) { 21 | return congestionRepository.findTopByLocationIdOrderByObservedAtDesc(locationId); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/repository/BanRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.repository; 2 | 3 | import bokjak.bokjakserver.domain.ban.model.Ban; 4 | import com.querydsl.jpa.impl.JPAQuery; 5 | import com.querydsl.jpa.impl.JPAQueryFactory; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | import static bokjak.bokjakserver.domain.ban.model.QBan.ban; 11 | 12 | @RequiredArgsConstructor 13 | public class BanRepositoryImpl implements BanRepositoryCustom { 14 | 15 | private final JPAQueryFactory queryFactory; 16 | 17 | @Override 18 | public List findLimitCountAndSortByRecent(Long userId) { 19 | JPAQuery query = queryFactory.selectFrom(ban) 20 | .where(ban.user.id.eq(userId)) 21 | .orderBy(ban.id.asc()) 22 | .limit(10); 23 | 24 | return query.fetch(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/CustomSliceExecutionUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util; 2 | 3 | import org.springframework.data.domain.Pageable; 4 | import org.springframework.data.domain.Slice; 5 | import org.springframework.data.domain.SliceImpl; 6 | 7 | import java.util.List; 8 | 9 | public class CustomSliceExecutionUtils { 10 | public static Slice getSlice(List content, Pageable pageable) { 11 | boolean hasNext = false; 12 | 13 | if (content.size() > pageable.getPageSize()) { // content.size가 최대일 경우: 항상 page size + 1 이고 다음 레코드가 있다. 14 | content.remove(pageable.getPageSize()); // limit걸 때 +1 했던 마지막 레코드를 삭제 15 | hasNext = true; 16 | } 17 | 18 | return new SliceImpl<>(content, pageable, hasNext); 19 | } 20 | 21 | public static int buildSliceLimit(int size) { // 언제나 요청한 size + 1개 조회 22 | return size + 1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/spot/model/SpotImage.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.spot.model; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.NotNull; 5 | import lombok.*; 6 | import org.hibernate.validator.constraints.URL; 7 | 8 | @Entity 9 | @Getter 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class SpotImage { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | @Column(name = "spot_image_id") 18 | private Long id; 19 | 20 | @ManyToOne(fetch = FetchType.LAZY) 21 | @NotNull 22 | @JoinColumn(name = "spot_id") 23 | private Spot spot; 24 | 25 | @NotNull 26 | @URL 27 | private String imageUrl; 28 | 29 | public static SpotImage of(Spot spot, String imageUrl) { 30 | return SpotImage.builder().spot(spot).imageUrl(imageUrl).build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/constant/ConstraintConstants.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.constant; 2 | 3 | public class ConstraintConstants { 4 | /** 5 | * AWS S3 6 | */ 7 | public static final int S3_FILE_TYPE_MAX_LENGTH = 25; 8 | 9 | /** 10 | * Category 11 | */ 12 | public static final int LOCATION_CATEGORY_NAME_MAX_LENGTH = 45; 13 | public static final int SPOT_CATEGORY_NAME_MAX_LENGTH = 45; 14 | 15 | /** 16 | * Congestion 17 | */ 18 | public static final int CONGESTION_LEVEL_MAX_VALUE = 3; 19 | 20 | /** 21 | * Spot & Comment 22 | */ 23 | public static final int SPOT_TITLE_MAX_LENGTH = 50; 24 | public static final int SPOT_ADDRESS_MAX_LENGTH = 500; 25 | public static final int SPOT_CONTENT_MAX_LENGTH = 1500; 26 | public static final int SPOT_IMAGE_MAX_SIZE = 5; 27 | 28 | public static final int COMMENT_CONTENT_MAX_LENGTH = 300; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/client/ClientIPAddressUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.client; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | import java.util.Objects; 6 | 7 | public class ClientIPAddressUtils { 8 | 9 | public static String getClientIP(HttpServletRequest request) { 10 | final String[] ipHeaderNames = { 11 | "X-Forwarded-For", 12 | "Proxy-Client-IP", 13 | "WL-Proxy-Client-IP", 14 | "HTTP_CLIENT_IP", 15 | "HTTP_X_FORWARDED_FOR" 16 | }; 17 | 18 | String ip = null; 19 | for (String headerName : ipHeaderNames) { 20 | ip = request.getHeader(headerName); 21 | if (Objects.nonNull(ip)) break; 22 | } 23 | 24 | if (Objects.isNull(ip) || ip.isEmpty()) { 25 | ip = request.getRemoteAddr(); 26 | } 27 | return ip; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/web/log/ReadableRequestBodyWrapperFilter.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.web.log; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.annotation.WebFilter; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.springframework.web.filter.OncePerRequestFilter; 10 | 11 | import java.io.IOException; 12 | 13 | @WebFilter(urlPatterns = "/*") // 대상: 전체 URI 14 | public class ReadableRequestBodyWrapperFilter extends OncePerRequestFilter { 15 | @Override 16 | protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 17 | filterChain.doFilter(new ReadableRequestBodyWrapper(request), response); // 필터 체인에 커스텀 Wrapper 추가 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/UserBlockUser.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import jakarta.persistence.*; 5 | import lombok.*; 6 | 7 | @Entity 8 | @Table(name = "user_block_user") 9 | @Getter 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class UserBlockUser extends BaseEntity { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | @Column(name = "user_block_user_id") 18 | private Long id; 19 | 20 | @ManyToOne(fetch = FetchType.LAZY) 21 | @JoinColumn(name = "blocker_user_id") 22 | private User blockerUser; 23 | 24 | @ManyToOne(fetch = FetchType.LAZY) 25 | @JoinColumn(name = "blocked_user_id") 26 | private User blockedUser; 27 | 28 | public void deleteBlock() { 29 | this.blockedUser = null; 30 | this.blockerUser = null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/bookmark/model/SpotBookmark.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.bookmark.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import bokjak.bokjakserver.domain.spot.model.Spot; 5 | import bokjak.bokjakserver.domain.user.model.User; 6 | import jakarta.persistence.*; 7 | import jakarta.validation.constraints.NotNull; 8 | import lombok.*; 9 | 10 | @Entity 11 | @Getter 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | public class SpotBookmark extends BaseEntity { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "spot_bookmark_id") 20 | private Long id; 21 | 22 | @ManyToOne(fetch = FetchType.LAZY) 23 | @NotNull 24 | @JoinColumn(name = "spot_id") 25 | private Spot spot; 26 | 27 | @ManyToOne(fetch = FetchType.LAZY) 28 | @NotNull 29 | @JoinColumn(name = "user_id") 30 | private User user; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/comment/repository/CommentRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.comment.repository; 2 | 3 | import bokjak.bokjakserver.domain.comment.model.Comment; 4 | import bokjak.bokjakserver.domain.user.model.User; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Slice; 7 | 8 | import java.util.List; 9 | 10 | public interface CommentRepositoryCustom { 11 | Slice findAllParentBySpotExceptBlockedAuthors(Pageable pageable, Long cursorId, Long spotId, Long userId); 12 | 13 | Long countAllParentBySpotExceptBlockedAuthors(Long spotId, Long userId); 14 | 15 | Slice findAllChildrenByParentExceptBlockedAuthors(Pageable pageable, Long cursorId, Long parentId, Long userId); 16 | 17 | Long countAllChildrenByParentExceptBlockedAuthors(Long parentId, Long userId); 18 | List findAllByparentCommentAndDistinctExceptParentAuthor(Long parentId, Long parentAuthorId); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/bookmark/model/LocationBookmark.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.bookmark.model; 2 | 3 | 4 | import bokjak.bokjakserver.common.model.BaseEntity; 5 | import bokjak.bokjakserver.domain.location.model.Location; 6 | import bokjak.bokjakserver.domain.user.model.User; 7 | import jakarta.persistence.*; 8 | import jakarta.validation.constraints.NotNull; 9 | import lombok.*; 10 | 11 | @Entity 12 | @Getter 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | public class LocationBookmark extends BaseEntity { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | @Column(name = "location_bookmark_id") 21 | private Long id; 22 | 23 | @ManyToOne(fetch = FetchType.LAZY) 24 | @NotNull 25 | @JoinColumn(name = "location_id") 26 | private Location location; 27 | 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | @NotNull 30 | @JoinColumn(name = "user_id") 31 | private User user; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/model/Notification.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import bokjak.bokjakserver.domain.user.model.User; 5 | import jakarta.persistence.*; 6 | import lombok.*; 7 | 8 | @Entity 9 | @Getter 10 | @AllArgsConstructor 11 | @Builder 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class Notification extends BaseEntity { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | @Column(name = "notification_id") 18 | private Long id; 19 | 20 | private String title; 21 | private String content; 22 | private boolean isRead; 23 | 24 | @Enumerated(EnumType.STRING) 25 | private NotificationType type; 26 | 27 | private Long redirectTargetId; 28 | 29 | @ManyToOne(fetch = FetchType.LAZY) 30 | @JoinColumn(name = "user_id") 31 | private User user; 32 | 33 | public void read() { 34 | this.isRead = true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/BokjakserverApplication.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.servlet.ServletComponentScan; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | @SpringBootApplication 13 | @EnableJpaAuditing 14 | @EnableWebSecurity 15 | @EnableScheduling 16 | @ServletComponentScan 17 | public class BokjakserverApplication { 18 | 19 | @Bean 20 | public RestTemplate restTemplate() { 21 | return new RestTemplate(); 22 | } 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(BokjakserverApplication.class, args); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/redis/RedisService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.redis; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.data.redis.core.ValueOperations; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class RedisService { 13 | 14 | private final RedisTemplate redisTemplate; 15 | 16 | public void setValues(String key, String data, Long duration, TimeUnit timeUnit) { 17 | ValueOperations values = redisTemplate.opsForValue(); 18 | values.set(key, data, duration, timeUnit); 19 | } 20 | 21 | public String getValues(String key) { 22 | ValueOperations values = redisTemplate.opsForValue(); 23 | return values.get(key); 24 | } 25 | 26 | public void deleteValues(String key) { 27 | redisTemplate.delete(key); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | servlet: 3 | multipart: 4 | max-file-size: 10MB 5 | max-request-size: 10MB 6 | 7 | data: 8 | web: 9 | pageable: 10 | default-page-size: 10 # @PageableDefault로 대체 11 | max-page-size: 500 12 | one-indexed-parameters: true 13 | 14 | messages: 15 | basename: errors 16 | 17 | logging.level: 18 | org.springframework.core.LocalVariableTableParameterNameDiscoverer: error # https://github.com/spring-projects/spring-framework/issues/29612 19 | 20 | server: 21 | error: 22 | include-stacktrace: on_param 23 | include-exception: true 24 | servlet: 25 | session: 26 | tracking-modes: cookie 27 | port: 8090 28 | tomcat: 29 | keep-alive-timeout: 300 30 | 31 | springdoc: 32 | swagger-ui: 33 | path: /api-docs.html 34 | tagsSorter: alpha 35 | operations-sorter: alpha 36 | api-docs: 37 | path: /api-docs 38 | show-actuator: true 39 | default-produces-media-type: application/json 40 | default-consumes-media-type: application/json 41 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/model/SleepingUser.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import jakarta.persistence.*; 5 | import lombok.*; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @Entity 10 | @Getter 11 | @Builder 12 | @AllArgsConstructor 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | public class SleepingUser extends BaseEntity { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "sleeping_user_id") 19 | private Long id; 20 | 21 | private Long originalId; 22 | @Enumerated(EnumType.STRING) 23 | private Role role; 24 | @Enumerated(EnumType.STRING) 25 | private UserStatus userStatus; 26 | private String email; 27 | private String socialEmail; 28 | private String password; 29 | @Enumerated(EnumType.STRING) 30 | private SocialType socialType; 31 | private String profileImageUrl; 32 | private String nickname; 33 | private LocalDateTime lastLoginDate; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/model/SpotCategory.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.model; 2 | 3 | import bokjak.bokjakserver.common.constant.ConstraintConstants; 4 | import bokjak.bokjakserver.domain.spot.model.Spot; 5 | import jakarta.persistence.*; 6 | import jakarta.validation.constraints.NotNull; 7 | import jakarta.validation.constraints.Size; 8 | import lombok.*; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @Entity 14 | @Getter 15 | @Builder 16 | @AllArgsConstructor 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | public class SpotCategory { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "spot_category_id") 23 | private Long id; 24 | 25 | @NotNull 26 | @Size(max = ConstraintConstants.SPOT_CATEGORY_NAME_MAX_LENGTH) 27 | private String name; 28 | 29 | @OneToMany(mappedBy = "spotCategory", cascade = CascadeType.ALL, orphanRemoval = true) 30 | @Builder.Default 31 | private List spotList = new ArrayList<>(); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/DummyLocationCategory.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum DummyLocationCategory { 9 | SUBWAY("지하철", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/subway.png"), 10 | AMUSE("놀이공원", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/amusement.png"), 11 | DEPART("백화점", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/department.png"), 12 | ETC("기타", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/etc.png"), 13 | PARK("공원", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/park.png"), 14 | MART("마트", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/mart.png"), 15 | VACATION("휴양지", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/vacation_spot.png"), 16 | MARKET("시장", "https://s3-buz.s3.ap-northeast-2.amazonaws.com/constant/market.png"); 17 | 18 | private final String name; 19 | private final String iconImageUrl; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/model/WeeklyCongestionStatistic.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.model; 2 | 3 | import bokjak.bokjakserver.domain.location.model.Location; 4 | import jakarta.persistence.*; 5 | import jakarta.validation.constraints.NotNull; 6 | import lombok.*; 7 | import org.springframework.data.annotation.CreatedDate; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | @Entity 12 | @Getter 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | public class WeeklyCongestionStatistic { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | @Column(name = "weekly_congestion_statistic_id") 21 | private Long id; 22 | 23 | @ManyToOne(fetch = FetchType.LAZY) 24 | @NotNull 25 | @JoinColumn(name = "location_id") 26 | private Location location; 27 | 28 | @NotNull 29 | private Float averageCongestionLevel; // 일주일 혼잡도 평균 30 | 31 | @CreatedDate 32 | private LocalDateTime createdAt; // 혼잡도 통계 더미 데이터의 created_at을 임의 설정하기 위함. deprecated. 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/security/PrincipalDetailService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.security; 2 | 3 | import bokjak.bokjakserver.domain.user.model.User; 4 | import bokjak.bokjakserver.domain.user.repository.UserRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | @RequiredArgsConstructor 13 | public class PrincipalDetailService implements UserDetailsService { 14 | 15 | private final UserRepository userRepository; 16 | 17 | @Override 18 | public UserDetails loadUserByUsername(String socialEmail) throws UsernameNotFoundException { 19 | User principal = userRepository.findBySocialEmail(socialEmail) 20 | .orElseThrow(()-> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다. :" + socialEmail)); 21 | return new PrincipalDetails(principal); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/service/BanService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.service; 2 | 3 | import bokjak.bokjakserver.domain.ban.repository.BanRepository; 4 | import bokjak.bokjakserver.domain.user.service.UserService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | 10 | import java.util.List; 11 | 12 | import static bokjak.bokjakserver.domain.ban.dto.BanDto.*; 13 | 14 | @Service 15 | @RequiredArgsConstructor 16 | @Transactional(readOnly = true) 17 | public class BanService { 18 | 19 | private final UserService userService; 20 | private final BanRepository banRepository; 21 | 22 | public BanListResponse getMyBanList(Long userId) { 23 | userService.getUser(userId); 24 | 25 | List banResponseList = banRepository.findLimitCountAndSortByRecent(userId).stream() 26 | .map(BanResponse::of) 27 | .toList(); 28 | 29 | return BanListResponse.of(banResponseList); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/enums/EnumQueryValue.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.enums; 2 | 3 | import bokjak.bokjakserver.domain.category.dto.CongestionHistoricalDateChoice; 4 | import lombok.Getter; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | import static bokjak.bokjakserver.domain.category.dto.CongestionHistoricalDateChoice.toDateTime; 11 | 12 | @Getter 13 | public class EnumQueryValue{ 14 | private final String key; 15 | private final V value; 16 | private final String query; 17 | 18 | public EnumQueryValue(EnumModel enumModel) { 19 | this.key = enumModel.getKey(); 20 | this.value = enumModel.getValue(); 21 | this.query = toDateTime(CongestionHistoricalDateChoice.toEnum(enumModel.getKey())); 22 | } 23 | 24 | public static List> toEnumQueryValues(Class> enumModel) { 25 | return Arrays 26 | .stream(enumModel.getEnumConstants()) 27 | .map(EnumQueryValue::new) 28 | .collect(Collectors.toList()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/model/Congestion.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.model; 2 | 3 | import bokjak.bokjakserver.common.constant.ConstraintConstants; 4 | import bokjak.bokjakserver.common.model.BaseEntity; 5 | import bokjak.bokjakserver.domain.location.model.Location; 6 | import jakarta.persistence.*; 7 | import jakarta.validation.constraints.Max; 8 | import jakarta.validation.constraints.NotNull; 9 | import lombok.*; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | @Entity 14 | @Getter 15 | @Builder 16 | @AllArgsConstructor 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | public class Congestion extends BaseEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "congestion_id") 23 | private Long id; 24 | 25 | @ManyToOne(fetch = FetchType.LAZY) 26 | @NotNull 27 | @JoinColumn(name = "location_id") 28 | private Location location; 29 | 30 | @NotNull 31 | @Max(ConstraintConstants.CONGESTION_LEVEL_MAX_VALUE) 32 | private int congestionLevel; //range : 1 ~ 3 (여유, 보통, 혼잡) 33 | 34 | private LocalDateTime observedAt; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/model/Report.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import bokjak.bokjakserver.domain.ban.model.Ban; 5 | import bokjak.bokjakserver.domain.user.model.User; 6 | import jakarta.persistence.*; 7 | import lombok.*; 8 | 9 | @Entity 10 | @Getter 11 | @Builder 12 | @AllArgsConstructor 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | public class Report extends BaseEntity { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "report_id") 19 | private Long id; 20 | 21 | @ManyToOne(fetch = FetchType.LAZY) 22 | @JoinColumn(name = "reporter_user_id") 23 | private User reporter; 24 | @ManyToOne(fetch = FetchType.LAZY) 25 | @JoinColumn(name = "reported_user_id") 26 | private User reportedUser; 27 | @OneToOne(fetch = FetchType.LAZY) 28 | @JoinColumn(name = "ban_id") 29 | private Ban banId; 30 | @Enumerated(EnumType.STRING) 31 | private ReportTarget reportTarget; 32 | private Long targetId; 33 | @Column(length = 300) 34 | private String content; 35 | private Boolean isChecked; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/model/LocationCategory.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.model; 2 | 3 | import bokjak.bokjakserver.common.constant.ConstraintConstants; 4 | import bokjak.bokjakserver.domain.location.model.Location; 5 | import jakarta.persistence.*; 6 | import jakarta.validation.constraints.NotNull; 7 | import jakarta.validation.constraints.Size; 8 | import lombok.*; 9 | import org.hibernate.validator.constraints.URL; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @Entity 15 | @Getter 16 | @Builder 17 | @AllArgsConstructor 18 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 19 | public class LocationCategory { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "location_category_id") 24 | private Long id; 25 | 26 | @NotNull 27 | @Size(max = ConstraintConstants.LOCATION_CATEGORY_NAME_MAX_LENGTH) 28 | private String name; 29 | 30 | @URL 31 | private String iconImageUrl; 32 | 33 | @OneToMany(mappedBy = "locationCategory", cascade = CascadeType.ALL, orphanRemoval = true) 34 | @Builder.Default 35 | private List locationList = new ArrayList<>(); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/location/repository/LocationRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location.repository; 2 | 3 | import bokjak.bokjakserver.domain.congestion.model.CongestionLevel; 4 | import bokjak.bokjakserver.domain.location.model.Location; 5 | import bokjak.bokjakserver.util.queries.SortOrder; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | public interface LocationRepositoryCustom { 14 | Page search( 15 | Pageable pageable, 16 | Long cursorId, 17 | String keyword, 18 | List categoryIds, 19 | SortOrder congestionLevelSortOrder, 20 | CongestionLevel cursorCongestionLevel 21 | ); 22 | 23 | Page getLocations(Pageable pageable,Long cursorId); 24 | 25 | Optional getLocation(Long locationId); 26 | Page getTopOfWeeklyAverageCongestion(Pageable pageable, LocalDateTime start, LocalDateTime end); 27 | 28 | Page getBookmarked(Pageable pageable, Long cursorId, Long userId); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/AwsS3/AwsS3Config.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.AwsS3; 2 | 3 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 4 | import com.amazonaws.auth.BasicAWSCredentials; 5 | import com.amazonaws.services.s3.AmazonS3Client; 6 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class AwsS3Config { 13 | @Value("${cloud.aws.credentials.access-key}") 14 | private String accessKey; 15 | @Value("${cloud.aws.credentials.secret-key}") 16 | private String secretKey; 17 | @Value("${cloud.aws.region.static}") 18 | private String region; 19 | 20 | @Bean 21 | public AmazonS3Client amazonS3Client() { 22 | BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); // AWS IAM keys 23 | return (AmazonS3Client) AmazonS3ClientBuilder.standard() 24 | .withRegion(region) 25 | .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) 26 | .build(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/model/Ban.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.model; 2 | 3 | 4 | import bokjak.bokjakserver.common.model.BaseEntity; 5 | import bokjak.bokjakserver.domain.report.model.Report; 6 | import bokjak.bokjakserver.domain.user.model.User; 7 | import jakarta.persistence.*; 8 | import lombok.*; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | @Entity 13 | @Getter 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | public class Ban extends BaseEntity { 18 | 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "ban_id") 23 | private Long id; 24 | 25 | @ManyToOne(fetch = FetchType.LAZY) 26 | @JoinColumn(name = "banned_user_id") 27 | private User user; 28 | @OneToOne(mappedBy = "banId") //n+1 잘 생각해봐야할 듯 29 | private Report report; 30 | @Column(length = 50) 31 | private String title; 32 | @Column(length = 300) 33 | private String content; 34 | private LocalDateTime banStartedAt; 35 | private LocalDateTime banEndedAt; 36 | private Boolean isBanned; 37 | 38 | public void changeIsBanned(boolean isBanned) { 39 | this.isBanned = isBanned; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/dto/UserDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.dto; 2 | 3 | import bokjak.bokjakserver.domain.user.model.User; 4 | import jakarta.validation.constraints.NotBlank; 5 | import lombok.Builder; 6 | 7 | public class UserDto { 8 | public record UserInfoResponse(String email, String nickname, String profileImageUrl) { 9 | 10 | @Builder 11 | public UserInfoResponse{} 12 | 13 | public static UserInfoResponse of(User user) { 14 | return UserInfoResponse.builder() 15 | .email(user.getEmail()) 16 | .nickname(user.getNickname()) 17 | .profileImageUrl(user.getProfileImageUrl()) 18 | .build(); 19 | } 20 | } 21 | 22 | public record UpdateUserInfoRequest( 23 | @NotBlank 24 | String email, 25 | @NotBlank 26 | String nickname, 27 | @NotBlank 28 | String profileImageUrl) {} 29 | 30 | 31 | public record HideRequest( 32 | @NotBlank 33 | Long blockUserId) {} 34 | 35 | public record HideResponse(boolean blockedResult) {} 36 | 37 | public record NicknameResponse(boolean isAvailableNickname) {} 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/spot/repository/SpotRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.spot.repository; 2 | 3 | import bokjak.bokjakserver.domain.spot.model.Spot; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public interface SpotRepositoryCustom { 11 | // 리스트 조회 12 | Page findAllByLocationAndCategoriesExceptBlockedAuthors( 13 | Long userId, 14 | Pageable pageable, 15 | Long cursorId, 16 | Long locationId, 17 | List categoryIds 18 | ); 19 | 20 | Page findAllByCategoriesExceptBlockedAuthors( 21 | Long userId, 22 | Pageable pageable, 23 | Long cursorId, 24 | List categoryIds 25 | ); 26 | 27 | // 상세 조회 28 | Optional findOne(Long spotId); 29 | 30 | // 내가 북마크한 스팟 조회 31 | Page findAllBookmarked(Pageable pageable, Long cursorId, Long userId); 32 | 33 | // 내가 작성한 스팟 조회 34 | Page findAllMy(Pageable pageable, Long cursorId, Long userId); 35 | 36 | // 내가 댓글 단 글 조회 37 | Page findAllCommentedByMeExceptBlockedAuthors(Pageable pageable, Long cursorId, Long userId); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/controller/CategoryController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.controller; 2 | 3 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 4 | import bokjak.bokjakserver.common.dto.ApiResponse; 5 | import bokjak.bokjakserver.domain.category.dto.CategoryDto.AllCategoryResponse; 6 | import bokjak.bokjakserver.domain.category.service.CategoryService; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import io.swagger.v3.oas.annotations.tags.Tag; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RestController 15 | @RequestMapping("/categories") 16 | @RequiredArgsConstructor 17 | @Tag(name = SwaggerConstants.TAG_CATEGORY, description = SwaggerConstants.TAG_CATEGORY_DESCRIPTION) 18 | public class CategoryController { 19 | private final CategoryService categoryService; 20 | 21 | @GetMapping 22 | @Operation(summary = SwaggerConstants.CATEGORY_GET_ALL, description = SwaggerConstants.CATEGORY_GET_ALL_DESCRIPTION) 23 | public ApiResponse getAllCategory() { 24 | return ApiResponse.success(categoryService.getAllCategory()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/swagger/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.swagger; 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 4 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; 5 | import io.swagger.v3.oas.annotations.info.Info; 6 | import io.swagger.v3.oas.annotations.security.SecurityScheme; 7 | import org.springdoc.core.models.GroupedOpenApi; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import static bokjak.bokjakserver.common.constant.SwaggerConstants.*; 12 | 13 | @OpenAPIDefinition(info = @Info(title = DEFINITION_TITLE, description = DEFINITION_DESCRIPTION, version = DEFINITION_VERSION)) 14 | @SecurityScheme( 15 | type = SecuritySchemeType.HTTP, 16 | scheme = SECURITY_SCHEME, 17 | name = SECURITY_SCHEME_NAME, 18 | bearerFormat = SECURITY_SCHEME_BEARER_FORMAT, 19 | description = SECURITY_SCHEME_DESCRIPTION 20 | ) 21 | @Configuration 22 | public class SwaggerConfig { 23 | 24 | @Bean 25 | public GroupedOpenApi mainAPI() { 26 | return GroupedOpenApi.builder() 27 | .group(DEFINITION_TITLE) 28 | .pathsToMatch(SWAGGER_APPOINTED_PATHS) // 스웨거로 명세화할 엔드포인트들의 URI 경로 29 | .build(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 복쟉복쟉 2 | 3 | ![복쟉배경](https://github.com/Akatsuki-USW/Buzzzzing-Server/assets/72124326/42828c59-0ebe-421b-bd3d-cda88760fbce) 4 | 5 | [📱 Google Play 앱 스토어 링크](https://play.google.com/store/apps/details?id=com.onewx2m.buzzzzing) 6 | 7 | 8 | 주요 장소별 인구 혼잡 현황을 쉽게 알 수 있다면 어떨까? 9 | 10 | 복쟉복쟉 서비스는 서울시, SK의 실시간 혼잡도 API를 기반으로 장소별로 현재 인구 혼잡도 현황과 미래 인구 혼잡도 예측치를 제공하고, 유저의 세부 장소 추천 및 커뮤니티 공간을 마련합니다. 11 | 12 | ### 💡 Features 13 | 14 | - 인증 서비스 : 카카오 소셜 로그인 15 | - 혼잡도 서비스 : 장소 조회 및 혼잡도 현황, 과거 데이터 기반 미래 혼잡도 예측 16 | - 커뮤니티 서비스 : 장소별 세부 장소 추천(스팟) 조회 및 댓글, 대댓글 17 | - 유저 서비스 : 휴면 유저 공지 메일 스케줄링, 유저 신고 및 차단, 앱 활동 푸시 알림(FCM) 18 | 19 | ## ⚒️ Tech Stack 20 | 21 | - Framework & Language : Spring Boot 3.0 & Java 17 22 | - Auth : Spring Security, OAuth2, JWT 23 | - ORM : JPA, Spring Data JPA, QueryDSL 24 | - DB : MySQL, Redis 25 | - Test : JUnit(+ AssertJ), Mockito 26 | - AWS : EC2, RDS, S3, CloudWatch, SNS 27 | - CI/CD : Github Actions, Docker 28 | - Firebase Cloud Messaging 29 | - Spring Scheduling. Spring Mail 30 | - Swagger 31 | 32 | ## 🔍 Architecture 33 | ### ERD 34 | ![buzzing_erd](https://github.com/Akatsuki-USW/Buzzzzing-Server/assets/72124326/2248f961-ade7-4a59-a716-be7422c8edf7) 35 | 36 | ### System Architecture 37 | ![Web App Reference Architecture(1)](https://github.com/Akatsuki-USW/Buzzzzing-Server/assets/72124326/104981fc-29e0-48ee-b0d8-edd4965350d8) 38 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dto/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dto; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.HashMap; 10 | 11 | 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | public class ApiResponse { 16 | private Integer statusCode; 17 | private T data; 18 | private String message; 19 | 20 | private ApiResponse(T data) { 21 | this.statusCode = null; 22 | this.data = data; 23 | } 24 | 25 | private ApiResponse(int status, T data, String message) { 26 | this.statusCode = status; 27 | this.data = data; 28 | this.message = message; 29 | } 30 | 31 | public static ApiResponse success(T data) { 32 | return new ApiResponse<>(data); 33 | } 34 | 35 | public static ApiResponse error(int errorCode, String message) { 36 | HashMap empty = new HashMap<>(); 37 | return new ApiResponse<>(errorCode, empty, message); 38 | } 39 | 40 | public static ApiResponse error(int errorCode, T data, String message) { 41 | return new ApiResponse<>(errorCode, data, message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/pr-test.yml: -------------------------------------------------------------------------------- 1 | # separate terms of service, privacy policy, and support 2 | # documentation. 3 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 4 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 5 | # gradlew 빌드까지만 테스트 6 | 7 | name: Gradlew build Test on PR 8 | 9 | ## trigger : PR이 생성됐을 때 10 | on: 11 | pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | ## JDK 버전 : 17 21 | - uses: actions/checkout@v3 22 | - name: Set up JDK 17 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: '17' 26 | distribution: 'adopt' 27 | 28 | ## 빌드 시간 단축용 gradle 캐싱 29 | - name: Gradle Caching 30 | uses: actions/cache@v3 31 | with: 32 | path: | 33 | ~/.gradle/caches 34 | ~/.gradle/wrapper 35 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 36 | restore-keys: | 37 | ${{ runner.os }}-gradle- 38 | 39 | ## gradlew 권한 부여 및 빌드 40 | - name: Grant execute permission for gradlew 41 | run: chmod +x gradlew 42 | - name: Build with Gradle 43 | run: ./gradlew bootJar 44 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/jwt/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.jwt; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.util.StringUtils; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | 14 | import java.io.IOException; 15 | 16 | @RequiredArgsConstructor 17 | @Slf4j 18 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 19 | 20 | private final JwtProvider jwtProvider; 21 | 22 | @Override 23 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 24 | String accessToken = jwtProvider.resolveToken(request); 25 | if (StringUtils.hasText(accessToken) && jwtProvider.validate(accessToken)) { 26 | Authentication authentication = jwtProvider.getAuthentication(accessToken); 27 | SecurityContextHolder.getContext().setAuthentication(authentication); //Authentication 객체에 대한 인증 허가 처리 28 | } 29 | filterChain.doFilter(request,response); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/model/DailyCongestionStatistic.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.model; 2 | 3 | import bokjak.bokjakserver.domain.location.model.Location; 4 | import com.vladmihalcea.hibernate.type.json.JsonType; 5 | import jakarta.persistence.*; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.*; 8 | import org.hibernate.annotations.Type; 9 | import org.springframework.data.annotation.CreatedDate; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Entity 17 | @Getter 18 | @Builder 19 | @AllArgsConstructor 20 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 21 | public class DailyCongestionStatistic { 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | @Column(name = "daily_congestion_statistic_id") 25 | private Long id; 26 | 27 | @ManyToOne(fetch = FetchType.LAZY) 28 | @NotNull 29 | @JoinColumn(name = "location_id") 30 | private Location location; 31 | 32 | @CreatedDate 33 | private LocalDateTime createdAt; // 혼잡도 통계 더미 데이터의 created_at을 임의 설정하기 위함. deprecated. 34 | 35 | // TODO refactor: Map -> 클래스 36 | @Type(JsonType.class) 37 | @Column(columnDefinition = "json", nullable = false) 38 | @Builder.Default 39 | private Map>> content = new HashMap<>(); // 시간별 혼잡도 (09시 ~ 24시) 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/redis/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.redis; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.connection.RedisConnectionFactory; 7 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; 10 | import org.springframework.data.redis.serializer.StringRedisSerializer; 11 | 12 | @EnableRedisRepositories 13 | @Configuration 14 | public class RedisConfig { 15 | @Value("${spring.data.redis.host}") 16 | private String host; 17 | @Value("${spring.data.redis.port}") 18 | private int port; 19 | 20 | @Bean 21 | public RedisConnectionFactory redisConnectionFactory() { 22 | return new LettuceConnectionFactory(host, port); 23 | } 24 | 25 | @Bean 26 | public RedisTemplate redisTemplate() { 27 | RedisTemplate redisTemplate = new RedisTemplate<>(); 28 | redisTemplate.setConnectionFactory(redisConnectionFactory()); 29 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 30 | redisTemplate.setValueSerializer(new StringRedisSerializer()); 31 | return redisTemplate; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/CustomDateUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util; 2 | 3 | import java.time.*; 4 | import java.time.format.DateTimeFormatter; 5 | import java.util.Calendar; 6 | 7 | public class CustomDateUtils { 8 | 9 | public static LocalDateTime makePastWeekDayDateTime(int dayOfWeek, LocalTime localTime) { 10 | return toSpecificLocalTime(makePastWeekDayDate(dayOfWeek), localTime); 11 | } 12 | 13 | public static Calendar makePastWeekDayDate(int dayOfWeek) { 14 | Calendar calendar = Calendar.getInstance(); 15 | // 한 주의 시작 요일 설정: 복쟉복쟉의 일주일 시작 요일은 월요일, 반면 Calendar의 디폴트 시작 요일은 일요일 16 | calendar.setFirstDayOfWeek(Calendar.MONDAY); 17 | // 일주일 전으로 설정: 과거 일주일간의 혼잡도 데이터를 보기 위함 18 | calendar.add(Calendar.DATE, -Calendar.DAY_OF_WEEK); 19 | // 요일에 따라 날짜 설정 20 | calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek); 21 | return calendar; 22 | } 23 | 24 | 25 | 26 | private static LocalDateTime toSpecificLocalTime(Calendar calendar, LocalTime localTime) { 27 | return ZonedDateTime.of( 28 | LocalDate.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId()), 29 | localTime, 30 | ZoneId.systemDefault() 31 | ).toLocalDateTime(); 32 | } 33 | 34 | public static String customDateFormat(LocalDateTime localDateTime, String pattern) { 35 | return localDateTime.format(DateTimeFormatter.ofPattern(pattern)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/dto/BanDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.dto; 2 | 3 | import bokjak.bokjakserver.common.constant.GlobalConstants; 4 | import bokjak.bokjakserver.domain.ban.model.Ban; 5 | import lombok.Builder; 6 | 7 | import java.util.List; 8 | 9 | import static bokjak.bokjakserver.util.CustomDateUtils.customDateFormat; 10 | 11 | public class BanDto { 12 | 13 | public record BanResponse( 14 | String title, 15 | String content, 16 | String banStartAt, 17 | String banEndedAt 18 | ) { 19 | @Builder 20 | public BanResponse { 21 | } 22 | 23 | public static BanResponse of(Ban ban) { 24 | return BanResponse.builder() 25 | .title(ban.getTitle()) 26 | .content(ban.getContent()) 27 | .banStartAt(customDateFormat(ban.getBanStartedAt(), GlobalConstants.DATE_FORMAT_YYYY_MM_DD)) 28 | .banEndedAt(customDateFormat(ban.getBanEndedAt(), GlobalConstants.DATE_FORMAT_YYYY_MM_DD)) 29 | .build(); 30 | } 31 | } 32 | 33 | public record BanListResponse( 34 | List banList 35 | ) { 36 | @Builder 37 | public BanListResponse { 38 | } 39 | 40 | public static BanListResponse of(List banList) { 41 | return BanListResponse.builder() 42 | .banList(banList) 43 | .build(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/repository/NotificationRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.repository; 2 | 3 | import bokjak.bokjakserver.domain.notification.model.Notification; 4 | import com.querydsl.jpa.impl.JPAQuery; 5 | import com.querydsl.jpa.impl.JPAQueryFactory; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | import static bokjak.bokjakserver.domain.notification.model.QNotification.notification; 12 | import static bokjak.bokjakserver.domain.user.model.QUser.user; 13 | 14 | @RequiredArgsConstructor 15 | public class NotificationRepositoryImpl implements NotificationRepositoryCustom{ 16 | 17 | private final JPAQueryFactory queryFactory; 18 | 19 | @Override 20 | public List findLimitCountAndSortByRecent(Long userId) { 21 | JPAQuery query = queryFactory.selectFrom(notification) 22 | .join(notification.user, user) 23 | .orderBy(notification.id.desc()) 24 | .where(user.id.eq(userId)) 25 | .limit(30); 26 | return query.fetch(); 27 | } 28 | 29 | @Override 30 | public Optional findByNotificationIdAndUserId(Long notificationId, Long userId) { 31 | JPAQuery query = queryFactory.selectFrom(notification) 32 | .where(notification.id.eq(notificationId) 33 | .and(user.id.eq(userId))); 34 | return Optional.ofNullable(query.fetchOne()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/service/SocialService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.core.ParameterizedTypeReference; 5 | import org.springframework.http.*; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.util.MultiValueMap; 8 | import org.springframework.web.client.RestTemplate; 9 | 10 | import java.util.Collections; 11 | import java.util.Map; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class SocialService { 16 | 17 | private final RestTemplate restTemplate; 18 | 19 | protected ResponseEntity> validOauthAccessToken(String oauthAccessToken, String socialUrl, HttpMethod httpMethod) { 20 | HttpHeaders headers = setHeaders(oauthAccessToken); 21 | HttpEntity> request = new HttpEntity<>(headers); 22 | ParameterizedTypeReference> RESPONSE_TYPE = 23 | new ParameterizedTypeReference>() {}; 24 | return restTemplate.exchange(socialUrl,httpMethod,request,RESPONSE_TYPE); 25 | } 26 | 27 | private HttpHeaders setHeaders(String oauthAccessToken) { 28 | HttpHeaders headers = new HttpHeaders(); 29 | headers.set("Authorization", "Bearer " + oauthAccessToken); 30 | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 31 | headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); 32 | return headers; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/security/PrincipalDetails.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.security; 2 | 3 | import bokjak.bokjakserver.domain.user.model.User; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | 12 | @Getter 13 | @AllArgsConstructor 14 | public class PrincipalDetails implements UserDetails { 15 | 16 | private User user; 17 | @Override 18 | public Collection getAuthorities() { 19 | Collection authorities = new ArrayList<>(); 20 | authorities.add(()->user.getRole().toString()); 21 | return authorities; 22 | } 23 | 24 | @Override 25 | public String getPassword() { 26 | return user.getPassword(); 27 | } 28 | 29 | @Override 30 | public String getUsername() { 31 | return user.getSocialEmail(); 32 | } 33 | 34 | public Long getUserId() { 35 | return user.getId(); 36 | } 37 | 38 | @Override 39 | public boolean isAccountNonExpired() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean isAccountNonLocked() { 45 | return true; 46 | } 47 | 48 | @Override 49 | public boolean isCredentialsNonExpired() { 50 | return true; 51 | } 52 | 53 | @Override 54 | public boolean isEnabled() { 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/ban/controller/BanController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.ban.controller; 2 | 3 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 4 | import bokjak.bokjakserver.common.dto.ApiResponse; 5 | import bokjak.bokjakserver.config.security.PrincipalDetails; 6 | import bokjak.bokjakserver.domain.ban.dto.BanDto.BanListResponse; 7 | import bokjak.bokjakserver.domain.ban.service.BanService; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 10 | import io.swagger.v3.oas.annotations.tags.Tag; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import static bokjak.bokjakserver.common.constant.SwaggerConstants.*; 18 | import static bokjak.bokjakserver.common.dto.ApiResponse.success; 19 | 20 | @RestController 21 | @RequestMapping("/bans") 22 | @RequiredArgsConstructor 23 | @Tag(name = TAG_BAN, description = TAG_BAN_DESCRIPTION) 24 | public class BanController { 25 | 26 | private final BanService banService; 27 | 28 | @GetMapping("/me") 29 | @Operation(summary = BAN_ME) 30 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 31 | public ApiResponse getMyBan(@AuthenticationPrincipal PrincipalDetails principalDetails) { 32 | return success(banService.getMyBanList(principalDetails.getUserId())); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/controller/ReportController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.controller; 2 | 3 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 4 | import bokjak.bokjakserver.common.dto.ApiResponse; 5 | import bokjak.bokjakserver.config.security.PrincipalDetails; 6 | import bokjak.bokjakserver.domain.report.service.ReportService; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import jakarta.validation.Valid; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import static bokjak.bokjakserver.common.constant.SwaggerConstants.*; 16 | import static bokjak.bokjakserver.common.dto.ApiResponse.success; 17 | import static bokjak.bokjakserver.domain.report.dto.ReportDto.*; 18 | 19 | @RestController 20 | @RequiredArgsConstructor 21 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 22 | @Tag(name = TAG_REPORT, description = TAG_REPORT_DESCRIPTION) 23 | public class ReportController { 24 | 25 | private final ReportService reportService; 26 | 27 | @Operation(summary = REPORT_CREATE, description = REPORT_CREATE_DESCRIPTION) 28 | @PostMapping("/reports") 29 | public ApiResponse createReport(@RequestBody @Valid ReportRequest reportRequest, 30 | @AuthenticationPrincipal PrincipalDetails principalDetails) { 31 | return success(reportService.createReport(principalDetails.getUserId(), reportRequest)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/image/ImageFilePathUtils.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util.image; 2 | 3 | import bokjak.bokjakserver.domain.image.S3SaveDir; 4 | import java.net.URLDecoder; 5 | import java.nio.charset.StandardCharsets; 6 | import java.text.MessageFormat; 7 | import java.util.Objects; 8 | import java.util.UUID; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | public class ImageFilePathUtils { 12 | 13 | private static final String FILE_EXTENSION_SEPARATOR = "."; 14 | private static final String URL_SEPARATOR = "/"; 15 | private static final String S3_OBJECT_NAME_PATTERN = "{0}_{1}.{2}"; 16 | 17 | 18 | public static String buildRootPath(String bucket, S3SaveDir saveDir) { 19 | return bucket + saveDir.path; 20 | } 21 | 22 | public static String buildImageFilePath(MultipartFile multipartFile, String owner) { // 실제 파일 이름으로부터 key 생성 23 | String fileExtension = getFileExtension(Objects.requireNonNull(multipartFile.getOriginalFilename())); 24 | return MessageFormat.format(S3_OBJECT_NAME_PATTERN, owner, UUID.randomUUID(), fileExtension); 25 | } 26 | 27 | public static String buildImageFilePath(String url) { // URL로부터 key 생성 28 | String[] parsedUrl = url.split(URL_SEPARATOR); 29 | String fileName = parsedUrl[parsedUrl.length - 1]; 30 | return URLDecoder.decode(fileName, StandardCharsets.UTF_8); 31 | } 32 | 33 | 34 | private static String getFileExtension(String originalFileName) {// 파일 확장자 추출 35 | int fileExtensionIndex = originalFileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); 36 | return originalFileName.substring(fileExtensionIndex + 1); 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/jwt/JwtAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.jwt; 2 | 3 | import bokjak.bokjakserver.common.dto.ApiResponse; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.security.core.AuthenticationException; 14 | import org.springframework.security.web.AuthenticationEntryPoint; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.io.IOException; 18 | 19 | @Component 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint{ 23 | 24 | private final ObjectMapper objectMapper; 25 | 26 | 27 | //로그인 안 한 상태로 접근할 경우 401 28 | @Override 29 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { 30 | log.warn("Unauthorized access = {}", e.getMessage()); 31 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 32 | response.setCharacterEncoding("utf-8"); 33 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 34 | String body = objectMapper.writeValueAsString( 35 | ApiResponse.error(StatusCode.FILTER_ACCESS_DENIED.getStatusCode(), StatusCode.FILTER_ACCESS_DENIED.getMessage()) 36 | ); 37 | response.getWriter().write(body); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/jwt/JwtAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.jwt; 2 | 3 | import bokjak.bokjakserver.common.dto.ApiResponse; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.security.access.AccessDeniedException; 14 | import org.springframework.security.web.access.AccessDeniedHandler; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.io.IOException; 18 | 19 | @Component 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | public class JwtAccessDeniedHandler implements AccessDeniedHandler { 23 | 24 | private final ObjectMapper objectMapper; 25 | 26 | //인증은 되었으나, 해당 자원 접근에 권한(Role)이 없을 경우 403 27 | @Override 28 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { 29 | log.warn("Forbidden access = {}", e.getMessage()); 30 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 31 | response.setCharacterEncoding("utf-8"); 32 | response.setStatus(HttpStatus.FORBIDDEN.value()); 33 | String body = objectMapper.writeValueAsString( 34 | ApiResponse.error(StatusCode.FILTER_ROLE_FORBIDDEN.getStatusCode(), StatusCode.FILTER_ROLE_FORBIDDEN.getMessage()) 35 | ); 36 | response.getWriter().write(body); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/templates/convertSleepingMail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 휴면계정으로 전환 안내 6 | 7 | 8 | 13 |
16 |

휴면계정으로 전환 안내

17 |

안녕하세요, 복작복작입니다.

18 |

귀하의 계정이 휴면 상태로 전환될 예정입니다. 휴면 상태로 전환되면 일정 기간 동안 계정에 로그인하지 않으면 일부 기능이 제한될 수 있습니다.

19 |

휴면계정 변환예정 닉네임 : 닉네임

20 |

계정을 활성 상태로 유지하려면 다음 단계를 따라 주십시오:

21 |
    22 |
  1. 아래의 "활성화" 버튼을 클릭하여 계정에 로그인합니다.
  2. 23 |
  3. 계정에 로그인한 후, 활동을 통해 계정을 활성화시킵니다.
  4. 24 |
25 |

활성화 버튼을 클릭하여 계정을 활성화하세요:

26 |

활성화

32 |

계정을 활성 상태로 유지하기 위해 로그인해야 하는 일정 기간은 수신일로부터 한달입니다.

33 |

계정을 계속 사용하지 않을 경우 휴면 상태로 유지될 수 있으며, 휴면 계정에 대한 추가 조치를 취할 수 있습니다.

34 |

추가 도움이 필요한 경우, 문의하실 사항이 있으면 저희에게 연락해 주세요.

35 |

감사합니다.
복작복작 팀 드림

36 |
37 | 38 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/constant/GlobalConstants.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.constant; 2 | 3 | public class GlobalConstants { 4 | /** 5 | * Common 6 | */ 7 | public static final String[] AUTH_WHITELIST = { 8 | "/", 9 | "/csrf", 10 | "/error", 11 | 12 | "/auth/login/admin", 13 | "/auth/reissue", 14 | "/auth/login", 15 | "/auth/signup", 16 | "/users/check/nickname/**", 17 | "/hello/**", 18 | "/categories/**", 19 | "/files", 20 | 21 | "/api-docs/**", 22 | "/v1/api-docs", 23 | "/v2/api-docs", 24 | "/docs/**", 25 | "/favicon.ico", 26 | "/configuration/ui", 27 | "/swagger-resources/**", 28 | "/configuration/security", 29 | "/swagger-ui.html", 30 | "/swagger-ui/#", 31 | "/webjars/**", 32 | "/swagger/**", 33 | "/swagger-ui/**" 34 | }; 35 | 36 | /** 37 | * Location 38 | */ 39 | public static final String DATE_FORMAT = "yyyy-MM-dd"; 40 | public static final String DATE_FORMAT_YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; 41 | public static final String DATE_FORMAT_YYYY_MM_DD_HH_MM = "yyyy-MM-dd HH:mm"; 42 | public static final String DATE_FORMAT_YYYY_MM_DD = "yyyy-MM-dd"; 43 | public static final int TOP_LOCATIONS_SIZE = 5; 44 | public static final String CONTENT_DATA = "statistics"; 45 | public static final String CONTENT_HOUR = "time"; 46 | public static final String CONTENT_CONGESTION_LEVEL = "congestionLevel"; 47 | public static final int CONGESTION_STATISTIC_START_TIME = 9; 48 | public static final int CONGESTION_PREDICTION_WEEK = 1; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/dto/ReportDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.dto; 2 | 3 | import bokjak.bokjakserver.domain.report.model.Report; 4 | import bokjak.bokjakserver.domain.report.model.ReportTarget; 5 | import bokjak.bokjakserver.domain.user.model.User; 6 | import io.swagger.v3.oas.annotations.media.Schema; 7 | import jakarta.validation.constraints.NotEmpty; 8 | import jakarta.validation.constraints.Positive; 9 | import lombok.Builder; 10 | 11 | public class ReportDto { 12 | 13 | public record ReportRequest( 14 | @Schema(example = "SPOT") 15 | @NotEmpty 16 | String reportTarget, 17 | @Schema(example = "333") 18 | @Positive 19 | Long reportTargetId, 20 | @Schema(example = "12") 21 | @Positive 22 | Long reportedUserId, 23 | @Schema(example = "부적절한 언어를 사용해요") 24 | @NotEmpty 25 | String content) { 26 | 27 | @Builder 28 | public ReportRequest{} 29 | 30 | public Report toEntity(User reporterUser, User reportedUser) { 31 | return Report.builder() 32 | .reporter(reporterUser) 33 | .reportedUser(reportedUser) 34 | .reportTarget(ReportTarget.toEnum(reportTarget)) 35 | .targetId(reportTargetId) 36 | .content(content) 37 | .isChecked(false) 38 | .build(); 39 | } 40 | } 41 | 42 | public record ReportIdResponse(Long reportId) { 43 | 44 | @Builder 45 | public ReportIdResponse{} 46 | 47 | public static ReportIdResponse of(Report report) { 48 | return ReportIdResponse.builder() 49 | .reportId(report.getId()).build(); 50 | } 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/CategoryDummy.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import bokjak.bokjakserver.domain.category.repository.LocationCategoryRepository; 4 | import bokjak.bokjakserver.domain.category.service.CategoryService; 5 | import jakarta.annotation.PostConstruct; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | 10 | import static bokjak.bokjakserver.common.dummy.DummyLocationCategory.*; 11 | import static bokjak.bokjakserver.common.dummy.DummySpotCategory.*; 12 | 13 | @Slf4j 14 | @RequiredArgsConstructor 15 | @Component("categoryDummy") 16 | @BuzzingDummy 17 | public class CategoryDummy { 18 | private final CategoryService categoryService; 19 | private final LocationCategoryRepository locationCategoryRepository; 20 | 21 | @PostConstruct 22 | public void init() { 23 | if (locationCategoryRepository.count() > 0) { 24 | log.info("[categoryDummy] 카테고리가 이미 존재"); 25 | return; 26 | } 27 | 28 | // 로케이션 카테고리 29 | categoryService.createDummyLocationCategory(SUBWAY); 30 | categoryService.createDummyLocationCategory(AMUSE); 31 | categoryService.createDummyLocationCategory(DEPART); 32 | categoryService.createDummyLocationCategory(ETC); 33 | categoryService.createDummyLocationCategory(PARK); 34 | categoryService.createDummyLocationCategory(MART); 35 | categoryService.createDummyLocationCategory(VACATION); 36 | categoryService.createDummyLocationCategory(MARKET); 37 | 38 | // 스팟 카테고리 39 | categoryService.createDummySpotCategory(CAFE); 40 | categoryService.createDummySpotCategory(PLAY); 41 | categoryService.createDummySpotCategory(RESTAURANT); 42 | 43 | log.info("[categoryDummy] 카테고리 더미 생성 완료"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/image/dto/ImageDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.image.dto; 2 | 3 | import bokjak.bokjakserver.common.constant.ConstraintConstants; 4 | import bokjak.bokjakserver.common.constant.MessageConstants; 5 | import jakarta.validation.constraints.*; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Objects; 11 | 12 | public class ImageDto { 13 | public record UploadFileRequest( 14 | @NotBlank @Size(max = ConstraintConstants.S3_FILE_TYPE_MAX_LENGTH) 15 | String type, 16 | @NotEmpty 17 | List files 18 | ) { 19 | } 20 | 21 | public record UpdateFileRequest( 22 | @NotBlank @Size(max = ConstraintConstants.S3_FILE_TYPE_MAX_LENGTH) 23 | String type, 24 | // 생성할 파일, 삭제할 파일은 NULL일 수 있음 25 | List urlsToDelete, 26 | List newFiles 27 | ) { 28 | public UpdateFileRequest{ // NULL일 경우 빈 리스트로 설정 29 | if (Objects.isNull(urlsToDelete)) { 30 | urlsToDelete = new ArrayList<>(); 31 | } 32 | if (Objects.isNull(newFiles)) { 33 | newFiles = new ArrayList<>(); 34 | } 35 | } 36 | } 37 | 38 | 39 | 40 | public record FileDto( 41 | String filename, 42 | String fileUrl 43 | ) { 44 | } 45 | 46 | public record UploadFilesResponse( 47 | List files 48 | ) { 49 | } 50 | 51 | public record UpdateFilesResponse( 52 | List files 53 | ) { 54 | } 55 | 56 | public record DeleteFileResponse( 57 | String message 58 | ) { 59 | public static DeleteFileResponse success() { 60 | return new DeleteFileResponse(MessageConstants.S3_FILE_DELETE_SUCCESS); 61 | } 62 | } 63 | } 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/model/CongestionLevel.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.model; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.congestion.exception.CongestionException; 5 | import bokjak.bokjakserver.util.enums.EnumModel; 6 | import bokjak.bokjakserver.util.queries.SortOrder; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @Getter 11 | @RequiredArgsConstructor 12 | public enum CongestionLevel implements EnumModel { 13 | RELAX(1), NORMAL(2), BUZZING(3); 14 | 15 | private final int value; 16 | 17 | @Override 18 | public String getKey() { 19 | return name(); 20 | } 21 | 22 | @Override 23 | public Integer getValue() { 24 | return value; 25 | } 26 | 27 | public static CongestionLevel toEnum(String stringParam) { 28 | if (stringParam == null) return null; 29 | else return switch (stringParam.toUpperCase()) { 30 | case "RELAX" -> RELAX; 31 | case "NORMAL" -> NORMAL; 32 | case "BUZZING" -> BUZZING; 33 | 34 | default -> throw new CongestionException(StatusCode.CHOICE_NOT_EXIST); 35 | }; 36 | } 37 | 38 | public static CongestionLevel toEnum(Integer intParam) { 39 | if (intParam == null) return null; 40 | 41 | return switch (intParam) { 42 | case 1 -> RELAX; 43 | case 2 -> NORMAL; 44 | case 3 -> BUZZING; 45 | 46 | default -> throw new CongestionException(StatusCode.CHOICE_NOT_EXIST); 47 | }; 48 | } 49 | 50 | // ASC DESC로 변환 51 | public static SortOrder toSortOrder(CongestionLevel congestionLevel) { 52 | if (congestionLevel == null) return null; 53 | 54 | return switch (congestionLevel) { 55 | case RELAX -> SortOrder.ASC; 56 | case BUZZING -> SortOrder.DESC; 57 | case NORMAL -> null; 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/congestion/dto/CongestionDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.congestion.dto; 2 | 3 | import bokjak.bokjakserver.common.constant.GlobalConstants; 4 | import bokjak.bokjakserver.domain.congestion.model.DailyCongestionStatistic; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public class CongestionDto { 13 | @Builder 14 | public record DailyCongestionStatisticResponse( 15 | Long id, 16 | List content 17 | ) { 18 | public static DailyCongestionStatisticResponse of( 19 | DailyCongestionStatistic dailyCongestionStatistic 20 | ) { 21 | List records = dailyCongestionStatistic.getContent().get(GlobalConstants.CONTENT_DATA).stream() 22 | .filter(it->it.get(GlobalConstants.CONTENT_HOUR) >= GlobalConstants.CONGESTION_STATISTIC_START_TIME) // 9시 이후 데이터만 응답 23 | .map(DailyCongestionRecord::of) 24 | .toList(); 25 | 26 | return DailyCongestionStatisticResponse.builder() 27 | .id(dailyCongestionStatistic.getId()) 28 | .content(records) 29 | .build(); 30 | } 31 | } 32 | 33 | @Builder 34 | public record DailyCongestionRecord( 35 | int time, 36 | int congestionLevel 37 | ) { 38 | public static DailyCongestionRecord of( 39 | Map raw 40 | ) { 41 | return DailyCongestionRecord.builder() 42 | .time(raw.get(GlobalConstants.CONTENT_HOUR)) 43 | .congestionLevel(raw.get(GlobalConstants.CONTENT_CONGESTION_LEVEL)) 44 | .build(); 45 | } 46 | } 47 | 48 | @Data 49 | @NoArgsConstructor 50 | public static class CongestionPrediction { 51 | private Integer mayRelaxAt; 52 | private Integer mayRelaxUntil; 53 | private Integer mayBuzzAt; 54 | private Integer mayBuzzUntil; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/comment/model/Comment.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.comment.model; 2 | 3 | import bokjak.bokjakserver.common.constant.ConstraintConstants; 4 | import bokjak.bokjakserver.common.model.BaseEntity; 5 | import bokjak.bokjakserver.domain.spot.model.Spot; 6 | import bokjak.bokjakserver.domain.user.model.User; 7 | import jakarta.persistence.*; 8 | import jakarta.validation.constraints.NotNull; 9 | import jakarta.validation.constraints.Size; 10 | import lombok.*; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | @Entity 16 | @Getter 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | public class Comment extends BaseEntity { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | @Column(name = "comment_id") 25 | private Long id; 26 | 27 | @ManyToOne(fetch = FetchType.LAZY) 28 | @NotNull 29 | @JoinColumn(name = "user_id") 30 | private User user; 31 | 32 | @ManyToOne(fetch = FetchType.LAZY) 33 | @NotNull 34 | @JoinColumn(name = "spot_id") 35 | private Spot spot; 36 | 37 | @NotNull 38 | @Size(max = ConstraintConstants.COMMENT_CONTENT_MAX_LENGTH) 39 | private String content; 40 | 41 | private boolean presence; 42 | 43 | @ManyToOne(fetch = FetchType.LAZY) 44 | @JoinColumn(name = "parent_comment_id") 45 | private Comment parent; // 댓글, 대댓글 2가지로 나뉨. 무한 대댓글 아님 46 | 47 | @Builder.Default 48 | @OneToMany(mappedBy = "parent") 49 | List childList = new ArrayList<>(); 50 | 51 | // PrePersist : presence의 디폴트 값을 true로 52 | @PrePersist 53 | public void setDefaultPresence() { 54 | this.presence = true; 55 | } 56 | 57 | /* 편의 메서드 */ 58 | public void update(String content) { 59 | this.content = content; 60 | } 61 | 62 | public void addChild(Comment child) { 63 | child.parent = this; 64 | this.childList.add(child); 65 | } 66 | 67 | public void logicallyDelete() { // 논리적 삭제(소프트 딜리트와 유사) 68 | this.presence = false; 69 | } 70 | 71 | public boolean isParent() { 72 | return this.parent == null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/test/TestController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.test; 2 | 3 | import bokjak.bokjakserver.common.dto.ApiResponse; 4 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto; 5 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto.NotifyParams; 6 | import bokjak.bokjakserver.domain.notification.model.NotificationType; 7 | import bokjak.bokjakserver.domain.notification.service.NotificationService; 8 | import bokjak.bokjakserver.domain.user.model.User; 9 | import bokjak.bokjakserver.domain.user.service.SleepingUserService; 10 | import bokjak.bokjakserver.domain.user.service.UserService; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | @RestController 18 | @RequiredArgsConstructor 19 | @RequestMapping("/test") 20 | public class TestController { 21 | 22 | private final UserService userService; 23 | private final NotificationService notificationService; 24 | 25 | private final SleepingUserService sleepingUserService; 26 | 27 | @PostMapping("/push/self") 28 | public ApiResponse testCreateNotificationMySelf() { 29 | User currentUser = userService.getCurrentUser(); 30 | NotifyParams params = NotifyParams.builder() 31 | .receiver(currentUser) 32 | .type(NotificationType.TEST_USER_ITSELF) 33 | .redirectTargetId(currentUser.getId()) 34 | .title("test") 35 | .content("자기 자신의 아이디를 리턴") 36 | .build(); 37 | notificationService.pushMessage(params); 38 | return ApiResponse.success(notificationService.getMyNotifications(currentUser.getId())); 39 | } 40 | 41 | @GetMapping("/send/email") 42 | public String testSendEmailMySelf() { 43 | User currentUser = userService.getCurrentUser(); 44 | sleepingUserService.sendMail(currentUser.getEmail()); 45 | return "good"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/CommentDummy.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import bokjak.bokjakserver.domain.comment.model.Comment; 4 | import bokjak.bokjakserver.domain.comment.repository.CommentRepository; 5 | import bokjak.bokjakserver.domain.spot.model.Spot; 6 | import bokjak.bokjakserver.domain.spot.repository.SpotRepository; 7 | import bokjak.bokjakserver.domain.user.model.User; 8 | import bokjak.bokjakserver.domain.user.repository.UserRepository; 9 | import jakarta.annotation.PostConstruct; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.context.annotation.DependsOn; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.util.List; 16 | 17 | @Slf4j 18 | @Component("commentDummy") 19 | @DependsOn("spotDummy") 20 | @RequiredArgsConstructor 21 | @BuzzingDummy 22 | public class CommentDummy { 23 | private final UserRepository userRepository; 24 | private final SpotRepository spotRepository; 25 | private final CommentRepository commentRepository; 26 | 27 | @PostConstruct 28 | public void init() { 29 | if (commentRepository.count() > 0) { 30 | log.info("[commentDummy] 댓글 데이터가 이미 존재"); 31 | } else { 32 | createComments(); 33 | log.info("[commentDummy] 댓글 더미 생성 완료"); 34 | } 35 | } 36 | 37 | private void createComments() { 38 | for (Spot spot : spotRepository.findAll()) {// 모든 스팟에 대해 39 | List allUser = userRepository.findAll(); 40 | 41 | for (int i = 0; i < 10; i++) {// 유저 10명 42 | Comment parent = commentRepository.save(Comment.builder() 43 | .user(allUser.get(i)) 44 | .spot(spot) 45 | .content("댓글" + spot.getId() * (i + 1)) 46 | .build()); 47 | 48 | for (int j = 0; j < 5; j++) { 49 | commentRepository.save(Comment.builder() 50 | .user(allUser.get(j)) 51 | .spot(spot) 52 | .parent(parent) 53 | .content(parent.getId() + "의 대댓글") 54 | .build()); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/service/CategoryService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.service; 2 | 3 | import bokjak.bokjakserver.common.dummy.DummyLocationCategory; 4 | import bokjak.bokjakserver.common.dummy.DummySpotCategory; 5 | import bokjak.bokjakserver.domain.category.dto.CategoryDto.*; 6 | import bokjak.bokjakserver.domain.category.model.LocationCategory; 7 | import bokjak.bokjakserver.domain.category.model.SpotCategory; 8 | import bokjak.bokjakserver.domain.category.repository.LocationCategoryRepository; 9 | import bokjak.bokjakserver.domain.category.repository.SpotCategoryRepository; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import java.util.List; 15 | 16 | @Service 17 | @Transactional(readOnly = true) 18 | @RequiredArgsConstructor 19 | public class CategoryService { 20 | private final LocationCategoryRepository locationCategoryRepository; 21 | private final SpotCategoryRepository spotCategoryRepository; 22 | 23 | public AllCategoryResponse getAllCategory(){ 24 | List locationCategoryResponses = locationCategoryRepository.findAll() 25 | .stream().map(LocationCategoryResponse::of) 26 | .toList(); 27 | List spotCategoryResponses = spotCategoryRepository.findAll() 28 | .stream().map(SpotCategoryResponse::of) 29 | .toList(); 30 | return AllCategoryResponse.of(locationCategoryResponses, spotCategoryResponses); 31 | } 32 | 33 | // Dummy 생성 및 테스트용 메서드 34 | @Transactional 35 | public void createDummyLocationCategory(DummyLocationCategory dummyLocationCategory) { 36 | LocationCategory category = LocationCategory.builder() 37 | .name(dummyLocationCategory.getName()) 38 | .iconImageUrl(dummyLocationCategory.getIconImageUrl()) 39 | .build(); 40 | locationCategoryRepository.save(category); 41 | } 42 | 43 | @Transactional 44 | public void createDummySpotCategory(DummySpotCategory dummySpotCategory) { 45 | SpotCategory category = SpotCategory.builder() 46 | .name(dummySpotCategory.getName()) 47 | .build(); 48 | spotCategoryRepository.save(category); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dto/PageResponse.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageImpl; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.data.domain.Slice; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @Builder 15 | @Getter 16 | @Slf4j 17 | public class PageResponse { 18 | Long totalElements; 19 | boolean last; 20 | List content; 21 | 22 | public static PageResponse of(Page page) { 23 | return PageResponse.builder() 24 | .totalElements(page.getTotalElements()) 25 | .last(page.isLast()) 26 | .content(page.getContent()) 27 | .build(); 28 | } 29 | 30 | public static PageResponse of(Slice slice) { 31 | return PageResponse.builder() 32 | .last(slice.isLast()) 33 | .content(slice.getContent()) 34 | .build(); 35 | } 36 | 37 | public static PageResponse of(Slice slice, Long totalElements) { 38 | return PageResponse.builder() 39 | .totalElements(totalElements) 40 | .last(slice.isLast()) 41 | .content(slice.getContent()) 42 | .build(); 43 | } 44 | 45 | // 리스트를 자체 페이지 처리 46 | public static PageResponse of(Pageable pageable, List list) { 47 | int start = (int) pageable.getOffset(); 48 | int end = Math.min(start + pageable.getPageSize(), list.size()); 49 | List content = new ArrayList<>(); 50 | 51 | if (start > end) list.subList(end, list.size()); 52 | else content = list.subList(start, end); 53 | 54 | Page newPage = new PageImpl<>( 55 | content, 56 | pageable, 57 | list.size() 58 | ); 59 | return PageResponse.of(newPage); 60 | } 61 | 62 | public static PageResponse of(Page origin, List list) { 63 | return PageResponse.builder() 64 | .totalElements(origin.getTotalElements()) 65 | .last(origin.isLast()) 66 | .content(list) 67 | .build(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/controller/NotificationController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.controller; 2 | 3 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 4 | import bokjak.bokjakserver.common.dto.ApiResponse; 5 | import bokjak.bokjakserver.config.security.PrincipalDetails; 6 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto; 7 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto.NotificationResponse; 8 | import bokjak.bokjakserver.domain.notification.service.NotificationService; 9 | import bokjak.bokjakserver.domain.user.model.User; 10 | import bokjak.bokjakserver.domain.user.service.UserService; 11 | import io.swagger.v3.oas.annotations.Operation; 12 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 13 | import io.swagger.v3.oas.annotations.tags.Tag; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import static bokjak.bokjakserver.common.constant.SwaggerConstants.*; 19 | import static bokjak.bokjakserver.common.dto.ApiResponse.*; 20 | 21 | @RestController 22 | @RequiredArgsConstructor 23 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 24 | @RequestMapping("/notification") 25 | @Tag(name = TAG_NOTIFICATION, description = TAG_NOTIFICATION_DESCRIPTION) 26 | public class NotificationController { 27 | 28 | private final NotificationService notificationService; 29 | 30 | @Operation(summary = NOTIFICATION_ME, description = NOTIFICATION_ME_DESCRIPTION) 31 | @GetMapping("/users/me") 32 | public ApiResponse getMyNotifications(@AuthenticationPrincipal PrincipalDetails principalDetails) { 33 | return success(notificationService.getMyNotifications(principalDetails.getUserId())); 34 | } 35 | 36 | @Operation(summary = NOTIFICATION_READ, description = NOTIFICATION_READ_DESCRIPTION) 37 | @PutMapping("/{notificationId}/read") 38 | public ApiResponse readNotification(@PathVariable Long notificationId, 39 | @AuthenticationPrincipal PrincipalDetails principalDetails) { 40 | return success(notificationService.readNotification(notificationId, principalDetails.getUserId())); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/location/model/Location.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location.model; 2 | 3 | import bokjak.bokjakserver.common.model.BaseEntity; 4 | import bokjak.bokjakserver.domain.bookmark.model.LocationBookmark; 5 | import bokjak.bokjakserver.domain.category.model.LocationCategory; 6 | import bokjak.bokjakserver.domain.congestion.model.Congestion; 7 | import bokjak.bokjakserver.domain.congestion.model.DailyCongestionStatistic; 8 | import bokjak.bokjakserver.domain.congestion.model.WeeklyCongestionStatistic; 9 | import bokjak.bokjakserver.domain.spot.model.Spot; 10 | import jakarta.persistence.*; 11 | import jakarta.validation.constraints.NotNull; 12 | import lombok.*; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | @Entity 18 | @Getter 19 | @Builder 20 | @AllArgsConstructor 21 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 22 | public class Location extends BaseEntity { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | @Column(name = "location_id") 27 | private Long id; 28 | 29 | @NotNull 30 | private String name; 31 | 32 | @Column(name = "realtime_congestion_level") 33 | private Integer realtimeCongestionLevel; 34 | private int apiId; // 혼잡도 API(SK, 서울시)상의 장소 PK값 35 | 36 | @ManyToOne(fetch = FetchType.LAZY) 37 | @NotNull 38 | @JoinColumn(name = "location_category_id") 39 | private LocationCategory locationCategory; 40 | 41 | @OneToMany(mappedBy = "location", cascade = CascadeType.ALL, orphanRemoval = true) 42 | @Builder.Default 43 | private List congestionList = new ArrayList<>(); 44 | 45 | @OneToMany(mappedBy = "location", cascade = CascadeType.ALL, orphanRemoval = true) 46 | @Builder.Default 47 | private List dailyCongestionStatisticList = new ArrayList<>(); 48 | 49 | @OneToMany(mappedBy = "location", cascade = CascadeType.ALL, orphanRemoval = true) 50 | @Builder.Default 51 | private List weeklyCongestionStatisticList = new ArrayList<>(); 52 | 53 | @OneToMany(mappedBy = "location", cascade = CascadeType.ALL, orphanRemoval = true) 54 | @Builder.Default 55 | private List spotList = new ArrayList<>(); 56 | 57 | @OneToMany(mappedBy = "location", cascade = CascadeType.ALL, orphanRemoval = true) 58 | @Builder.Default 59 | private List locationBookmarkList = new ArrayList<>(); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/dto/CongestionHistoricalDateChoice.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.dto; 2 | 3 | import bokjak.bokjakserver.common.constant.GlobalConstants; 4 | import bokjak.bokjakserver.common.exception.StatusCode; 5 | import bokjak.bokjakserver.domain.category.exception.CategoryException; 6 | import bokjak.bokjakserver.util.CustomDateUtils; 7 | import bokjak.bokjakserver.util.enums.EnumModel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | 11 | import java.text.SimpleDateFormat; 12 | import java.util.Calendar; 13 | 14 | @Getter 15 | @AllArgsConstructor 16 | public enum CongestionHistoricalDateChoice implements EnumModel { 17 | MON("월요일"), TUE("화요일"), WED("수요일"), THU("목요일"), 18 | FRI("금요일"), SAT("토요일"), SUN("일요일"); 19 | 20 | private final String value; 21 | 22 | public static CongestionHistoricalDateChoice toEnum(String stringParam) { 23 | return switch (stringParam.toUpperCase()) { 24 | case "MON" -> MON; 25 | case "TUE" -> TUE; 26 | case "WED" -> WED; 27 | case "THU" -> THU; 28 | case "FRI" -> FRI; 29 | case "SAT" -> SAT; 30 | case "SUN" -> SUN; 31 | 32 | default -> throw new CategoryException(StatusCode.CHOICE_NOT_EXIST); 33 | }; 34 | } 35 | 36 | // toDateTime: congestionDateFilter에 대해 요청된 요일(name 필드)에 따라 값을 다르게 변환. SQL 날짜 형식에 맞도록 포맷 37 | public static String toDateTime(CongestionHistoricalDateChoice choice) { 38 | Calendar calendar = CustomDateUtils.makePastWeekDayDate(switchChoiceToDayOfWeek(choice)); 39 | 40 | SimpleDateFormat formatter = new SimpleDateFormat(GlobalConstants.DATE_FORMAT); 41 | return formatter.format(calendar.getTime()); 42 | } 43 | 44 | private static int switchChoiceToDayOfWeek(CongestionHistoricalDateChoice choice) {// choice에 따라 다른 요일값으로 설정 45 | return switch (choice) { 46 | case MON -> Calendar.MONDAY; 47 | case TUE -> Calendar.TUESDAY; 48 | case WED -> Calendar.WEDNESDAY; 49 | case THU -> Calendar.THURSDAY; 50 | case FRI -> Calendar.FRIDAY; 51 | case SAT -> Calendar.SATURDAY; 52 | case SUN -> Calendar.SUNDAY; 53 | }; 54 | } 55 | @Override 56 | public String getKey() { 57 | return name(); 58 | } 59 | 60 | @Override 61 | public String getValue() { 62 | return value; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ${LOG_PATTERN} 22 | 23 | 24 | 25 | 26 | ${AWS_LOG_PATTERN} 27 | 28 | bokjak-log 29 | bokjak-log- 30 | ${AWS_REGION} 31 | 50 32 | 30000 33 | 5000 34 | 0 35 | ${AWS_ACCESS_KEY} 36 | ${AWS_SECRET_KEY} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/category/dto/CategoryDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.category.dto; 2 | 3 | import bokjak.bokjakserver.domain.category.model.LocationCategory; 4 | import bokjak.bokjakserver.domain.category.model.SpotCategory; 5 | import bokjak.bokjakserver.util.enums.EnumQueryValue; 6 | import bokjak.bokjakserver.util.enums.EnumValue; 7 | import lombok.Builder; 8 | 9 | import java.util.*; 10 | 11 | import static bokjak.bokjakserver.util.enums.EnumQueryValue.toEnumQueryValues; 12 | import static bokjak.bokjakserver.util.enums.EnumValue.toEnumValues; 13 | 14 | 15 | public class CategoryDto { 16 | @Builder 17 | public record LocationCategoryResponse( 18 | Long id, 19 | String name, 20 | String iconImageUrl 21 | ) { 22 | 23 | public static LocationCategoryResponse of(LocationCategory locationCategory) { 24 | return LocationCategoryResponse.builder() 25 | .id(locationCategory.getId()) 26 | .name(locationCategory.getName()) 27 | .iconImageUrl(locationCategory.getIconImageUrl()) 28 | .build(); 29 | } 30 | } 31 | 32 | @Builder 33 | public record SpotCategoryResponse( 34 | Long id, 35 | String name 36 | ) { 37 | 38 | public static SpotCategoryResponse of(SpotCategory spotCategory) { 39 | return SpotCategoryResponse.builder() 40 | .id(spotCategory.getId()) 41 | .name(spotCategory.getName()) 42 | .build(); 43 | } 44 | } 45 | 46 | @Builder 47 | public record AllCategoryResponse( 48 | List locationCategories, 49 | List spotCategories, 50 | List> congestionLevelChoices, 51 | List> congestionHistoricalDateChoices 52 | ) { 53 | 54 | public static AllCategoryResponse of(List locationCategories, 55 | List spotCategories) { 56 | 57 | return AllCategoryResponse.builder() 58 | .locationCategories(locationCategories) 59 | .spotCategories(spotCategories) 60 | .congestionLevelChoices(toEnumValues(CongestionLevelChoice.class)) 61 | .congestionHistoricalDateChoices(toEnumQueryValues(CongestionHistoricalDateChoice.class)) 62 | .build(); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/service/KakaoService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.service; 2 | 3 | import bokjak.bokjakserver.domain.user.dto.AuthDto.OAuthSocialEmailResponse; 4 | import bokjak.bokjakserver.domain.user.dto.AuthDto.RevokeKakaoResponse; 5 | import bokjak.bokjakserver.domain.user.model.SocialType; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.http.*; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.util.LinkedMultiValueMap; 12 | import org.springframework.util.MultiValueMap; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | import java.util.Collections; 16 | import java.util.Map; 17 | 18 | @Service 19 | @RequiredArgsConstructor 20 | @Slf4j 21 | public class KakaoService { 22 | 23 | @Value("${kakao.adminKey}") 24 | private String adminKey; 25 | 26 | private final RestTemplate restTemplate; 27 | private final SocialService socialService; 28 | 29 | protected OAuthSocialEmailResponse getKakaoId(String oauthAccessToken) { 30 | String socialUrl = SocialType.KAKAO.getSocialUrl(); 31 | HttpMethod httpMethod = SocialType.KAKAO.getHttpMethod(); 32 | ResponseEntity> response = 33 | socialService.validOauthAccessToken(oauthAccessToken, socialUrl, httpMethod); 34 | Map oauthBody = response.getBody(); 35 | String id = String.valueOf(oauthBody.get("id")); 36 | log.info("id = {}", id); 37 | return OAuthSocialEmailResponse.to(id + "@KAKAO"); 38 | } 39 | 40 | protected boolean revokeKakao(String socialUuid) { 41 | String unlinkUrl = "https://kapi.kakao.com/v1/user/unlink"; 42 | MultiValueMap params = new LinkedMultiValueMap<>(); 43 | 44 | params.add("target_id_type", "user_id"); 45 | params.add("target_id", socialUuid); 46 | log.info("socialUuid = {}", socialUuid); 47 | HttpHeaders headers = new HttpHeaders(); 48 | headers.set("Authorization", "KakaoAK " + adminKey); 49 | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 50 | headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); 51 | HttpEntity> httpEntity = new HttpEntity<>(params, headers); 52 | ResponseEntity response = restTemplate.postForEntity(unlinkUrl, httpEntity, RevokeKakaoResponse.class); 53 | int revokeStatusCode = response.getStatusCode().value(); 54 | return revokeStatusCode == 200; 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/web/log/LoggerAspect.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.web.log; 2 | 3 | import bokjak.bokjakserver.util.client.ClientIPAddressUtils; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.aspectj.lang.ProceedingJoinPoint; 7 | import org.aspectj.lang.annotation.Around; 8 | import org.aspectj.lang.annotation.Aspect; 9 | import org.aspectj.lang.annotation.Pointcut; 10 | import org.json.simple.JSONObject; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.context.request.RequestContextHolder; 13 | import org.springframework.web.context.request.ServletRequestAttributes; 14 | 15 | import java.util.Enumeration; 16 | import java.util.Objects; 17 | 18 | @Component 19 | @Aspect 20 | @Slf4j 21 | public class LoggerAspect { 22 | @Pointcut("execution(* bokjak.bokjakserver..*Controller.*(..)) || execution(* bokjak.bokjakserver..*GlobalExceptionHandler.*(..))") 23 | // 이런 패턴이 실행될 경우 수행 24 | public void loggerPointCut() { 25 | } 26 | 27 | @Around("loggerPointCut()") 28 | public Object logRequestUri(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 29 | Object result = proceedingJoinPoint.proceed(); 30 | ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 31 | 32 | if (Objects.nonNull(requestAttributes)) { 33 | HttpServletRequest request = requestAttributes.getRequest(); // request 정보를 가져온다. 34 | String controllerName = proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName(); 35 | String methodName = proceedingJoinPoint.getSignature().getName(); 36 | log.info("{} {}.{}: {} {} PARAM={}", 37 | ClientIPAddressUtils.getClientIP(request), // IP 38 | controllerName, 39 | methodName, 40 | request.getMethod(), 41 | request.getRequestURI(), 42 | extractParams(request) 43 | ); // param에 담긴 정보들을 한번에 로깅한다. 44 | } 45 | 46 | return result; 47 | } 48 | 49 | private static JSONObject extractParams(HttpServletRequest request) { // request로부터 param 추출, JSONObject로 변환 50 | JSONObject jsonObject = new JSONObject(); 51 | Enumeration params = request.getParameterNames(); 52 | while (params.hasMoreElements()) { 53 | String param = params.nextElement(); 54 | String replaceParam = param.replaceAll("\\.", "-"); 55 | jsonObject.put(replaceParam, request.getParameter(param)); 56 | } 57 | return jsonObject; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/bokjak/bokjakserver/domain/location/LocationServiceTest.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location; 2 | 3 | import bokjak.bokjakserver.domain.bookmark.model.LocationBookmark; 4 | import bokjak.bokjakserver.domain.location.dto.LocationDto; 5 | import bokjak.bokjakserver.domain.location.model.Location; 6 | import bokjak.bokjakserver.domain.location.repository.LocationRepository; 7 | import bokjak.bokjakserver.domain.location.service.LocationService; 8 | import bokjak.bokjakserver.domain.user.UserTemplate; 9 | import bokjak.bokjakserver.domain.user.model.User; 10 | import bokjak.bokjakserver.domain.user.service.UserService; 11 | import org.assertj.core.api.Assertions; 12 | import org.junit.jupiter.api.DisplayName; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import java.util.Optional; 20 | 21 | import static org.mockito.ArgumentMatchers.anyLong; 22 | import static org.mockito.BDDMockito.given; 23 | 24 | @ExtendWith(MockitoExtension.class) 25 | public class LocationServiceTest {// BDD 26 | @Mock 27 | UserService userService; 28 | 29 | @Mock 30 | LocationRepository locationRepository; 31 | 32 | @InjectMocks 33 | LocationService locationService; 34 | 35 | static User mockUser = UserTemplate.makeDummyUserA(); 36 | static Location mockLocation = LocationMockUtils.makeDummyLocation(); 37 | 38 | @Test 39 | @DisplayName("북마크 - 등록") 40 | public void bookmark_enroll() { 41 | // given 42 | given(userService.getUser(anyLong())).willReturn(mockUser); 43 | given(locationRepository.findById(anyLong())).willReturn(Optional.ofNullable(mockLocation)); 44 | 45 | // when 46 | LocationDto.BookmarkResponse response = locationService.bookmark(1L, 1L); 47 | 48 | // then 49 | Assertions.assertThat(response.isBookmarked()).isTrue(); 50 | } 51 | 52 | @Test 53 | @DisplayName("북마크 - 취소") 54 | public void bookmark_cancel() { 55 | // given 56 | given(userService.getUser(anyLong())).willReturn(mockUser); 57 | given(locationRepository.findById(anyLong())).willReturn(Optional.ofNullable(mockLocation)); 58 | 59 | mockUser.addLocationBookmark(LocationBookmark.builder() // 북마크 데이터 삽입 60 | .user(mockUser) 61 | .location(mockLocation) 62 | .build()); 63 | 64 | // when 65 | LocationDto.BookmarkResponse response = locationService.bookmark(1L, 1L); 66 | 67 | // then 68 | Assertions.assertThat(response.isBookmarked()).isFalse(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/bokjak/bokjakserver/domain/user/UserTemplate.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user; 2 | 3 | import bokjak.bokjakserver.domain.user.model.Role; 4 | import bokjak.bokjakserver.domain.user.model.SocialType; 5 | import bokjak.bokjakserver.domain.user.model.User; 6 | import bokjak.bokjakserver.domain.user.model.UserStatus; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | 9 | public class UserTemplate { 10 | 11 | private static final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); 12 | 13 | private static Long id = 10000L; 14 | private static final String EMAIL = "test@naver.com"; 15 | private static final String DUMMY_PASSWORD_A = "test1@naver.com"; 16 | private static final String DUMMY_PASSWORD_B = "test2@naver.com"; 17 | private static final String DUMMY_NICKNAME_A = "test"; 18 | private static final String DUMMY_NICKNAME_B = "test2"; 19 | private static final String DUMMY_SOCIAL_EMAIL_A = "test1@KAKAO"; 20 | private static final String DUMMY_SOCIAL_EMAIL_B = "test2@KAKAO"; 21 | private static final String PROFILE_IMAGE_URL = "2031239.com"; 22 | private static final UserStatus USER_STATUS = UserStatus.NORMAL; 23 | private static final SocialType SOCIAL_TYPE = SocialType.KAKAO; 24 | private static final Role ROLE = Role.ROLE_USER; 25 | 26 | 27 | private static User getMakeUser(String email, String password, String nickname, String socialEmail, 28 | String profileImageUrl, UserStatus userStatus, SocialType socialType, Role role) { 29 | return User.builder() 30 | .email(email) 31 | .password(password) 32 | .nickname(nickname) 33 | .socialEmail(socialEmail) 34 | .profileImageUrl(profileImageUrl) 35 | .userStatus(userStatus) 36 | .socialType(socialType) 37 | .role(role).build(); 38 | } 39 | 40 | public static User makeTestUser(String email, String password, String nickname, String socialEmail, 41 | String profileImageUrl, UserStatus userStatus, SocialType socialType, Role role) { 42 | User user = getMakeUser(email, password, nickname, socialEmail, profileImageUrl, userStatus, socialType, role); 43 | return user; 44 | } 45 | 46 | public static User makeDummyUserA() { 47 | return makeTestUser(EMAIL,DUMMY_PASSWORD_A,DUMMY_NICKNAME_A,DUMMY_SOCIAL_EMAIL_A,PROFILE_IMAGE_URL,USER_STATUS,SOCIAL_TYPE,ROLE); 48 | } 49 | public static User makeDummyUserB() { 50 | return makeTestUser(EMAIL,DUMMY_PASSWORD_B,DUMMY_NICKNAME_B,DUMMY_SOCIAL_EMAIL_B,PROFILE_IMAGE_URL,USER_STATUS,SOCIAL_TYPE,ROLE); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/UserDummy.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import bokjak.bokjakserver.domain.user.dto.AuthDto.SignUpRequest; 4 | import bokjak.bokjakserver.domain.user.repository.UserRepository; 5 | import jakarta.annotation.PostConstruct; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.ArrayList; 11 | 12 | @Slf4j 13 | @Component("userDummy") 14 | @RequiredArgsConstructor 15 | @BuzzingDummy 16 | public class UserDummy { 17 | private final UserRepository userRepository; 18 | 19 | @PostConstruct 20 | public void init() { 21 | if (userRepository.count() > 0) { 22 | log.info("[userDummy] 더미 유저가 이미 존재합니다."); 23 | } else { 24 | createUsers(); 25 | log.info("[userDummy] 더미 유저 생성완료"); 26 | } 27 | } 28 | 29 | private void createUsers() { 30 | ArrayList imageUrls = new ArrayList<>(); 31 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/astro_pepe.jpeg"); 32 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/classic_pepe.jpg"); 33 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/crying_pepe.jpeg"); 34 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/Feels_good_pepe.jpg"); 35 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/gatsby_pepe.jpg"); 36 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/health_pepe.jpeg"); 37 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/heyatch_pepe.png"); 38 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/laughing_pepe.jpg"); 39 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/thinking_pepe.jpg"); 40 | imageUrls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/user/profile/punching_pepe.jpg"); 41 | 42 | ArrayList nicknames = new ArrayList<>(); 43 | nicknames.add("astro_pepe"); 44 | nicknames.add("classic_pepe"); 45 | nicknames.add("crying_pepe"); 46 | nicknames.add("Feels_good_pepe"); 47 | nicknames.add("gatsby_pepe"); 48 | nicknames.add("health_pepe"); 49 | nicknames.add("heyatch_pepe"); 50 | nicknames.add("laughing_pepe"); 51 | nicknames.add("thinking_pepe"); 52 | nicknames.add("punching_pepe"); 53 | 54 | for (int i = 0; i < 10; i++) { 55 | SignUpRequest dummyUserForm = SignUpRequest.builder() 56 | .build(); 57 | userRepository.save(dummyUserForm.toDummy(nicknames.get(i) + "@naver.com", nicknames.get(i), i + "@KAKAO", imageUrls.get(i))); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.controller; 2 | 3 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 4 | import bokjak.bokjakserver.common.dto.ApiResponse; 5 | import bokjak.bokjakserver.config.jwt.JwtDto; 6 | import bokjak.bokjakserver.config.security.PrincipalDetails; 7 | import bokjak.bokjakserver.domain.user.service.AuthService; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 10 | import io.swagger.v3.oas.annotations.tags.Tag; 11 | import jakarta.validation.Valid; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import static bokjak.bokjakserver.common.constant.SwaggerConstants.*; 18 | import static bokjak.bokjakserver.common.dto.ApiResponse.success; 19 | import static bokjak.bokjakserver.domain.user.dto.AuthDto.*; 20 | 21 | @RestController 22 | @RequestMapping("/auth") 23 | @RequiredArgsConstructor 24 | @Tag(name = TAG_AUTH, description = TAG_AUTH_DESCRIPTION) 25 | @Slf4j 26 | public class AuthController { 27 | 28 | private final AuthService authService; 29 | 30 | @Operation(summary = AUTH_LOGIN, description = AUTH_LOGIN_DESCRIPTION) 31 | @PostMapping("/login") 32 | public ApiResponse login(@RequestBody @Valid SocialLoginRequest socialLoginRequest) { 33 | AuthMessage authMessage = authService.loginAccess(socialLoginRequest); 34 | return success(authMessage.detailData()); 35 | } 36 | 37 | @Operation(summary = AUTH_SIGNUP) 38 | @PostMapping("/signup") 39 | public ApiResponse signup(@Valid @RequestBody SignUpRequest signUpRequest) { 40 | SignAuthMessage signAuthMessage = authService.signUp(signUpRequest); 41 | return success(signAuthMessage.detaildata()); 42 | } 43 | 44 | @Operation(summary = AUTH_REISSUE) 45 | @PostMapping("/reissue") 46 | public ApiResponse reissue(@RequestBody ReissueRequest reissueRequest) { 47 | return success(authService.reissue(reissueRequest)); 48 | } 49 | 50 | @Operation(summary = AUTH_LOGOUT) 51 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 52 | @PostMapping("/logout") 53 | public ApiResponse logout(@AuthenticationPrincipal PrincipalDetails principalDetails) { 54 | return success(authService.logout(principalDetails.getUserId())); 55 | } 56 | 57 | @PostMapping("/login/admin") 58 | @Operation(summary = ADMIN_LOGIN, description = ADMIN_LOGIN_DESCRIPTION) 59 | public ApiResponse adminLogin(@RequestBody @Valid AdminLoginRequest adminLoginRequest) { 60 | AuthMessage authMessage = authService.loginAdmin(adminLoginRequest); 61 | return success(authMessage.detailData()); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/service/NotificationService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.service; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto.NotificationListResponse; 5 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto.NotificationResponse; 6 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto.NotifyParams; 7 | import bokjak.bokjakserver.domain.notification.exception.NotificationException; 8 | import bokjak.bokjakserver.domain.notification.model.Notification; 9 | import bokjak.bokjakserver.domain.notification.repository.NotificationRepository; 10 | import bokjak.bokjakserver.domain.user.model.User; 11 | import bokjak.bokjakserver.domain.user.service.UserService; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | import java.util.List; 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | @Transactional(readOnly = true) 23 | public class NotificationService { 24 | 25 | private final FcmService fcmService; 26 | private final NotificationRepository notificationRepository; 27 | private final UserService userService; 28 | 29 | @Transactional 30 | public void pushMessage(NotifyParams params) { 31 | User receiver = userService.getUser(params.receiver().getId()); 32 | 33 | fcmService.sendPushMessage(receiver.getFcmToken(), params); 34 | Notification notification = Notification.builder() 35 | .user(receiver) 36 | .title(params.title()) 37 | .content(params.content()) 38 | .isRead(false) 39 | .type(params.type()) 40 | .redirectTargetId(params.redirectTargetId()) 41 | .build(); 42 | 43 | notificationRepository.save(notification); 44 | 45 | receiver.addNotification(notification); 46 | } 47 | 48 | public NotificationListResponse getMyNotifications(Long userId) { 49 | userService.getUser(userId); 50 | 51 | List limitCountAndSortByRecent = notificationRepository.findLimitCountAndSortByRecent(userId); 52 | return NotificationListResponse.of(limitCountAndSortByRecent.stream() 53 | .map(NotificationResponse::of) 54 | .toList()); 55 | } 56 | 57 | @Transactional 58 | public NotificationResponse readNotification(Long notificationId, Long userId) { 59 | userService.getUser(userId); 60 | 61 | Notification notification = notificationRepository.findByNotificationIdAndUserId(notificationId, userId) 62 | .orElseThrow(() -> new NotificationException(StatusCode.NOT_FOUND_NOTIFICATION)); 63 | 64 | notification.read(); 65 | 66 | return NotificationResponse.of(notification); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/util/CustomEncryptUtil.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.util; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.user.exeption.AuthException; 5 | import bokjak.bokjakserver.domain.user.exeption.UserException; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.crypto.Cipher; 10 | import javax.crypto.spec.SecretKeySpec; 11 | import java.nio.charset.StandardCharsets; 12 | import java.security.MessageDigest; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.util.Arrays; 15 | import java.util.Base64; 16 | 17 | @Component 18 | public class CustomEncryptUtil { 19 | 20 | private byte[] key; 21 | private SecretKeySpec secretKeySpec; 22 | 23 | public CustomEncryptUtil(@Value("${aes.secret-key}") String rawKey) { 24 | try { 25 | MessageDigest sha = MessageDigest.getInstance("SHA-256"); 26 | key = rawKey.getBytes(StandardCharsets.UTF_8); 27 | key = sha.digest(key); //바이트배열로 해쉬 반환 28 | key = Arrays.copyOf(key, 24); 29 | secretKeySpec = new SecretKeySpec(key, "AES"); 30 | } catch (Exception ignored) { 31 | } 32 | } 33 | 34 | public String hash(String socialEmail) { 35 | try{ 36 | MessageDigest sha = MessageDigest.getInstance("SHA-256"); 37 | sha.update(socialEmail.getBytes()); 38 | 39 | return bytesToHex(sha.digest()); 40 | } catch (Exception ignored) { 41 | throw new UserException(StatusCode.ENCRYPTION_FAILURE); 42 | } 43 | } 44 | 45 | private String bytesToHex(byte[] bytes) { 46 | StringBuilder builder = new StringBuilder(); 47 | for (byte b : bytes) { 48 | builder.append(String.format("%02x", b)); 49 | } 50 | return builder.toString(); 51 | } 52 | 53 | public String encrypt(String str) { 54 | try { 55 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 56 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); 57 | return encodeBase64(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8))); 58 | } catch (Exception e) { 59 | throw new AuthException(StatusCode.ENCRYPTION_FAILURE); 60 | } 61 | } 62 | //secretKey의 길이가 32bit면 AES-256, 24bit면 AES-192, 16bit의 경우 AES-128로 암호화 63 | 64 | public String decrypt(String str) { 65 | try { 66 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 67 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); 68 | return new String(cipher.doFinal(decodeBase64(str))); 69 | } catch (Exception e) { 70 | throw new AuthException(StatusCode.DECRYPTION_FAILURE); 71 | } 72 | } 73 | 74 | private String encodeBase64(byte[] source) { 75 | return Base64.getEncoder().encodeToString(source); 76 | } 77 | 78 | private byte[] decodeBase64(String encodedString) { 79 | return Base64.getDecoder().decode(encodedString); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/spot/model/Spot.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.spot.model; 2 | 3 | 4 | import bokjak.bokjakserver.common.constant.ConstraintConstants; 5 | import bokjak.bokjakserver.common.model.BaseEntity; 6 | import bokjak.bokjakserver.domain.bookmark.model.SpotBookmark; 7 | import bokjak.bokjakserver.domain.category.model.SpotCategory; 8 | import bokjak.bokjakserver.domain.comment.model.Comment; 9 | import bokjak.bokjakserver.domain.location.model.Location; 10 | import bokjak.bokjakserver.domain.user.model.User; 11 | import jakarta.persistence.*; 12 | import jakarta.validation.constraints.NotNull; 13 | import jakarta.validation.constraints.Size; 14 | import lombok.*; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | @Entity 20 | @Getter 21 | @Builder 22 | @AllArgsConstructor 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | public class Spot extends BaseEntity { 25 | 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | @Column(name = "spot_id") 29 | private Long id; 30 | 31 | @NotNull 32 | @Size(max = ConstraintConstants.SPOT_TITLE_MAX_LENGTH) 33 | private String title; 34 | 35 | @Size(max = ConstraintConstants.SPOT_ADDRESS_MAX_LENGTH) 36 | private String address; 37 | 38 | private String roadNameAddress; // deprecated : 클라이언트측에서 도로명 입력 안 함 39 | 40 | @NotNull 41 | @Size(max = ConstraintConstants.SPOT_CONTENT_MAX_LENGTH) 42 | private String content; 43 | 44 | @ManyToOne(fetch = FetchType.LAZY) 45 | @NotNull 46 | @JoinColumn(name = "location_id") 47 | private Location location; 48 | 49 | @ManyToOne(fetch = FetchType.LAZY) 50 | @NotNull 51 | @JoinColumn(name = "spot_category_id") 52 | private SpotCategory spotCategory; 53 | 54 | @ManyToOne(fetch = FetchType.LAZY) 55 | @NotNull 56 | @JoinColumn(name = "user_id") 57 | private User user; 58 | 59 | @OneToMany(mappedBy = "spot", cascade = CascadeType.ALL, orphanRemoval = true) 60 | @Builder.Default 61 | private List spotBookmarkList = new ArrayList<>(); 62 | 63 | @OneToMany(mappedBy = "spot", cascade = CascadeType.ALL, orphanRemoval = true) 64 | @Builder.Default 65 | private List commentList = new ArrayList<>(); 66 | 67 | @OneToMany(mappedBy = "spot", cascade = CascadeType.ALL, orphanRemoval = true) 68 | @Builder.Default 69 | private List spotImageList = new ArrayList<>(); 70 | 71 | /* 연관관계 메서드 */ 72 | public void addSpotImage(SpotImage spotImage) { 73 | spotImageList.add(spotImage); 74 | } 75 | 76 | public void addSpotImages(List spotImages) { 77 | spotImageList.addAll(spotImages); 78 | } 79 | 80 | public void update(Location location, SpotCategory spotCategory, String title, String address, String content, List spotImages) { 81 | this.location = location; 82 | this.spotCategory = spotCategory; 83 | this.title = title; 84 | this.address = address; 85 | this.content = content; 86 | this.spotImageList.clear(); 87 | this.spotImageList.addAll(spotImages); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/image/controller/ImageController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.image.controller; 2 | 3 | import static bokjak.bokjakserver.common.dto.ApiResponse.success; 4 | 5 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 6 | import bokjak.bokjakserver.common.dto.ApiResponse; 7 | import bokjak.bokjakserver.domain.image.S3SaveDir; 8 | import bokjak.bokjakserver.domain.image.dto.ImageDto; 9 | import bokjak.bokjakserver.domain.image.dto.ImageDto.DeleteFileResponse; 10 | import bokjak.bokjakserver.domain.image.dto.ImageDto.UpdateFilesResponse; 11 | import bokjak.bokjakserver.domain.image.dto.ImageDto.UploadFilesResponse; 12 | import bokjak.bokjakserver.domain.image.service.AwsS3Service; 13 | import io.swagger.v3.oas.annotations.Operation; 14 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 15 | import io.swagger.v3.oas.annotations.tags.Tag; 16 | import jakarta.validation.Valid; 17 | import java.util.List; 18 | import lombok.RequiredArgsConstructor; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.web.bind.annotation.DeleteMapping; 21 | import org.springframework.web.bind.annotation.ModelAttribute; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.PostMapping; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RequestParam; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | @RestController 29 | @RequestMapping("/files") 30 | @RequiredArgsConstructor 31 | @Tag(name = SwaggerConstants.TAG_S3, description = SwaggerConstants.TAG_S3_DESCRIPTION) 32 | public class ImageController { 33 | private final AwsS3Service awsS3Service; 34 | 35 | @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 36 | @Operation(summary = SwaggerConstants.S3_FILE_UPLOAD, description = SwaggerConstants.S3_FILE_UPLOAD_DESCRIPTION) 37 | public ApiResponse uploadFiles( 38 | @Valid @ModelAttribute ImageDto.UploadFileRequest uploadFileRequest) { 39 | return success(awsS3Service.uploadFiles(uploadFileRequest)); 40 | } 41 | 42 | @PostMapping(value = "/change", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 43 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 44 | @Operation(summary = SwaggerConstants.S3_FILE_UPDATE, description = SwaggerConstants.S3_FILE_UPDATE_DESCRIPTION) 45 | public ApiResponse updateFiles( 46 | @Valid @ModelAttribute ImageDto.UpdateFileRequest updateFileRequest) { 47 | return success(awsS3Service.updateFiles(updateFileRequest)); 48 | } 49 | 50 | @DeleteMapping("/{type}") 51 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 52 | @Operation(summary = SwaggerConstants.S3_FILE_DELETE, description = SwaggerConstants.S3_FILE_DELETE_DESCRIPTION) 53 | public ApiResponse deleteFiles( 54 | @PathVariable(value = "type") String type, 55 | @RequestParam List fileUrl 56 | ) { 57 | awsS3Service.deleteMultipleFile(S3SaveDir.toEnum(type), fileUrl); 58 | return success(DeleteFileResponse.success()); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/user/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.user.controller; 2 | 3 | 4 | import bokjak.bokjakserver.common.constant.SwaggerConstants; 5 | import bokjak.bokjakserver.common.dto.ApiResponse; 6 | import bokjak.bokjakserver.config.security.PrincipalDetails; 7 | import bokjak.bokjakserver.domain.user.dto.AuthDto.AuthMessage; 8 | import bokjak.bokjakserver.domain.user.dto.UserDto.*; 9 | import bokjak.bokjakserver.domain.user.service.UserService; 10 | import io.swagger.v3.oas.annotations.Operation; 11 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 12 | import io.swagger.v3.oas.annotations.tags.Tag; 13 | import jakarta.validation.Valid; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import static bokjak.bokjakserver.common.constant.SwaggerConstants.*; 19 | import static bokjak.bokjakserver.common.dto.ApiResponse.success; 20 | 21 | @RestController 22 | @RequestMapping("/users") 23 | @RequiredArgsConstructor 24 | @Tag(name = TAG_USER, description = TAG_USER_DESCRIPTION) 25 | public class UserController { 26 | 27 | private final UserService userService; 28 | 29 | @Operation(summary = USER_ME) 30 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 31 | @GetMapping("/me") 32 | public ApiResponse getUserInfo(@AuthenticationPrincipal PrincipalDetails principalDetails) { 33 | return success(userService.getUserInfo(principalDetails.getUserId())); 34 | } 35 | 36 | @Operation(summary = USER_CHECK_NICKNAME, description = USER_CHECK_NICKNAME_DESCRIPTION) 37 | @GetMapping("/check/nickname/{nickname}") 38 | public ApiResponse isDuplicateNickname(@PathVariable String nickname) { 39 | return success(userService.isDuplicateNickname(nickname)); 40 | } 41 | 42 | @Operation(summary = USER_HIDE) 43 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 44 | @PostMapping("/hide") 45 | public ApiResponse hideUser(@RequestBody @Valid HideRequest hideRequest, 46 | @AuthenticationPrincipal PrincipalDetails principalDetails) { 47 | return success(userService.hideUser(hideRequest, principalDetails.getUserId())); 48 | } 49 | 50 | @Operation(summary = USER_UPDATE_PROFILE, description = USER_UPDATE_PROFILE_DESCRIPTION) 51 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 52 | @PostMapping("/me/profile") 53 | public ApiResponse updateUserInfo(@RequestBody @Valid UpdateUserInfoRequest updateUserInfoRequest, 54 | @AuthenticationPrincipal PrincipalDetails principalDetails) { 55 | return success(userService.updateUserInfo(updateUserInfoRequest, principalDetails.getUserId())); 56 | } 57 | 58 | @Operation(summary = USER_REVOKE) 59 | @SecurityRequirement(name = SwaggerConstants.SECURITY_SCHEME_NAME) 60 | @PostMapping("/revoke") 61 | public ApiResponse revoke(@AuthenticationPrincipal PrincipalDetails principalDetails) { 62 | AuthMessage revoke = userService.revoke(principalDetails.getUserId()); 63 | return success(revoke.detailData()); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/report/service/ReportService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.report.service; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.comment.model.Comment; 5 | import bokjak.bokjakserver.domain.comment.repository.CommentRepository; 6 | import bokjak.bokjakserver.domain.report.dto.ReportDto; 7 | import bokjak.bokjakserver.domain.report.dto.ReportDto.ReportIdResponse; 8 | import bokjak.bokjakserver.domain.report.dto.ReportDto.ReportRequest; 9 | import bokjak.bokjakserver.domain.report.exception.ReportException; 10 | import bokjak.bokjakserver.domain.report.model.Report; 11 | import bokjak.bokjakserver.domain.report.model.ReportTarget; 12 | import bokjak.bokjakserver.domain.report.repository.ReportRepository; 13 | import bokjak.bokjakserver.domain.spot.model.Spot; 14 | import bokjak.bokjakserver.domain.spot.repository.SpotRepository; 15 | import bokjak.bokjakserver.domain.user.model.User; 16 | import bokjak.bokjakserver.domain.user.repository.UserRepository; 17 | import lombok.RequiredArgsConstructor; 18 | import lombok.extern.slf4j.Slf4j; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.transaction.annotation.Transactional; 21 | 22 | @Service 23 | @RequiredArgsConstructor 24 | @Transactional(readOnly = true) 25 | @Slf4j 26 | public class ReportService { 27 | 28 | private final ReportRepository reportRepository; 29 | private final UserRepository userRepository; 30 | private final CommentRepository commentRepository; 31 | private final SpotRepository spotRepository; 32 | 33 | @Transactional 34 | public ReportIdResponse createReport(Long userId, ReportRequest reportRequest) { 35 | User reporter = userRepository.findById(userId).orElseThrow(() -> new ReportException(StatusCode.NOT_FOUND_USER)); 36 | User reportedUser = userRepository.findById(reportRequest.reportedUserId()).orElseThrow(() -> new ReportException(StatusCode.NOT_FOUND_REPORTED_USER)); 37 | checkContentLength(reportRequest); 38 | checkIsReport(reportedUser); 39 | Report report = reportRequest.toEntity(reporter, reportedUser); 40 | checkExistSpotOrComment(report.getReportTarget(), report.getTargetId(), reportedUser); 41 | boolean exists = reportRepository.existsByReporterAndReportedUserAndReportTargetAndTargetIdAndIsCheckedFalse( 42 | reporter, reportedUser, report.getReportTarget(), report.getTargetId()); 43 | if (exists) throw new ReportException(StatusCode.REPORT_DUPLICATION); 44 | 45 | reportRepository.save(report); 46 | reporter.addReporterUser(report); 47 | reportedUser.addReportedUser(report); 48 | 49 | return ReportIdResponse.of(report); 50 | } 51 | 52 | 53 | private void checkExistSpotOrComment(ReportTarget reportTarget, Long targetId, User reportedUser) { 54 | 55 | switch (reportTarget) { 56 | case SPOT : 57 | Spot spot = spotRepository.findById(targetId).orElseThrow(() -> new ReportException(StatusCode.NOT_FOUND_REPORT_TARGET)); 58 | if (!spot.getUser().equals(reportedUser)) throw new ReportException(StatusCode.NOT_CORRECT_USER_AND_TARGET); 59 | break; 60 | case COMMENT: 61 | Comment comment = commentRepository.findById(targetId).orElseThrow(() -> new ReportException(StatusCode.NOT_FOUND_REPORT_TARGET)); 62 | if (!comment.getUser().equals(reportedUser)) throw new ReportException(StatusCode.NOT_CORRECT_USER_AND_TARGET); 63 | } 64 | } 65 | 66 | private void checkIsReport(User user) { 67 | switch (user.getUserStatus()) { 68 | case BANNED: 69 | new ReportException(StatusCode.ALREADY_BAN_USER); 70 | case BLACKLIST: 71 | new ReportException(StatusCode.ALREADY_BAN_USER); 72 | default: 73 | break; 74 | } 75 | } 76 | 77 | private void checkContentLength(ReportRequest reportRequest) { 78 | if (reportRequest.content().length() > 300) throw new ReportException(StatusCode.OVER_CONTENT_LENGTH); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/SpotDummy.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import bokjak.bokjakserver.domain.category.model.SpotCategory; 4 | import bokjak.bokjakserver.domain.category.repository.SpotCategoryRepository; 5 | import bokjak.bokjakserver.domain.location.model.Location; 6 | import bokjak.bokjakserver.domain.location.repository.LocationRepository; 7 | import bokjak.bokjakserver.domain.spot.model.Spot; 8 | import bokjak.bokjakserver.domain.spot.model.SpotImage; 9 | import bokjak.bokjakserver.domain.spot.repository.SpotImageRepository; 10 | import bokjak.bokjakserver.domain.spot.repository.SpotRepository; 11 | import bokjak.bokjakserver.domain.user.model.User; 12 | import bokjak.bokjakserver.domain.user.repository.UserRepository; 13 | import jakarta.annotation.PostConstruct; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.context.annotation.DependsOn; 17 | import org.springframework.stereotype.Component; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | @Slf4j 23 | @Component("spotDummy") 24 | @DependsOn({"userDummy", "congestionDummy"}) 25 | @RequiredArgsConstructor 26 | @BuzzingDummy 27 | public class SpotDummy { 28 | private final LocationRepository locationRepository; 29 | private final SpotCategoryRepository spotCategoryRepository; 30 | private final SpotRepository spotRepository; 31 | private final UserRepository userRepository; 32 | private final SpotImageRepository spotImageRepository; 33 | 34 | @PostConstruct 35 | public void init() { 36 | if (spotRepository.count() > 0) { 37 | log.info("[spotDummy] 스팟 데이터가 이미 존재"); 38 | } else { 39 | createSpots(); 40 | log.info("[spotDummy] 스팟 더미 생성 완료"); 41 | } 42 | if (spotImageRepository.count() > 0) { 43 | log.info("[spotDummy-1] 스팟 이미지 데이터가 이미 존재"); 44 | } else { 45 | createSpotImages(); 46 | log.info("[spotDummy-1] 스팟 더미 생성 완료"); 47 | } 48 | } 49 | 50 | private void createSpots() { 51 | for (Location location : locationRepository.findAll()) {// 모든 로케이션에 대해 52 | List allCategory = spotCategoryRepository.findAll(); 53 | List allUser = userRepository.findAll(); 54 | 55 | ArrayList strings = new ArrayList<>(); 56 | strings.add("최고였다!!"); 57 | strings.add("나쁘지 않았다."); 58 | strings.add("별 거 없었다."); 59 | 60 | for (int i = 0; i < 3; i++) {// 유저 10명 61 | SpotCategory category = allCategory.get((int) (Math.random() * 100) % 3); 62 | spotRepository.save(Spot.builder() 63 | .location(location) 64 | .user(allUser.get(i)) 65 | .spotCategory(category) // 3가지 중 1 랜덤 66 | .address(location.getName() + i + "근처") 67 | .title(location.getName() + " 근처 " + category.getName()) 68 | .content(strings.get((int) (Math.random() * 100) % 3)) 69 | .build()); 70 | } 71 | } 72 | } 73 | 74 | private void createSpotImages() { 75 | List allSpot = spotRepository.findAll(); 76 | ArrayList urls = new ArrayList<>(); 77 | urls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/etc/daniel.jpg"); 78 | urls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/etc/hani_omg.png"); 79 | urls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/etc/hyein.jpg"); 80 | urls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/etc/hyerin.png"); 81 | urls.add("https://s3-buz.s3.ap-northeast-2.amazonaws.com/etc/minji.png"); 82 | 83 | for (Spot spot : allSpot) {// 스팟마다 0~5개 랜덤으로 84 | for (int i = 0; i < (int) (Math.random() * 100) % 6; i++) { 85 | spotImageRepository.save(SpotImage.builder() 86 | .spot(spot) 87 | .imageUrl(urls.get(i)) 88 | .build()); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/comment/dto/CommentDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.comment.dto; 2 | 3 | import bokjak.bokjakserver.common.constant.GlobalConstants; 4 | import bokjak.bokjakserver.domain.comment.model.Comment; 5 | import bokjak.bokjakserver.domain.spot.model.Spot; 6 | import bokjak.bokjakserver.domain.user.model.User; 7 | import bokjak.bokjakserver.util.CustomDateUtils; 8 | import bokjak.bokjakserver.util.web.PreventNullResponseUtils; 9 | import jakarta.validation.constraints.NotNull; 10 | import jakarta.validation.constraints.Size; 11 | import lombok.Builder; 12 | 13 | import static bokjak.bokjakserver.common.constant.ConstraintConstants.COMMENT_CONTENT_MAX_LENGTH; 14 | 15 | public class CommentDto { 16 | /* Request */ 17 | @Builder 18 | public record CreateSpotCommentRequest( 19 | @NotNull @Size(max = COMMENT_CONTENT_MAX_LENGTH) 20 | String content 21 | ) { 22 | public Comment toEntity(User user, Spot spot) { // 댓글 23 | return Comment.builder() 24 | .user(user) 25 | .spot(spot) 26 | .content(this.content) 27 | .build(); 28 | } 29 | 30 | public Comment toEntity(User user, Comment parent) { // 대댓글 31 | return Comment.builder() 32 | .user(user) 33 | .spot(parent.getSpot()) 34 | .parent(parent) 35 | .content(this.content) 36 | .build(); 37 | } 38 | } 39 | 40 | @Builder 41 | public record UpdateSpotCommentRequest( 42 | @NotNull @Size(max = COMMENT_CONTENT_MAX_LENGTH) 43 | String content 44 | ) { 45 | } 46 | 47 | /* Response */ 48 | @Builder 49 | public record CommentCardResponse( 50 | boolean presence, 51 | Long parentId, 52 | int childCount, 53 | Long id, 54 | String content, 55 | String createdAt, 56 | String updatedAt, 57 | Long userId, 58 | String userNickname, 59 | String userProfileImageUrl, 60 | boolean isAuthor 61 | ) { 62 | public static CommentCardResponse of(Comment comment, Long userId) { 63 | if (comment.isPresence()) {// 존재 여부에 따라 분기처리 64 | User author = comment.getUser(); 65 | 66 | return CommentCardResponse.builder() 67 | .presence(comment.isPresence()) 68 | .parentId(comment.isParent() ? null : comment.getParent().getId()) 69 | .childCount(comment.isParent() ? comment.getChildList().size() : 0) // 대댓글의 경우 항상 0 70 | .id(comment.getId()) 71 | .content(comment.getContent()) 72 | .createdAt(CustomDateUtils.customDateFormat(comment.getCreatedAt(), GlobalConstants.DATE_FORMAT_YYYY_MM_DD_HH_MM)) 73 | .updatedAt(CustomDateUtils.customDateFormat(comment.getUpdatedAt(), GlobalConstants.DATE_FORMAT_YYYY_MM_DD_HH_MM)) 74 | .userId(author.getId()) 75 | .userNickname(PreventNullResponseUtils.resolveUserNicknameFromNullable(author.getNickname())) 76 | .userProfileImageUrl(PreventNullResponseUtils.resolveUserProfileImageUrlFromNullable(author.getProfileImageUrl())) 77 | .isAuthor(author.getId().equals(userId)) 78 | .build(); 79 | } else { 80 | return CommentCardResponse.builder() 81 | .parentId(comment.isParent() ? null : comment.getParent().getId()) 82 | .childCount(comment.isParent() ? comment.getChildList().size() : 0) // 대댓글의 경우 항상 0 83 | .id(comment.getId()) 84 | .presence(comment.isPresence()) 85 | .build(); 86 | } 87 | } 88 | } 89 | 90 | public record CommentMessage( 91 | boolean result 92 | ) { 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/exception/StatusCode.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum StatusCode { 9 | 10 | /** 11 | * Common 12 | */ 13 | INTERNAL_SERVER_ERROR(500, -1, "internal server error."), 14 | COMMON_BAD_REQUEST(400, 9000, ""), 15 | INVALID_INPUT_VALUE(400, 9010, "invalid input value."), 16 | METHOD_NOT_ALLOWED(405, 9020, "method not allowed."), 17 | HTTP_CLIENT_ERROR(400, 9030, "http client error."), 18 | INVALID_REQUEST_PARAM(400, 9100, "invalid request param."), 19 | NOT_FOUND_URL(404, 9110, "not found url request"), 20 | 21 | AWS_S3_UPLOAD_FAIL(400, 9040, "AWS S3 upload fail."), 22 | AWS_S3_DELETE_FAIL(400, 9050, "AWS S3 delete fail."), 23 | AWS_S3_FILE_SIZE_EXCEEDED(400, 9060, "exceeded file size."), 24 | AWS_S3_FILE_TYPE_INVALID(400, 9070, "invalid file type."), 25 | /** 26 | * User 27 | */ 28 | NOT_FOUND_USER(404, 2000, "not found user error."), 29 | NICKNAME_DUPLICATION(409, 2010, "duplicated nickname error."), 30 | BANNED_USER(403,2020,"banned user error"), 31 | BLACKLIST_BANNED_USER(403, 2030, "blacklist user error."), 32 | SOCIAL_TYPE_ERROR(400,2040,"invalid social type error."), 33 | REVOKE_USER(403,2050,"revoke user error"), 34 | NICKNAME_VALIDATE_ERROR(400,2060,"invalid nickname error"), 35 | BLOCK_ERROR(400,2070,"not found blockId error"), 36 | IS_BLOCKED_ERROR(400,2080,"isblocked error"), 37 | ROLE_ACCESS_ERROR(400,2090,"role access error"), 38 | /** 39 | * Auth 40 | */ 41 | // success 42 | LOGIN(200, 1001, "account exist, process login."), 43 | SIGNUP_COMPLETE(200, 1011, "signup complete, access token is issued."), 44 | 45 | // fail 46 | 47 | FILTER_ACCESS_DENIED(401, 1000, "access denied."), 48 | FILTER_ROLE_FORBIDDEN(403, 1010, "role forbidden."), 49 | SIGNUP_TOKEN_ERROR(400, 1020, "invalid sign up token error."), 50 | NOT_FOUND_REFRESH_TOKEN(404,1030,"not found refresh token"), 51 | NEED_TO_SIGNUP(404, 1040, "need to signup, X-ACCESS-TOKEN is issued."), 52 | ENCRYPTION_FAILURE(400,1050,"encryption failure"), 53 | DECRYPTION_FAILURE(400,1060,"decryption failure"), 54 | REVOKE_ERROR(404,1070,"revoke error"), 55 | IS_NOT_REFRESH(400, 1070, "this token is not refresh token."), 56 | EXPIRED_REFRESH(400,1080,"expired refresh token"), 57 | IS_NOT_CORRECT_REFRESH(400,1090,"this token is not correct refresh token"), 58 | 59 | // 권한 60 | NOT_AUTHOR(403, 1200, "not an author of this content."), 61 | 62 | /** 63 | * Location & Congestion 64 | */ 65 | CHOICE_NOT_EXIST(404, 5010, "not found choice."), 66 | NOT_FOUND_LOCATION(404, 5100, "not found location."), 67 | NOT_FOUND_CONGESTION(404, 5110, "not found congestion."), 68 | NOT_FOUND_DAILY_CONGESTION_STAT(404, 5120, "not found daily congestion statistics."), 69 | NOT_FOUND_WEEKLY_CONGESTION_STAT(404, 5130, "not found weekly congestion statistics."), 70 | NOT_FOUND_LOCATION_CATEGORY(404, 5600, "not found spot."), 71 | 72 | 73 | /** 74 | * Spot & Comment 75 | */ 76 | NOT_FOUND_SPOT(404, 5500, "not found spot."), 77 | NOT_FOUND_SPOT_CATEGORY(404, 5600, "not found spot category."), 78 | 79 | NOT_FOUND_COMMENT(404, 5700, "not found comment."), 80 | 81 | 82 | /** 83 | * report 84 | */ 85 | 86 | NOT_FOUND_REPORTED_USER(404, 3000,"not found reported user error."), 87 | REPORT_DUPLICATION(400, 3010, "duplicate report."), 88 | NOT_FOUND_REPORT_TARGET(404,3020,"report target not found."), 89 | ALREADY_BAN_USER(400,3030,"already ban user."), 90 | NOT_CORRECT_USER_AND_TARGET(404, 3040,"not correct writer and report target id"), 91 | OVER_CONTENT_LENGTH(400,3080,"limit of the number of words."), 92 | 93 | 94 | /** 95 | * Notification 96 | */ 97 | GET_FCM_ACCESS_TOKEN_ERROR(400,4500,"fcm access token get failed"), 98 | FCM_MESSAGE_JSON_PARSING_ERROR(400,4510,"fcm message json parsing failed"), 99 | SEND_FCM_PUSH_ERROR(400,4520,"send fcm push message failed"), 100 | NOT_FOUND_NOTIFICATION(404, 4530, "not found notification error"); 101 | 102 | 103 | private final int HttpCode; 104 | private final int statusCode; 105 | private final String message; 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/config/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.config.security; 2 | 3 | import bokjak.bokjakserver.common.constant.GlobalConstants; 4 | import bokjak.bokjakserver.config.jwt.JwtAccessDeniedHandler; 5 | import bokjak.bokjakserver.config.jwt.JwtAuthenticationEntryPoint; 6 | import bokjak.bokjakserver.config.jwt.JwtAuthenticationFilter; 7 | import bokjak.bokjakserver.config.jwt.JwtProvider; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.http.HttpMethod; 13 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 14 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 15 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 16 | import org.springframework.security.config.http.SessionCreationPolicy; 17 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 18 | import org.springframework.security.crypto.password.PasswordEncoder; 19 | import org.springframework.security.web.SecurityFilterChain; 20 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 21 | import org.springframework.web.cors.CorsConfiguration; 22 | import org.springframework.web.cors.CorsConfigurationSource; 23 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 24 | import org.springframework.web.filter.CorsFilter; 25 | 26 | import java.util.Arrays; 27 | import java.util.List; 28 | 29 | @RequiredArgsConstructor 30 | @Configuration 31 | @EnableWebSecurity 32 | @EnableMethodSecurity(securedEnabled = true) 33 | public class SecurityConfig { 34 | 35 | private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; 36 | private final JwtAccessDeniedHandler jwtAccessDeniedHandler; 37 | private final JwtProvider jwtProvider; 38 | 39 | @Bean 40 | public PasswordEncoder passwordEncoder() { 41 | return new BCryptPasswordEncoder(); 42 | } 43 | 44 | @Bean 45 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 46 | http 47 | .csrf().disable() 48 | .formLogin().disable() 49 | .httpBasic().disable() 50 | .cors().configurationSource(corsConfigurationSource()) 51 | .and() 52 | 53 | .exceptionHandling() 54 | .authenticationEntryPoint(jwtAuthenticationEntryPoint) 55 | .accessDeniedHandler(jwtAccessDeniedHandler) 56 | .and() 57 | 58 | //세션 사용 안함 59 | .sessionManagement() 60 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 61 | .and() 62 | 63 | .authorizeHttpRequests() 64 | .requestMatchers(GlobalConstants.AUTH_WHITELIST).permitAll() 65 | .anyRequest().authenticated() 66 | 67 | .and() 68 | .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); 69 | 70 | return http.build(); 71 | } 72 | 73 | @Bean 74 | public JwtAuthenticationFilter jwtAuthenticationFilter() { 75 | return new JwtAuthenticationFilter(jwtProvider); 76 | } 77 | 78 | @Bean 79 | public CorsConfigurationSource corsConfigurationSource() { //다시 알아볼것.. 80 | 81 | CorsConfiguration configuration = new CorsConfiguration(); 82 | configuration.setAllowCredentials(true); 83 | configuration.setAllowedOrigins(List.of("*")); 84 | configuration.setAllowedMethods( 85 | Arrays.asList(HttpMethod.POST.name(), HttpMethod.GET.name(), 86 | HttpMethod.PUT.name(), HttpMethod.DELETE.name(), 87 | HttpMethod.OPTIONS.name()) 88 | ); 89 | configuration.setAllowedHeaders(List.of("*")); 90 | 91 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 92 | source.registerCorsConfiguration("/**", configuration); 93 | FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); 94 | bean.setOrder(0); 95 | return source; 96 | } 97 | 98 | } 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/common/dummy/CongestionDummy.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.common.dummy; 2 | 3 | import bokjak.bokjakserver.domain.congestion.model.Congestion; 4 | import bokjak.bokjakserver.domain.congestion.model.DailyCongestionStatistic; 5 | import bokjak.bokjakserver.domain.congestion.model.WeeklyCongestionStatistic; 6 | import bokjak.bokjakserver.domain.congestion.repository.CongestionRepository; 7 | import bokjak.bokjakserver.domain.congestion.repository.DailyCongestionStatisticRepository; 8 | import bokjak.bokjakserver.domain.congestion.repository.WeeklyCongestionStatisticRepository; 9 | import bokjak.bokjakserver.domain.location.model.Location; 10 | import bokjak.bokjakserver.domain.location.repository.LocationRepository; 11 | import jakarta.annotation.PostConstruct; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.context.annotation.DependsOn; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.time.LocalDateTime; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | @Slf4j 23 | @Component("congestionDummy") 24 | @DependsOn({"userDummy", "locationDummy"}) 25 | @RequiredArgsConstructor 26 | @BuzzingDummy 27 | public class CongestionDummy { 28 | // 개발용. 배포 이후엔 Django 혼잡도 서버에 의존 29 | private final CongestionRepository congestionRepository; 30 | private final LocationRepository locationRepository; 31 | private final DailyCongestionStatisticRepository dailyCongestionStatisticRepository; 32 | private final WeeklyCongestionStatisticRepository weeklyCongestionStatisticRepository; 33 | 34 | @PostConstruct 35 | public void init() { 36 | if (congestionRepository.count() > 0) { 37 | log.info("[congestionDummy] 혼잡도 데이터가 이미 존재"); 38 | } else { 39 | createCongestions(); 40 | createCongestionStatistics(); 41 | log.info("[congestionDummy] 혼잡도 더미 생성 완료"); 42 | } 43 | } 44 | 45 | private void createCongestions() { 46 | for (Location location : locationRepository.findAll()) { 47 | for (int k = 1; k <= 3; k++) { 48 | LocalDateTime now = LocalDateTime.now(); 49 | 50 | for (int j = 0; j <= 13; j++) { // 13일간의 데이터 51 | LocalDateTime observedAt = now.minusDays(j).plusHours(k); 52 | 53 | Congestion congestion = Congestion.builder() 54 | .location(location) 55 | .congestionLevel((int) (Math.random() * 100) % 3 + 1)// congestion level 랜덤 3가지 56 | .observedAt(observedAt) // 인위적으로 시간 다르게 설정 57 | .build(); 58 | 59 | congestionRepository.save(congestion); 60 | } 61 | } 62 | } 63 | } 64 | 65 | private void createCongestionStatistics() { 66 | List allLocations = locationRepository.findAll(); 67 | for (Location location : allLocations) {// 모든 로케이션에 대해 68 | // daily 69 | for (int i = 0; i <= 28; i++) { // 28일간의 데이터 70 | ArrayList> list = new ArrayList<>(); 71 | LocalDateTime pastCreatedAt = LocalDateTime.now().minusDays(i + 1); 72 | 73 | 74 | for (int j = 9; j <= 24; j++) {// 9~24시 75 | list.add(Map.of( 76 | "time", j, 77 | "congestionLevel", (int) (Math.random() * 100) % 3 + 1 78 | )); 79 | } 80 | 81 | dailyCongestionStatisticRepository.save( 82 | DailyCongestionStatistic.builder() 83 | .location(location) 84 | .content(Map.of("statistics", list)) 85 | .createdAt(pastCreatedAt) 86 | .build() 87 | ); 88 | } 89 | 90 | // weekly 91 | for (int i = 0; i < 5; i++) {// 5주간의 데이터 92 | LocalDateTime pastCreatedAt = LocalDateTime.now().minusWeeks(i + 1); 93 | 94 | weeklyCongestionStatisticRepository.save( 95 | WeeklyCongestionStatistic.builder() 96 | .location(location) 97 | .averageCongestionLevel((float) ((Math.random() * 100) % 3 + 1)) 98 | .createdAt(pastCreatedAt) 99 | .build() 100 | ); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Java Gradle CI/CD Pipeline using Docker Hub 9 | 10 | ## trigger : 해당 브랜치에 push할 때, 해당 브랜치를 base로 한 PR이 merge될 때 11 | on: 12 | push: 13 | branches: [ "master" ] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | ## JDK 버전 : 17 25 | - uses: actions/checkout@v3 26 | - name: Set up JDK 17 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: '17' 30 | distribution: 'adopt' 31 | 32 | ## 빌드 시간 단축용 gradle 캐싱 33 | - name: Gradle Caching 34 | uses: actions/cache@v3 35 | with: 36 | path: | 37 | ~/.gradle/caches 38 | ~/.gradle/wrapper 39 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 40 | restore-keys: | 41 | ${{ runner.os }}-gradle- 42 | 43 | ## yml 파일 생성 44 | - name: Set up yml file 45 | env: 46 | YAML_SECRET: ${{ secrets.APPLICATION_PROD_YML }} 47 | YAML_DIR: src/main/resources 48 | YAML_FILE_NAME: application-prod.yml 49 | run: echo $YAML_SECRET | base64 --decode > $YAML_DIR/$YAML_FILE_NAME 50 | 51 | ## FCM 설정 파일 셋업 52 | - name: Set up Json file 53 | env: 54 | JSON_SECRET: ${{ secrets.FIREBASE_PRIVATE_KEY_JSON }} 55 | JSON_DIR: src/main/resources 56 | JSON_FILE_NAME: buzzzzing-firebase-private-key.json 57 | run: echo $JSON_SECRET | base64 --decode > $JSON_DIR/$JSON_FILE_NAME 58 | 59 | ## gradlew 권한 부여 및 빌드 60 | - name: Grant execute permission for gradlew 61 | run: chmod +x gradlew 62 | - name: Build with Gradle 63 | run: ./gradlew bootJar 64 | 65 | ## 이미지 생성 -> 도커 허브에 Push 66 | - name: Docker build 67 | run: | 68 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 69 | docker build -t ${{ secrets.DOCKER_USERNAME }}/buzzing-server . 70 | docker tag ${{ secrets.DOCKER_USERNAME }}/buzzing-server ${{ secrets.DOCKER_USERNAME }}/buzzing-server:${GITHUB_SHA::7} 71 | docker push ${{ secrets.DOCKER_USERNAME }}/buzzing-server:${GITHUB_SHA::7} # Github에서 기본적으로 제공하는 환경변수 commit SHA 72 | 73 | ## 도커 허브의 이미지를 리눅스 인스턴스에 받아와서 run 74 | - name: Deploy 75 | uses: appleboy/ssh-action@master 76 | with: 77 | host: ${{ secrets.EC2_IP }} 78 | username: ubuntu 79 | key: ${{ secrets.EC2_PEM_KEY }} 80 | envs: GITHUB_SHA 81 | script: | 82 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 83 | docker pull ${{ secrets.DOCKER_USERNAME }}/buzzing-server:${GITHUB_SHA::7} 84 | docker tag ${{ secrets.DOCKER_USERNAME }}/buzzing-server:${GITHUB_SHA::7} buzzing-server 85 | docker rm -f $(docker ps -qa) 86 | docker run -d --name server -e TZ=Asia/Seoul -p 8080:8090 buzzing-server 87 | 88 | ## 서버 헬스 체크 89 | TARGET_URL=localhost PORT=8080 90 | 91 | for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do 92 | echo "> #${RETRY_COUNT} trying..." 93 | RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" $TARGET_URL:$PORT/hello) 94 | 95 | if [ "${RESPONSE_CODE}" -eq 200 ]; then 96 | echo "> New WAS successfully running" 97 | exit 0 98 | elif [ ${RETRY_COUNT} -eq 10 ]; then 99 | echo "> Health check failed." 100 | exit 1 101 | fi 102 | sleep 10 103 | done 104 | 105 | ## 배포 결과를 슬랙 채널에 전송 106 | - name: action-slack # https://github.com/8398a7/action-slack/blob/v2/README.md 107 | uses: 8398a7/action-slack@v3 108 | with: 109 | status: ${{ job.status }} 110 | author_name: Buzzzzing-Server 111 | fields: repo,message,commit,author,action,eventName,ref,workflow,job,took 112 | if_mention: failure,cancelled 113 | env: 114 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required 115 | if: always() # Pick up events even if the job fails or is canceled. 116 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/notification/service/FcmService.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.notification.service; 2 | 3 | import bokjak.bokjakserver.common.exception.StatusCode; 4 | import bokjak.bokjakserver.domain.notification.dto.FcmDto; 5 | import bokjak.bokjakserver.domain.notification.dto.FcmDto.FcmMessage; 6 | import bokjak.bokjakserver.domain.notification.dto.FcmDto.Message; 7 | import bokjak.bokjakserver.domain.notification.dto.NotificationDto; 8 | import bokjak.bokjakserver.domain.notification.exception.NotificationException; 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import com.google.auth.oauth2.GoogleCredentials; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import okhttp3.*; 15 | import org.json.simple.JSONObject; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.core.io.ClassPathResource; 18 | import org.springframework.scheduling.annotation.Async; 19 | import org.springframework.stereotype.Service; 20 | import org.json.simple.parser.JSONParser; 21 | 22 | import java.io.IOException; 23 | import java.util.List; 24 | import java.util.concurrent.CompletableFuture; 25 | 26 | 27 | @Slf4j 28 | @Service 29 | @RequiredArgsConstructor 30 | public class FcmService { 31 | 32 | private final ObjectMapper objectMapper; 33 | private final JSONParser jsonParser; 34 | 35 | 36 | private static String FCM_PRIVATE_KEY_PATH = "buzzzzing-firebase-private-key.json"; 37 | private static String fireBaseScope = "https://www.googleapis.com/auth/cloud-platform"; 38 | private static String PROJECT_ID_URL = "https://fcm.googleapis.com/v1/projects/buzzzzing-c258e/messages:send"; 39 | 40 | 41 | private String getAccessToken() { 42 | try { 43 | String firebaseConfigPath = FCM_PRIVATE_KEY_PATH; 44 | GoogleCredentials credentials = GoogleCredentials 45 | .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) 46 | .createScoped(List.of(fireBaseScope)); 47 | credentials.refreshIfExpired(); 48 | return credentials.getAccessToken().getTokenValue(); 49 | } catch (IOException e) { 50 | log.warn("FCM getAccessToken Error : {}", e.getMessage()); 51 | throw new NotificationException(StatusCode.GET_FCM_ACCESS_TOKEN_ERROR); 52 | } 53 | } 54 | 55 | public String makeMessage(String targetToken, NotificationDto.NotifyParams params) { 56 | try { 57 | FcmMessage fcmMessage = new FcmMessage( 58 | false, 59 | new Message( 60 | targetToken, 61 | new FcmDto.Notification( 62 | params.title(), 63 | params.content(), 64 | String.valueOf(params.redirectTargetId()), 65 | params.type().toString() 66 | ) 67 | ) 68 | ); 69 | return objectMapper.writeValueAsString(fcmMessage); 70 | } catch (JsonProcessingException e) { 71 | log.warn("FCM [makeMessage] Error : {}", e.getMessage()); 72 | throw new NotificationException(StatusCode.FCM_MESSAGE_JSON_PARSING_ERROR); 73 | } 74 | } 75 | 76 | @Async(value = "AsyncBean") 77 | public CompletableFuture sendPushMessage(String fcmToken, NotificationDto.NotifyParams params) { 78 | String message = makeMessage(fcmToken, params); 79 | String accessToken = getAccessToken(); 80 | OkHttpClient client = new OkHttpClient(); 81 | Request request = new Request.Builder() 82 | .url(PROJECT_ID_URL) 83 | .addHeader("Authorization", "Bearer " + accessToken) 84 | .addHeader("Content-Type", "application/json; UTF-8") 85 | .post(RequestBody.create(message, MediaType.parse("application/json; charset=urf-8"))) 86 | .build(); 87 | try (Response response = client.newCall(request).execute()){ 88 | if (!response.isSuccessful() && response.body() != null) { 89 | JSONObject responseBody = (JSONObject) jsonParser.parse(response.body().string()); 90 | String errorMessage = ((JSONObject) responseBody.get("error")).get("message").toString(); 91 | log.warn("FCM [sendPushMessage] okHttp response is not OK : {}", errorMessage); 92 | return CompletableFuture.completedFuture(false); 93 | } 94 | return CompletableFuture.completedFuture(true); 95 | } catch (Exception e) { 96 | log.warn("FCM [sendPushMessage] I/O Exception : {}", e.getMessage()); 97 | throw new NotificationException(StatusCode.SEND_FCM_PUSH_ERROR); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/bokjak/bokjakserver/domain/location/dto/LocationDto.java: -------------------------------------------------------------------------------- 1 | package bokjak.bokjakserver.domain.location.dto; 2 | 3 | import bokjak.bokjakserver.domain.congestion.dto.CongestionDto.CongestionPrediction; 4 | import bokjak.bokjakserver.domain.congestion.model.Congestion; 5 | import bokjak.bokjakserver.domain.congestion.model.CongestionLevel; 6 | import bokjak.bokjakserver.domain.location.model.Location; 7 | import lombok.Builder; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | public class LocationDto { 12 | /** 13 | * Response 14 | */ 15 | @Builder 16 | public record LocationCardResponse( 17 | Long id, 18 | String name, 19 | Long categoryId, 20 | String categoryName, 21 | String categoryIconUrl, 22 | boolean isBookmarked, 23 | CongestionLevel congestionSymbol, 24 | int congestionLevel, 25 | int bookMarkCount 26 | 27 | ) { 28 | public static LocationCardResponse of( 29 | Location location, 30 | boolean isBookmarked 31 | ) { 32 | return LocationCardResponse.builder() 33 | .id(location.getId()) 34 | .name(location.getName()) 35 | .categoryId(location.getLocationCategory().getId()) 36 | .categoryName(location.getLocationCategory().getName()) 37 | .categoryIconUrl(location.getLocationCategory().getIconImageUrl()) 38 | .congestionSymbol(CongestionLevel.toEnum(location.getRealtimeCongestionLevel())) 39 | .congestionLevel(location.getRealtimeCongestionLevel()) 40 | .bookMarkCount(location.getLocationBookmarkList().size()) 41 | .isBookmarked(isBookmarked) 42 | .build(); 43 | } 44 | } 45 | 46 | @Builder 47 | public record LocationSimpleCardResponse( 48 | Long id, 49 | String name, 50 | Long categoryId, 51 | String categoryName, 52 | String categoryIconUrl 53 | ) { 54 | public static LocationSimpleCardResponse of(Location location) { 55 | return LocationSimpleCardResponse.builder() 56 | .id(location.getId()) 57 | .name(location.getName()) 58 | .categoryId(location.getLocationCategory().getId()) 59 | .categoryName(location.getLocationCategory().getName()) 60 | .categoryIconUrl(location.getLocationCategory().getIconImageUrl()) 61 | .build(); 62 | } 63 | } 64 | 65 | @Builder 66 | public record LocationDetailResponse( 67 | Long id, 68 | String name, 69 | Long categoryId, 70 | String categoryName, 71 | int bookMarkCount, 72 | boolean isBookmarked, 73 | CongestionLevel congestionSymbol, 74 | int congestionLevel, 75 | Long congestionId, 76 | LocalDateTime observedAt, 77 | Integer mayRelaxAt, 78 | Integer mayRelaxUntil, 79 | Integer mayBuzzAt, 80 | Integer mayBuzzUntil 81 | ) { 82 | public static LocationDetailResponse of( 83 | Location location, 84 | Congestion congestion, 85 | CongestionPrediction congestionPrediction, 86 | boolean isBookmarked 87 | ) { 88 | return LocationDetailResponse.builder() 89 | .id(location.getId()) 90 | .name(location.getName()) 91 | .categoryId(location.getLocationCategory().getId()) 92 | .categoryName(location.getLocationCategory().getName()) 93 | .bookMarkCount(location.getLocationBookmarkList().size()) 94 | .isBookmarked(isBookmarked) 95 | // TODO: congestion... -> location.getRealtimeCongestionLevel. V2로 해보자 96 | .congestionSymbol(CongestionLevel.toEnum(congestion.getCongestionLevel())) 97 | .congestionLevel(congestion.getCongestionLevel()) 98 | .congestionId(congestion.getId()) 99 | .observedAt(congestion.getObservedAt()) 100 | .mayRelaxAt(congestionPrediction.getMayRelaxAt()) 101 | .mayRelaxUntil(congestionPrediction.getMayRelaxUntil()) 102 | .mayBuzzAt(congestionPrediction.getMayBuzzAt()) 103 | .mayBuzzUntil(congestionPrediction.getMayBuzzUntil()) 104 | .build(); 105 | } 106 | } 107 | 108 | @Builder 109 | public record BookmarkResponse( 110 | Long locationId, 111 | boolean isBookmarked 112 | ) { 113 | public static BookmarkResponse of(Long locationId, boolean isBookmarked) { 114 | return BookmarkResponse.builder() 115 | .locationId(locationId) 116 | .isBookmarked(isBookmarked) 117 | .build(); 118 | } 119 | } 120 | } 121 | --------------------------------------------------------------------------------