diff --git a/.github/workflows/cd_dev.yml b/.github/workflows/cd_dev.yml index 89d7d77a..6b04f311 100644 --- a/.github/workflows/cd_dev.yml +++ b/.github/workflows/cd_dev.yml @@ -36,6 +36,8 @@ jobs: echo "APPLE_TEAM_ID=${{ secrets.APPLE_TEAM_ID }}" >> .env echo "APPLE_KEY_ID=${{ secrets.APPLE_KEY_ID }}" >> .env echo "APPLE_PRIVATE_KEY=${{ secrets.APPLE_PRIVATE_KEY }}" >> .env + echo "NEED_UPDATE_VERSION=${{ secrets.NEED_UPDATE_VERSION }}" >> .env + echo "RECOMMEND_UPDATE=${{ secrets.RECOMMEND_UPDATE }}" >> .env echo "SPRING_PROFILES_ACTIVE=dev" >> .env diff --git a/.github/workflows/cd_prod.yml b/.github/workflows/cd_prod.yml index dd564308..2c73f61c 100644 --- a/.github/workflows/cd_prod.yml +++ b/.github/workflows/cd_prod.yml @@ -36,6 +36,8 @@ jobs: echo "APPLE_TEAM_ID=${{ secrets.APPLE_TEAM_ID }}" >> .env echo "APPLE_KEY_ID=${{ secrets.APPLE_KEY_ID }}" >> .env echo "APPLE_PRIVATE_KEY=${{ secrets.APPLE_PRIVATE_KEY }}" >> .env + echo "NEED_UPDATE_VERSION=${{ secrets.NEED_UPDATE_VERSION_PROD }}" >> .env + echo "RECOMMEND_UPDATE=${{ secrets.RECOMMEND_UPDATE_VERSION_PROD }}" >> .env - name: gradlew에 실행 권한 부여 run: chmod +x ./gradlew diff --git a/README.md b/README.md new file mode 100644 index 00000000..b64d5702 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ + +## GroundFlip - 땅따먹기 게임 기반 걷기 앱 +image + +### 👀 프로젝트 소개 +내가 가는 길이 내 것이 되는 즐거움, 그라운드 플립!
+운동이 재미가 없어 꾸준히 하기 어려웠다구요? + +그라운드 플립과 함께 걸어봐요! + +그라운드 플립은 땅따먹기 게임에서 아이디어를 얻어 현대인들의 운동 부족을 해결하기 위해 제작되었습니다.
+사용자는 지도 상에 존재하는 수많은 픽셀들을 걸어나가며 차지할 수 있습니다.
+실시간 랭킹, 그룹 기능을 제공합니다.
+ +--- +### 개발 환경 +- Java 17 +- Spring Boot 3.3.0 + +--- + +### 기술 세부 스택 +- Spring Boot + +- Spring Data JPA +- Spring Data Redis +- Spring Security +그 외 + +--- + +### 📚 기능 소개
+BE Architecture
+image + +ERD
+image diff --git a/deploy.sh b/deploy.sh index 2bb4465e..0ea633b0 100644 --- a/deploy.sh +++ b/deploy.sh @@ -3,24 +3,56 @@ APP_NAME="ground_flip" REPOSITORY=/home/ubuntu/ground_flip -echo "> Check the currently running container" -CONTAINER_ID=$(docker ps -aqf "name=$APP_NAME") -pwd -ls -if [ -z "$CONTAINER_ID" ]; -then - echo "> No such container is running." +echo "> Build Docker image" +sudo docker build -t "$APP_NAME" "$REPOSITORY" + +TARGET_PORT=0 +CURRENT_PORT=$(sudo docker ps --filter "name=$APP_NAME" --format "{{.Ports}}" | cut -d: -f2 | cut -d- -f1) +echo "> CURRENT_PORT = $CURRENT_PORT" + +if [ "$CURRENT_PORT" == "8081" ]; then + TARGET_PORT=8082 else - echo "> Stop and remove container: $CONTAINER_ID" - docker stop "$CONTAINER_ID" - docker rm "$CONTAINER_ID" + TARGET_PORT=8081 fi +echo "> TARGET_PORT = $TARGET_PORT" + + +NEW_CONTAINER_NAME="$APP_NAME-$TARGET_PORT" +OLD_CONTAINER_NAME="$APP_NAME-$CURRENT_PORT" + +echo "> Run the Docker container on port $TARGET_PORT" +sudo docker run -d -p $TARGET_PORT:8080 --env-file /home/ubuntu/ground_flip/.env -e TZ=Asia/Seoul -v /home/ubuntu/logs:/logs --name "$NEW_CONTAINER_NAME" "$APP_NAME" + +for cnt in {1..10} # 10번 실행 +do + echo "check server start.." + + RESPONSE=$(curl -s http://127.0.0.1:$TARGET_PORT/check) + + if echo "$RESPONSE" | grep -q "success"; then + echo "Container Started" + break; + else + echo "server not start.." + fi + + echo "wait 10 seconds" # 10 초간 대기 + sleep 10 +done + +echo "> Update NGINX configuration to route traffic to the new container" +NGINX_CONF="/etc/nginx/nginx.conf" +sudo sed -i "s/$CURRENT_PORT/$TARGET_PORT/g" "$NGINX_CONF" + +echo "> Reload NGINX to apply the new configuration" +sudo nginx -s reload + +echo "> Remove Old Container" +sudo docker rm -f $OLD_CONTAINER_NAME echo "> Remove previous Docker image" -docker rmi "$APP_NAME" +sudo docker image prune -f -echo "> Build Docker image" -docker build -t "$APP_NAME" "$REPOSITORY" +echo "> Deployment to port $TARGET_PORT completed successfully." -echo "> Run the Docker container" -docker run -d -p 8080:8080 --env-file /home/ubuntu/ground_flip/.env -e TZ=Asia/Seoul -v /home/ubuntu/logs:/logs --name "$APP_NAME" "$APP_NAME" diff --git a/src/main/java/com/m3pro/groundflip/config/SecurityConfig.java b/src/main/java/com/m3pro/groundflip/config/SecurityConfig.java index f2c582a8..ff318841 100644 --- a/src/main/java/com/m3pro/groundflip/config/SecurityConfig.java +++ b/src/main/java/com/m3pro/groundflip/config/SecurityConfig.java @@ -31,6 +31,7 @@ protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Except .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/version").permitAll() .requestMatchers("/v3/api-docs/**").permitAll() .requestMatchers("/api/swagger-ui/**").permitAll() .requestMatchers("/api/docs/**").permitAll() diff --git a/src/main/java/com/m3pro/groundflip/controller/MyPlaceController.java b/src/main/java/com/m3pro/groundflip/controller/MyPlaceController.java new file mode 100644 index 00000000..3627d69e --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/controller/MyPlaceController.java @@ -0,0 +1,59 @@ +package com.m3pro.groundflip.controller; + +import java.util.List; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.m3pro.groundflip.domain.dto.Response; +import com.m3pro.groundflip.domain.dto.myplace.MyPlaceRequest; +import com.m3pro.groundflip.domain.dto.myplace.MyPlaceResponse; +import com.m3pro.groundflip.service.MyPlaceService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/myplace") +@Tag(name = "myplace", description = "즐겨찾기 API") +public class MyPlaceController { + private final MyPlaceService myPlaceService; + + @Operation(summary = "사용자 즐겨찾기 등록", description = "장소의 좌표, 이름을 저장한다") + @PutMapping("") + public Response putMyPlace( + @Parameter(description = "즐겨찾기 저장 userId", required = true) + @RequestBody MyPlaceRequest myPlaceRequest + ) { + myPlaceService.putMyPlace(myPlaceRequest); + return Response.createSuccessWithNoData(); + } + + @Operation(summary = "사용자 즐겨찾기 get", description = "즐겨찾기 장소의 좌표, 이름을 가져온다") + @GetMapping("/{userId}") + public Response> getMyPlace( + @Parameter(description = "찾고자 하는 userId", required = true) + @PathVariable Long userId + ) { + return Response.createSuccess(myPlaceService.getMyPlace(userId)); + } + + @Operation(summary = "사용자 즐겨찾기 delete", description = "즐겨찾기 장소를 삭제한다") + @DeleteMapping("") + public Response deleteMyPlace( + @Parameter(description = "지우고자 하는 userId", required = true) + @RequestBody MyPlaceRequest myPlaceRequest + ) { + myPlaceService.deleteMyPlace(myPlaceRequest); + return Response.createSuccessWithNoData(); + } + +} diff --git a/src/main/java/com/m3pro/groundflip/controller/PermissionController.java b/src/main/java/com/m3pro/groundflip/controller/PermissionController.java new file mode 100644 index 00000000..4d2e2f03 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/controller/PermissionController.java @@ -0,0 +1,50 @@ +package com.m3pro.groundflip.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.m3pro.groundflip.domain.dto.Response; +import com.m3pro.groundflip.domain.dto.permission.PermissionRequest; +import com.m3pro.groundflip.domain.dto.permission.PermissionResponse; +import com.m3pro.groundflip.service.PermissionService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@Slf4j +@RequestMapping("/api/permission") +@Tag(name = "permission", description = "권한 동의 API") +@SecurityRequirement(name = "Authorization") +public class PermissionController { + private final PermissionService permissionService; + + @Operation(summary = "유저가 동의한 권한 조회", description = "유저가 동의한 권한을 조회하여 응답한다.") + @GetMapping("/{userId}") + public Response getPermission(@PathVariable("userId") Long userId) { + return Response.createSuccess(permissionService.getAllPermissions(userId)); + } + + @Operation(summary = "서비스 알림 권한 동의", description = "서비스 푸시 알림을 받을지 받지 않을지 선택하는 api 이다.") + @PutMapping("/service-notification") + public Response updateServiceNotification(@RequestBody PermissionRequest permissionRequest) { + permissionService.updateServiceNotificationsPreference(permissionRequest); + return Response.createSuccessWithNoData(); + } + + @Operation(summary = "마케팅 알림 권한 동의", description = "마케팅 푸시 알림을 받을지 받지 않을지 선택하는 api 이다.") + @PutMapping("/marketing-notification") + public Response updateMarketingNotification(@RequestBody PermissionRequest permissionRequest) { + permissionService.updateMarketingNotificationsPreference(permissionRequest); + return Response.createSuccessWithNoData(); + } + +} diff --git a/src/main/java/com/m3pro/groundflip/controller/UserController.java b/src/main/java/com/m3pro/groundflip/controller/UserController.java index e7e7c948..b360b08c 100644 --- a/src/main/java/com/m3pro/groundflip/controller/UserController.java +++ b/src/main/java/com/m3pro/groundflip/controller/UserController.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,9 +14,11 @@ import org.springframework.web.multipart.MultipartFile; import com.m3pro.groundflip.domain.dto.Response; +import com.m3pro.groundflip.domain.dto.user.FcmTokenRequest; import com.m3pro.groundflip.domain.dto.user.UserDeleteRequest; import com.m3pro.groundflip.domain.dto.user.UserInfoRequest; import com.m3pro.groundflip.domain.dto.user.UserInfoResponse; +import com.m3pro.groundflip.service.FcmService; import com.m3pro.groundflip.service.UserService; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +34,7 @@ @SecurityRequirement(name = "Authorization") public class UserController { private final UserService userService; + private final FcmService fcmService; @Operation(summary = "사용자 기본 정보 조회", description = "닉네임, id, 출생년도, 성별, 프로필 사진, 그룹이름, 그룹 id 를 조회 한다.") @GetMapping("/{userId}") @@ -62,4 +66,13 @@ public Response putUserInfo( userService.deleteUser(userId, userDeleteRequest); return Response.createSuccessWithNoData(); } + + @Operation(summary = "FCM 등록 토큰 등록", description = "푸시 알림을 위한 FCM 등록 토큰을 저장한다.") + @PostMapping("/fcm-token") + public Response postFcmToken( + @RequestBody FcmTokenRequest fcmTokenRequest + ) { + fcmService.registerFcmToken(fcmTokenRequest); + return Response.createSuccessWithNoData(); + } } diff --git a/src/main/java/com/m3pro/groundflip/controller/VersionController.java b/src/main/java/com/m3pro/groundflip/controller/VersionController.java new file mode 100644 index 00000000..86726b87 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/controller/VersionController.java @@ -0,0 +1,32 @@ +package com.m3pro.groundflip.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.m3pro.groundflip.domain.dto.Response; +import com.m3pro.groundflip.domain.dto.version.VersionResponse; +import com.m3pro.groundflip.service.VersionService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +@Tag(name = "version", description = "앱 버전 API") +public class VersionController { + private final VersionService versionService; + + @Operation(summary = "버전 업데이트 필요 여부 확인", description = "현재 앱 버전 업데이트가 필요한지 판별") + @GetMapping("/version") + public Response getVersion( + @Parameter(description = "현재 버전", required = true) + @RequestParam String currentVersion + ) { + return Response.createSuccess(versionService.getVersion(currentVersion)); + } +} diff --git a/src/main/java/com/m3pro/groundflip/domain/dto/myplace/MyPlaceRequest.java b/src/main/java/com/m3pro/groundflip/domain/dto/myplace/MyPlaceRequest.java new file mode 100644 index 00000000..edc195f7 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/dto/myplace/MyPlaceRequest.java @@ -0,0 +1,30 @@ +package com.m3pro.groundflip.domain.dto.myplace; + +import com.m3pro.groundflip.enums.Place; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(title = "사용자 즐겨찾기 정보 등록") +public class MyPlaceRequest { + + @Schema(description = "유저 id", example = "1") + private Long userId; + + @Schema(description = "즐겨찾기 장소 이름", example = "학교") + private Place placeName; + + @Schema(description = "위도", example = "37.321147") + private double latitude; + + @Schema(description = "경도", example = "127.093171") + private double longitude; + +} diff --git a/src/main/java/com/m3pro/groundflip/domain/dto/myplace/MyPlaceResponse.java b/src/main/java/com/m3pro/groundflip/domain/dto/myplace/MyPlaceResponse.java new file mode 100644 index 00000000..e2bfa0a6 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/dto/myplace/MyPlaceResponse.java @@ -0,0 +1,37 @@ +package com.m3pro.groundflip.domain.dto.myplace; + +import org.locationtech.jts.geom.Point; + +import com.m3pro.groundflip.domain.entity.MyPlace; +import com.m3pro.groundflip.enums.Place; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(title = "사용자 즐겨찾기 정보 검색") +public class MyPlaceResponse { + + @Schema(description = "즐겨찾기 id", example = "1") + private Long id; + + @Schema(description = "즐겨찾기 장소 이름", example = "학교") + private Place placeName; + + @Schema(description = "즐겨찾기 장소 좌표", example = "0xE6100000010100000...") + private Point placePoint; + + public static MyPlaceResponse from(MyPlace myplace) { + return MyPlaceResponse.builder() + .id(myplace.getId()) + .placeName(myplace.getPlaceName()) + .placePoint(myplace.getPlacePoint()) + .build(); + } +} diff --git a/src/main/java/com/m3pro/groundflip/domain/dto/permission/PermissionRequest.java b/src/main/java/com/m3pro/groundflip/domain/dto/permission/PermissionRequest.java new file mode 100644 index 00000000..406f61a0 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/dto/permission/PermissionRequest.java @@ -0,0 +1,24 @@ +package com.m3pro.groundflip.domain.dto.permission; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@Schema(title = "권한 동의 Body") +public class PermissionRequest { + @Schema(description = "사용자 ID", example = "5") + private Long userId; + + @Schema(description = "권한 동의 여부", example = "true") + @JsonProperty("isEnabled") + private boolean isEnabled; + +} diff --git a/src/main/java/com/m3pro/groundflip/domain/dto/permission/PermissionResponse.java b/src/main/java/com/m3pro/groundflip/domain/dto/permission/PermissionResponse.java new file mode 100644 index 00000000..e302a409 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/dto/permission/PermissionResponse.java @@ -0,0 +1,20 @@ +package com.m3pro.groundflip.domain.dto.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +@Schema(title = "권한 정보 body") +public class PermissionResponse { + @Schema(description = "서비스 알림 동의 여부", example = "true") + private boolean isServiceNotificationEnabled; + + @Schema(description = "마케팅 알림 동의 여부", example = "true") + private boolean isMarketingNotificationEnabled; +} diff --git a/src/main/java/com/m3pro/groundflip/domain/dto/user/FcmTokenRequest.java b/src/main/java/com/m3pro/groundflip/domain/dto/user/FcmTokenRequest.java new file mode 100644 index 00000000..ce6d2375 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/dto/user/FcmTokenRequest.java @@ -0,0 +1,25 @@ +package com.m3pro.groundflip.domain.dto.user; + +import com.m3pro.groundflip.enums.Device; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(title = "FCM 등록 토큰 저장") +public class FcmTokenRequest { + @Schema(description = "사용자 Id", example = "125") + private Long userId; + + @Schema(description = "사용자 fcm token", example = "sdfghweredasdvasdfq/weqwefs;dvsdghrthwdffevdrer") + private String fcmToken; + + @Schema(description = "사용자 기기 종류 (iOS, Android)", example = "iOS") + private Device device; +} diff --git a/src/main/java/com/m3pro/groundflip/domain/dto/version/VersionResponse.java b/src/main/java/com/m3pro/groundflip/domain/dto/version/VersionResponse.java new file mode 100644 index 00000000..a889fbd3 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/dto/version/VersionResponse.java @@ -0,0 +1,23 @@ +package com.m3pro.groundflip.domain.dto.version; + +import com.m3pro.groundflip.enums.Version; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(title = "앱 버전 get") +public class VersionResponse { + @Schema(description = "앱 버전", example = "1.0.5") + private String version; + + @Schema(description = "앱 버전", example = "1.0.5") + private Version needUpdate; + +} diff --git a/src/main/java/com/m3pro/groundflip/domain/entity/FcmToken.java b/src/main/java/com/m3pro/groundflip/domain/entity/FcmToken.java new file mode 100644 index 00000000..32118164 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/entity/FcmToken.java @@ -0,0 +1,51 @@ +package com.m3pro.groundflip.domain.entity; + +import com.m3pro.groundflip.domain.entity.global.BaseTimeEntity; +import com.m3pro.groundflip.enums.Device; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "fcm_token") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class FcmToken extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "fcm_token_id") + private Long id; + + private String token; + + @Enumerated(EnumType.STRING) + private Device device; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public void updateToken(String token) { + this.token = token; + } + + public void updateDevice(Device device) { + this.device = device; + } +} diff --git a/src/main/java/com/m3pro/groundflip/domain/entity/MyPlace.java b/src/main/java/com/m3pro/groundflip/domain/entity/MyPlace.java new file mode 100644 index 00000000..d4548167 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/entity/MyPlace.java @@ -0,0 +1,52 @@ +package com.m3pro.groundflip.domain.entity; + +import org.locationtech.jts.geom.Point; + +import com.m3pro.groundflip.enums.Place; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "my_place") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class MyPlace { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "my_place_id") + private Long id; + + @Enumerated(EnumType.STRING) + private Place placeName; + + private Point placePoint; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public void updatePlaceName(Place placeName) { + this.placeName = placeName; + } + + public void updatePlacePoint(Point placePoint) { + this.placePoint = placePoint; + } +} diff --git a/src/main/java/com/m3pro/groundflip/domain/entity/Permission.java b/src/main/java/com/m3pro/groundflip/domain/entity/Permission.java new file mode 100644 index 00000000..e10fc113 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/domain/entity/Permission.java @@ -0,0 +1,49 @@ +package com.m3pro.groundflip.domain.entity; + +import com.m3pro.groundflip.domain.entity.global.BaseTimeEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "permission") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Permission extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "permission_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private Boolean serviceNotificationsEnabled; + + @Column(nullable = false) + private Boolean marketingNotificationsEnabled; + + public void updateServiceNotificationsEnabled(boolean enabled) { + serviceNotificationsEnabled = enabled; + } + + public void updateMarketingNotificationsEnabled(boolean enabled) { + marketingNotificationsEnabled = enabled; + } +} diff --git a/src/main/java/com/m3pro/groundflip/domain/entity/global/BaseTimeEntity.java b/src/main/java/com/m3pro/groundflip/domain/entity/global/BaseTimeEntity.java index abac1d16..83e0bf94 100644 --- a/src/main/java/com/m3pro/groundflip/domain/entity/global/BaseTimeEntity.java +++ b/src/main/java/com/m3pro/groundflip/domain/entity/global/BaseTimeEntity.java @@ -22,7 +22,11 @@ public class BaseTimeEntity { @LastModifiedDate private LocalDateTime modifiedAt; - public void updateModifiedAt() { + public void updateModifiedAtToNow() { modifiedAt = LocalDateTime.now(); } + + public void updateModifiedAt(LocalDateTime localDateTime) { + modifiedAt = localDateTime; + } } diff --git a/src/main/java/com/m3pro/groundflip/enums/Device.java b/src/main/java/com/m3pro/groundflip/enums/Device.java new file mode 100644 index 00000000..0ff56f9e --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/enums/Device.java @@ -0,0 +1,13 @@ +package com.m3pro.groundflip.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Device { + IOS("iOS"), + ANDROID("Android"); + + private final String device; +} diff --git a/src/main/java/com/m3pro/groundflip/enums/Place.java b/src/main/java/com/m3pro/groundflip/enums/Place.java new file mode 100644 index 00000000..d60ecfe9 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/enums/Place.java @@ -0,0 +1,14 @@ +package com.m3pro.groundflip.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Place { + HOME("집"), + COMPANY("회사/학교"), + ELSE("기타 장소"); + + private final String place; +} diff --git a/src/main/java/com/m3pro/groundflip/enums/Version.java b/src/main/java/com/m3pro/groundflip/enums/Version.java new file mode 100644 index 00000000..a4b6b812 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/enums/Version.java @@ -0,0 +1,14 @@ +package com.m3pro.groundflip.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Version { + OK("업데이트 불필요"), + NEED("업데이트 필요"), + FORCE("강제 업데이트"); + + private final String update; +} diff --git a/src/main/java/com/m3pro/groundflip/exception/ErrorCode.java b/src/main/java/com/m3pro/groundflip/exception/ErrorCode.java index 769faf31..692c3d3e 100644 --- a/src/main/java/com/m3pro/groundflip/exception/ErrorCode.java +++ b/src/main/java/com/m3pro/groundflip/exception/ErrorCode.java @@ -13,7 +13,9 @@ public enum ErrorCode { GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 그룹입니다."), PIXEL_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 픽셀입니다."), IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 이미지입니다."), + PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "장소가 등록되어 있지 않습니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러 입니다"), + VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "버전이 존재하지 않습니다."), // 권한 관련 에러 UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다"), diff --git a/src/main/java/com/m3pro/groundflip/jwt/JwtAuthorizationFilter.java b/src/main/java/com/m3pro/groundflip/jwt/JwtAuthorizationFilter.java index 08a482ad..131b0ae1 100644 --- a/src/main/java/com/m3pro/groundflip/jwt/JwtAuthorizationFilter.java +++ b/src/main/java/com/m3pro/groundflip/jwt/JwtAuthorizationFilter.java @@ -24,7 +24,8 @@ public class JwtAuthorizationFilter extends OncePerRequestFilter { "/api/docs", "/v3/api-docs", "/api/swagger-ui", - "/check" + "/check", + "/api/version" ); private static final List WHITE_LIST_TMP = List.of( "/api", diff --git a/src/main/java/com/m3pro/groundflip/repository/FcmTokenRepository.java b/src/main/java/com/m3pro/groundflip/repository/FcmTokenRepository.java new file mode 100644 index 00000000..b75cadee --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/repository/FcmTokenRepository.java @@ -0,0 +1,14 @@ +package com.m3pro.groundflip.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.m3pro.groundflip.domain.entity.FcmToken; +import com.m3pro.groundflip.domain.entity.User; + +public interface FcmTokenRepository extends JpaRepository { + Optional findByUser(User user); + + void deleteByUser(User user); +} diff --git a/src/main/java/com/m3pro/groundflip/repository/MyPlaceRepository.java b/src/main/java/com/m3pro/groundflip/repository/MyPlaceRepository.java new file mode 100644 index 00000000..af4500ce --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/repository/MyPlaceRepository.java @@ -0,0 +1,27 @@ +package com.m3pro.groundflip.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.m3pro.groundflip.domain.entity.MyPlace; +import com.m3pro.groundflip.enums.Place; + +public interface MyPlaceRepository extends JpaRepository { + + @Query(value = """ + SELECT mp.* + FROM my_place mp + JOIN ( + SELECT place_name, MAX(my_place_id) AS max_place_mark_id + FROM my_place + GROUP BY place_name + ) grouped_mp + ON mp.my_place_id = grouped_mp.max_place_mark_id + WHERE mp.user_id = :userId + """, nativeQuery = true) + List findByUserId(Long userId); + + List findByUserIdAndPlaceName(Long userId, Place placeName); +} diff --git a/src/main/java/com/m3pro/groundflip/repository/PermissionRepository.java b/src/main/java/com/m3pro/groundflip/repository/PermissionRepository.java new file mode 100644 index 00000000..44ec03a3 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/repository/PermissionRepository.java @@ -0,0 +1,12 @@ +package com.m3pro.groundflip.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.m3pro.groundflip.domain.entity.Permission; +import com.m3pro.groundflip.domain.entity.User; + +public interface PermissionRepository extends JpaRepository { + Optional findByUser(User user); +} diff --git a/src/main/java/com/m3pro/groundflip/repository/RankingRedisRepository.java b/src/main/java/com/m3pro/groundflip/repository/RankingRedisRepository.java index 0b8f4a12..98d71868 100644 --- a/src/main/java/com/m3pro/groundflip/repository/RankingRedisRepository.java +++ b/src/main/java/com/m3pro/groundflip/repository/RankingRedisRepository.java @@ -39,7 +39,7 @@ public void decreaseCurrentPixelCount(Long userId) { } } - public void saveUserInRedis(Long userId) { + public void saveUserInRanking(Long userId) { Double score = zSetOperations.score(RANKING_KEY, userId.toString()); if (score == null) { @@ -47,6 +47,14 @@ public void saveUserInRedis(Long userId) { } } + public void deleteUserInRanking(Long userId) { + Double score = zSetOperations.score(RANKING_KEY, userId.toString()); + + if (score != null) { + zSetOperations.remove(RANKING_KEY, userId.toString()); + } + } + public List getRankingsWithCurrentPixelCount() { Set> typedTuples = zSetOperations.reverseRangeWithScores(RANKING_KEY, RANKING_START_INDEX, RANKING_END_INDEX); diff --git a/src/main/java/com/m3pro/groundflip/service/AuthService.java b/src/main/java/com/m3pro/groundflip/service/AuthService.java index 08929e84..143f3f4a 100644 --- a/src/main/java/com/m3pro/groundflip/service/AuthService.java +++ b/src/main/java/com/m3pro/groundflip/service/AuthService.java @@ -11,11 +11,13 @@ import com.m3pro.groundflip.domain.dto.auth.OauthUserInfoResponse; import com.m3pro.groundflip.domain.dto.auth.ReissueReponse; import com.m3pro.groundflip.domain.entity.AppleRefreshToken; +import com.m3pro.groundflip.domain.entity.Permission; import com.m3pro.groundflip.domain.entity.User; import com.m3pro.groundflip.enums.Provider; import com.m3pro.groundflip.enums.UserStatus; import com.m3pro.groundflip.jwt.JwtProvider; import com.m3pro.groundflip.repository.AppleRefreshTokenRepository; +import com.m3pro.groundflip.repository.PermissionRepository; import com.m3pro.groundflip.repository.UserRepository; import com.m3pro.groundflip.service.oauth.OauthService; @@ -29,6 +31,7 @@ public class AuthService { private final JwtProvider jwtProvider; private final UserRepository userRepository; private final AppleRefreshTokenRepository appleRefreshTokenRepository; + private final PermissionRepository permissionRepository; /** * Oauth Provider를 사용해 로그인을 진행한다. @@ -68,12 +71,17 @@ public LoginResponse login(Provider provider, LoginRequest loginRequest) { * @author 김민욱 */ private User registerUser(Provider provider, String email) { - User newUser = User.builder() + User newUser = userRepository.save(User.builder() .email(email) .provider(provider) .status(UserStatus.PENDING) - .build(); - return userRepository.save(newUser); + .build()); + permissionRepository.save(Permission.builder() + .user(newUser) + .serviceNotificationsEnabled(true) + .marketingNotificationsEnabled(false) + .build()); + return newUser; } /** diff --git a/src/main/java/com/m3pro/groundflip/service/FcmService.java b/src/main/java/com/m3pro/groundflip/service/FcmService.java new file mode 100644 index 00000000..3788710e --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/service/FcmService.java @@ -0,0 +1,46 @@ +package com.m3pro.groundflip.service; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.m3pro.groundflip.domain.dto.user.FcmTokenRequest; +import com.m3pro.groundflip.domain.entity.FcmToken; +import com.m3pro.groundflip.domain.entity.User; +import com.m3pro.groundflip.exception.AppException; +import com.m3pro.groundflip.exception.ErrorCode; +import com.m3pro.groundflip.repository.FcmTokenRepository; +import com.m3pro.groundflip.repository.UserRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FcmService { + private final UserRepository userRepository; + private final FcmTokenRepository fcmTokenRepository; + + @Transactional + public void registerFcmToken(FcmTokenRequest fcmTokenRequest) { + Long userId = fcmTokenRequest.getUserId(); + User user = userRepository.findById(userId).orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND)); + Optional fcmToken = fcmTokenRepository.findByUser(user); + + if (fcmToken.isPresent()) { + fcmToken.get().updateModifiedAtToNow(); + fcmToken.get().updateToken(fcmTokenRequest.getFcmToken()); + fcmToken.get().updateDevice(fcmTokenRequest.getDevice()); + } else { + fcmTokenRepository.save( + FcmToken.builder() + .user(user) + .token(fcmTokenRequest.getFcmToken()) + .device(fcmTokenRequest.getDevice()) + .build() + ); + } + } +} diff --git a/src/main/java/com/m3pro/groundflip/service/MyPlaceService.java b/src/main/java/com/m3pro/groundflip/service/MyPlaceService.java new file mode 100644 index 00000000..d9e83e58 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/service/MyPlaceService.java @@ -0,0 +1,62 @@ +package com.m3pro.groundflip.service; + +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Service; + +import com.m3pro.groundflip.domain.dto.myplace.MyPlaceRequest; +import com.m3pro.groundflip.domain.dto.myplace.MyPlaceResponse; +import com.m3pro.groundflip.domain.entity.MyPlace; +import com.m3pro.groundflip.domain.entity.User; +import com.m3pro.groundflip.exception.AppException; +import com.m3pro.groundflip.exception.ErrorCode; +import com.m3pro.groundflip.repository.MyPlaceRepository; +import com.m3pro.groundflip.repository.UserRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class MyPlaceService { + + private final UserRepository userRepository; + private final GeometryFactory geometryFactory; + private final MyPlaceRepository myPlaceRepository; + + @Transactional + public void putMyPlace(MyPlaceRequest myPlaceRequest) { + User user = userRepository.findById(myPlaceRequest.getUserId()) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND)); + double placeLongitude = myPlaceRequest.getLongitude(); + double placeLatitude = myPlaceRequest.getLatitude(); + + Point point = geometryFactory.createPoint(new Coordinate(placeLongitude, placeLatitude)); + + myPlaceRepository.save( + MyPlace.builder() + .user(user) + .placePoint(point) + .placeName(myPlaceRequest.getPlaceName()) + .build() + ); + } + + public List getMyPlace(Long userId) { + List myPlaces = myPlaceRepository.findByUserId(userId); + return myPlaces.stream().map(MyPlaceResponse::from).toList(); + } + + public void deleteMyPlace(MyPlaceRequest myPlaceRequest) { + List myplaces = myPlaceRepository.findByUserIdAndPlaceName(myPlaceRequest.getUserId(), + myPlaceRequest.getPlaceName()); + if (myplaces.isEmpty()) { + throw new AppException(ErrorCode.PLACE_NOT_FOUND); + } + myPlaceRepository.deleteAll(myplaces); + } + +} diff --git a/src/main/java/com/m3pro/groundflip/service/PermissionService.java b/src/main/java/com/m3pro/groundflip/service/PermissionService.java new file mode 100644 index 00000000..c19f2160 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/service/PermissionService.java @@ -0,0 +1,52 @@ +package com.m3pro.groundflip.service; + +import org.springframework.stereotype.Service; + +import com.m3pro.groundflip.domain.dto.permission.PermissionRequest; +import com.m3pro.groundflip.domain.dto.permission.PermissionResponse; +import com.m3pro.groundflip.domain.entity.Permission; +import com.m3pro.groundflip.domain.entity.User; +import com.m3pro.groundflip.exception.AppException; +import com.m3pro.groundflip.exception.ErrorCode; +import com.m3pro.groundflip.repository.PermissionRepository; +import com.m3pro.groundflip.repository.UserRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + private final UserRepository userRepository; + private final PermissionRepository permissionRepository; + + public PermissionResponse getAllPermissions(Long userId) { + User user = userRepository.getReferenceById(userId); + Permission permission = permissionRepository.findByUser(user) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND)); + return new PermissionResponse( + permission.getServiceNotificationsEnabled(), + permission.getMarketingNotificationsEnabled() + ); + } + + @Transactional + public void updateServiceNotificationsPreference(PermissionRequest permissionRequest) { + User user = userRepository.findById(permissionRequest.getUserId()).orElseThrow(() -> new AppException( + ErrorCode.USER_NOT_FOUND)); + Permission permission = permissionRepository.findByUser(user) + .orElseThrow(() -> new AppException(ErrorCode.INTERNAL_SERVER_ERROR)); + permission.updateServiceNotificationsEnabled(permissionRequest.isEnabled()); + } + + @Transactional + public void updateMarketingNotificationsPreference(PermissionRequest permissionRequest) { + User user = userRepository.findById(permissionRequest.getUserId()).orElseThrow(() -> new AppException( + ErrorCode.USER_NOT_FOUND)); + Permission permission = permissionRepository.findByUser(user) + .orElseThrow(() -> new AppException(ErrorCode.INTERNAL_SERVER_ERROR)); + permission.updateMarketingNotificationsEnabled(permissionRequest.isEnabled()); + } +} diff --git a/src/main/java/com/m3pro/groundflip/service/PixelService.java b/src/main/java/com/m3pro/groundflip/service/PixelService.java index 9923ba74..7335af41 100644 --- a/src/main/java/com/m3pro/groundflip/service/PixelService.java +++ b/src/main/java/com/m3pro/groundflip/service/PixelService.java @@ -150,7 +150,7 @@ private void occupyPixel(PixelOccupyRequest pixelOccupyRequest) { Pixel targetPixel = pixelRepository.findByXAndY(pixelOccupyRequest.getX(), pixelOccupyRequest.getY()) .orElseThrow(() -> new AppException(ErrorCode.PIXEL_NOT_FOUND)); - updateRankingOnCache(targetPixel, occupyingUserId); + rankingService.updateRanking(targetPixel, occupyingUserId); updatePixelOwner(targetPixel, occupyingUserId); updatePixelAddress(targetPixel); @@ -159,7 +159,7 @@ private void occupyPixel(PixelOccupyRequest pixelOccupyRequest) { private void updatePixelOwner(Pixel targetPixel, Long occupyingUserId) { if (Objects.equals(targetPixel.getUserId(), occupyingUserId)) { - targetPixel.updateModifiedAt(); + targetPixel.updateModifiedAtToNow(); } else { targetPixel.updateUserId(occupyingUserId); } @@ -178,32 +178,6 @@ private void updatePixelAddress(Pixel targetPixel) { } } - /** - * 레디스 상에서 랭킹을 조정한다. - * @param targetPixel 랭킹을 조정할 픽셀 - * @param occupyingUserId 현재 픽셀을 방문한 유저 - * @return - * @author 김민욱 - */ - private void updateRankingOnCache(Pixel targetPixel, Long occupyingUserId) { - Long originalOwnerUserId = targetPixel.getUserId(); - LocalDateTime thisWeekStart = DateUtils.getThisWeekStartDate().atTime(0, 0); - LocalDateTime modifiedAt = targetPixel.getModifiedAt(); - - if (Objects.equals(originalOwnerUserId, occupyingUserId)) { - if (modifiedAt.isAfter(thisWeekStart)) { - return; - } - rankingService.increaseCurrentPixelCount(occupyingUserId); - } else { - if (originalOwnerUserId == null || modifiedAt.isBefore(thisWeekStart)) { - rankingService.increaseCurrentPixelCount(occupyingUserId); - } else { - rankingService.updateRankingAfterOccupy(occupyingUserId, originalOwnerUserId); - } - } - } - /** * 개인 기록 모드에서 픽셀 방문 기록을 가져온다 * @param pixelId 기록을 조회할 픽셀 diff --git a/src/main/java/com/m3pro/groundflip/service/RankingService.java b/src/main/java/com/m3pro/groundflip/service/RankingService.java index 4ea38f20..49822954 100644 --- a/src/main/java/com/m3pro/groundflip/service/RankingService.java +++ b/src/main/java/com/m3pro/groundflip/service/RankingService.java @@ -1,8 +1,10 @@ package com.m3pro.groundflip.service; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -11,6 +13,7 @@ import com.m3pro.groundflip.domain.dto.ranking.Ranking; import com.m3pro.groundflip.domain.dto.ranking.UserRankingResponse; +import com.m3pro.groundflip.domain.entity.Pixel; import com.m3pro.groundflip.domain.entity.RankingHistory; import com.m3pro.groundflip.domain.entity.User; import com.m3pro.groundflip.exception.AppException; @@ -31,20 +34,23 @@ public class RankingService { private final UserRepository userRepository; private final RankingHistoryRepository rankingHistoryRepository; - /** - * 현재 픽셀의 수를 1 증가 시킨다. - * @param userId 사용자 id - */ - public void increaseCurrentPixelCount(Long userId) { - rankingRedisRepository.increaseCurrentPixelCount(userId); - } + public void updateRanking(Pixel targetPixel, Long occupyingUserId) { + Long originalOwnerUserId = targetPixel.getUserId(); + LocalDateTime thisWeekStart = DateUtils.getThisWeekStartDate().atTime(0, 0); + LocalDateTime modifiedAt = targetPixel.getModifiedAt(); - /** - * 현재 픽셀의 수를 1 감소 시킨다. - * @param userId 사용자 id - */ - public void decreaseCurrentPixelCount(Long userId) { - rankingRedisRepository.decreaseCurrentPixelCount(userId); + if (Objects.equals(originalOwnerUserId, occupyingUserId)) { + if (modifiedAt.isAfter(thisWeekStart)) { + return; + } + rankingRedisRepository.increaseCurrentPixelCount(occupyingUserId); + } else { + if (originalOwnerUserId == null || modifiedAt.isBefore(thisWeekStart)) { + rankingRedisRepository.increaseCurrentPixelCount(occupyingUserId); + } else { + updateRankingAfterOccupy(occupyingUserId, originalOwnerUserId); + } + } } /** @@ -53,8 +59,8 @@ public void decreaseCurrentPixelCount(Long userId) { * @param deprivedUserId 픽셀을 뺴앗긴 유저 */ public void updateRankingAfterOccupy(Long occupyingUserId, Long deprivedUserId) { - increaseCurrentPixelCount(occupyingUserId); - decreaseCurrentPixelCount(deprivedUserId); + rankingRedisRepository.increaseCurrentPixelCount(occupyingUserId); + rankingRedisRepository.decreaseCurrentPixelCount(deprivedUserId); } /** diff --git a/src/main/java/com/m3pro/groundflip/service/UserService.java b/src/main/java/com/m3pro/groundflip/service/UserService.java index 23d7ad3e..7d17ba78 100644 --- a/src/main/java/com/m3pro/groundflip/service/UserService.java +++ b/src/main/java/com/m3pro/groundflip/service/UserService.java @@ -21,6 +21,7 @@ import com.m3pro.groundflip.exception.ErrorCode; import com.m3pro.groundflip.jwt.JwtProvider; import com.m3pro.groundflip.repository.AppleRefreshTokenRepository; +import com.m3pro.groundflip.repository.FcmTokenRepository; import com.m3pro.groundflip.repository.RankingRedisRepository; import com.m3pro.groundflip.repository.UserCommunityRepository; import com.m3pro.groundflip.repository.UserRepository; @@ -39,6 +40,7 @@ public class UserService { private final UserRepository userRepository; private final AppleRefreshTokenRepository appleRefreshTokenRepository; private final UserCommunityRepository userCommunityRepository; + private final FcmTokenRepository fcmTokenRepository; private final S3Uploader s3Uploader; private final JwtProvider jwtProvider; private final AppleApiClient appleApiClient; @@ -97,7 +99,7 @@ public void putUserInfo(Long userId, UserInfoRequest userInfoRequest, MultipartF user.updateStatus(UserStatus.COMPLETE); user.updateProfileImage(fileS3Url); userRepository.save(user); - rankingRedisRepository.saveUserInRedis(user.getId()); + rankingRedisRepository.saveUserInRanking(user.getId()); } private Date convertToDate(int year) { @@ -122,6 +124,8 @@ public void deleteUser(Long userId, UserDeleteRequest userDeleteRequest) { if (deletedUser.getProvider() == Provider.APPLE) { revokeAppleToken(deletedUser.getId()); } + fcmTokenRepository.deleteByUser(deletedUser); + deletedUser.updateBirthYear(convertToDate(1900)); deletedUser.updateNickName(null); deletedUser.updateProfileImage(null); @@ -129,6 +133,8 @@ public void deleteUser(Long userId, UserDeleteRequest userDeleteRequest) { jwtProvider.expireToken(userDeleteRequest.getAccessToken()); jwtProvider.expireToken(userDeleteRequest.getRefreshToken()); + + rankingRedisRepository.deleteUserInRanking(userId); } private void revokeAppleToken(Long userId) { diff --git a/src/main/java/com/m3pro/groundflip/service/VersionService.java b/src/main/java/com/m3pro/groundflip/service/VersionService.java new file mode 100644 index 00000000..ddca2cd4 --- /dev/null +++ b/src/main/java/com/m3pro/groundflip/service/VersionService.java @@ -0,0 +1,60 @@ +package com.m3pro.groundflip.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.m3pro.groundflip.domain.dto.version.VersionResponse; +import com.m3pro.groundflip.enums.Version; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class VersionService { + + @Value("${version.update}") + private String latestVersion; + + @Value("${version.recommend}") + private String recommendUpdate; + + public VersionResponse getVersion(String currentVersion) { + Version needUpdate = Version.OK; + + if (compareVersions(currentVersion, recommendUpdate) == -1) { + needUpdate = Version.FORCE; + } + if (compareVersions(currentVersion, recommendUpdate) == 1 + && compareVersions(currentVersion, latestVersion) == -1) { + needUpdate = Version.NEED; + } + if (compareVersions(currentVersion, latestVersion) == 1) { + needUpdate = Version.OK; + } + + return VersionResponse.builder() + .version(latestVersion) + .needUpdate(needUpdate) + .build(); + } + + private static int compareVersions(String version1, String version2) { + String[] v1Parts = version1.split("\\."); + String[] v2Parts = version2.split("\\."); + + int length = Math.max(v1Parts.length, v2Parts.length); + + for (int i = 0; i < length; i++) { + int v1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0; + int v2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0; + + if (v1 < v2) { + return -1; + } + if (v1 > v2) { + return 1; + } + } + return 1; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index a52eed2b..26561bd2 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -85,5 +85,9 @@ cloud: s3: bucket: ${AWS_S3_BUCKET} +version: + update: ${NEED_UPDATE_VERSION} + recommend: ${RECOMMEND_UPDATE} + diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index ff715e68..025a91f1 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -85,4 +85,7 @@ cloud: s3: bucket: ${AWS_S3_BUCKET} +version: + update: ${NEED_UPDATE_VERSION} + recommend: ${RECOMMEND_UPDATE} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index f168e6ab..7750a4f0 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -83,5 +83,8 @@ cloud: s3: bucket: ${AWS_S3_BUCKET} +version: + update: ${NEED_UPDATE_VERSION} + recommend: ${RECOMMEND_UPDATE} diff --git a/src/main/resources/static/permission_insert.sql b/src/main/resources/static/permission_insert.sql new file mode 100644 index 00000000..0e33a29c --- /dev/null +++ b/src/main/resources/static/permission_insert.sql @@ -0,0 +1,6 @@ +INSERT INTO permission (user_id, marketing_notifications_enabled, service_notifications_enabled, created_at, + modified_at) +SELECT u.user_id, FALSE, FALSE, NOW(), NOW() +FROM user u + LEFT JOIN permission p ON u.user_id = p.user_id +WHERE p.user_id IS NULL; \ No newline at end of file diff --git a/src/test/java/com/m3pro/groundflip/repository/RankingRedisRepositoryTest.java b/src/test/java/com/m3pro/groundflip/repository/RankingRedisRepositoryTest.java index 2293f495..533a1e66 100644 --- a/src/test/java/com/m3pro/groundflip/repository/RankingRedisRepositoryTest.java +++ b/src/test/java/com/m3pro/groundflip/repository/RankingRedisRepositoryTest.java @@ -40,7 +40,7 @@ void increaseCurrentPixelCountTest() { //Given ZSetOperations zSetOperations = redisTemplate.opsForZSet(); Long userId = 1L; - rankingRedisRepository.saveUserInRedis(userId); + rankingRedisRepository.saveUserInRanking(userId); // When rankingRedisRepository.increaseCurrentPixelCount(userId); @@ -71,7 +71,7 @@ void decreaseCurrentPixelCountTest() { //Given ZSetOperations zSetOperations = redisTemplate.opsForZSet(); Long userId = 1L; - rankingRedisRepository.saveUserInRedis(userId); + rankingRedisRepository.saveUserInRanking(userId); rankingRedisRepository.increaseCurrentPixelCount(userId); rankingRedisRepository.increaseCurrentPixelCount(userId); @@ -89,7 +89,7 @@ void decreaseCurrentPixelCountTestCaseZero() { //Given ZSetOperations zSetOperations = redisTemplate.opsForZSet(); Long userId = 1L; - rankingRedisRepository.saveUserInRedis(userId); + rankingRedisRepository.saveUserInRanking(userId); // When rankingRedisRepository.decreaseCurrentPixelCount(userId); @@ -101,19 +101,34 @@ void decreaseCurrentPixelCountTestCaseZero() { @Test @DisplayName("[save] userId 를 넣으면 0으로 초기화 한다.") - void saveUserInRedisTest() { + void saveUserInRankingTest() { //Given ZSetOperations zSetOperations = redisTemplate.opsForZSet(); Long userId = 1L; // When - rankingRedisRepository.saveUserInRedis(userId); + rankingRedisRepository.saveUserInRanking(userId); // Then Double score = zSetOperations.score(RANKING_KEY, userId.toString()); assertThat(score).isEqualTo(0); } + @Test + @DisplayName("[delete] userId를 레디스에서 지울 수 있다.") + void deleteUserInRankingTest() { + //Given + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + Long userId = 1L; + + // When + rankingRedisRepository.deleteUserInRanking(userId); + + // Then + Double score = zSetOperations.score(RANKING_KEY, userId.toString()); + assertThat(score).isEqualTo(null); + } + @Test @DisplayName("[getRankingsWithScore] 점수 순으로 내림차순으로 30개가 리스트에 반환된다. 30개가 되지 않는다면 채워진 개수만 반환한다.") void getRankingsWithScoreTest() { @@ -199,9 +214,9 @@ void getUserCurrentPixelCountTestNull() { } private void setRanking(Long userId1, Long userId2, Long userId3) { - rankingRedisRepository.saveUserInRedis(userId1); - rankingRedisRepository.saveUserInRedis(userId2); - rankingRedisRepository.saveUserInRedis(userId3); + rankingRedisRepository.saveUserInRanking(userId1); + rankingRedisRepository.saveUserInRanking(userId2); + rankingRedisRepository.saveUserInRanking(userId3); rankingRedisRepository.increaseCurrentPixelCount(userId1); rankingRedisRepository.increaseCurrentPixelCount(userId1); diff --git a/src/test/java/com/m3pro/groundflip/service/AuthServiceTest.java b/src/test/java/com/m3pro/groundflip/service/AuthServiceTest.java index dfd15ed1..29fae43f 100644 --- a/src/test/java/com/m3pro/groundflip/service/AuthServiceTest.java +++ b/src/test/java/com/m3pro/groundflip/service/AuthServiceTest.java @@ -25,6 +25,7 @@ import com.m3pro.groundflip.enums.UserStatus; import com.m3pro.groundflip.jwt.JwtProvider; import com.m3pro.groundflip.repository.AppleRefreshTokenRepository; +import com.m3pro.groundflip.repository.PermissionRepository; import com.m3pro.groundflip.repository.RankingRedisRepository; import com.m3pro.groundflip.repository.UserRepository; import com.m3pro.groundflip.service.oauth.OauthService; @@ -41,6 +42,8 @@ class AuthServiceTest { private RankingRedisRepository rankingRedisRepository; @Mock private AppleRefreshTokenRepository appleRefreshTokenRepository; + @Mock + private PermissionRepository permissionRepository; @InjectMocks private AuthService authService; diff --git a/src/test/java/com/m3pro/groundflip/service/FcmServiceTest.java b/src/test/java/com/m3pro/groundflip/service/FcmServiceTest.java new file mode 100644 index 00000000..74818d62 --- /dev/null +++ b/src/test/java/com/m3pro/groundflip/service/FcmServiceTest.java @@ -0,0 +1,76 @@ +package com.m3pro.groundflip.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.m3pro.groundflip.domain.dto.user.FcmTokenRequest; +import com.m3pro.groundflip.domain.entity.FcmToken; +import com.m3pro.groundflip.domain.entity.User; +import com.m3pro.groundflip.enums.Device; +import com.m3pro.groundflip.exception.AppException; +import com.m3pro.groundflip.exception.ErrorCode; +import com.m3pro.groundflip.repository.FcmTokenRepository; +import com.m3pro.groundflip.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +class FcmServiceTest { + private static final Long testUserId = 1L; + private static final String testFcmToken = "test token"; + private static FcmTokenRequest fcmTokenRequest; + @Mock + private UserRepository userRepository; + @Mock + private FcmTokenRepository fcmTokenRepository; + @InjectMocks + private FcmService fcmService; + + @BeforeAll + static void beforeAll() { + fcmTokenRequest = new FcmTokenRequest(testUserId, testFcmToken, Device.IOS); + } + + @Test + @DisplayName("[registerFcmToken] user 가 없는 경우 에러 발생") + void registerFcmToken_UserNotFound() { + when(userRepository.findById(testUserId)).thenReturn(Optional.empty()); + + AppException exception = assertThrows(AppException.class, () -> fcmService.registerFcmToken(fcmTokenRequest)); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND); + } + + @Test + @DisplayName("[registerFcmToken] fcm 토큰 새로 등록") + void registerFcmToken_RegisterNewToken() { + User user = User.builder().id(1L).email("test@test.com").build(); + when(userRepository.findById(testUserId)).thenReturn(Optional.of(user)); + when(fcmTokenRepository.findByUser(user)).thenReturn(Optional.empty()); + + fcmService.registerFcmToken(fcmTokenRequest); + + verify(fcmTokenRepository, times(1)).save(any()); + } + + @Test + @DisplayName("[registerFcmToken] fcm 토큰이 이미 등록된 경우 수정 날짜만 변경") + void registerFcmToken_UpdateModifiedDate() { + User user = User.builder().id(1L).email("test@test.com").build(); + FcmToken fcmToken = FcmToken.builder().id(1L).token(testFcmToken).user(user).build(); + when(userRepository.findById(testUserId)).thenReturn(Optional.of(user)); + when(fcmTokenRepository.findByUser(user)).thenReturn(Optional.of(fcmToken)); + + fcmService.registerFcmToken(fcmTokenRequest); + + assertThat(fcmToken.getModifiedAt()).isNotNull(); + } +} \ No newline at end of file diff --git a/src/test/java/com/m3pro/groundflip/service/MyPlaceServiceTest.java b/src/test/java/com/m3pro/groundflip/service/MyPlaceServiceTest.java new file mode 100644 index 00000000..93c7f3d6 --- /dev/null +++ b/src/test/java/com/m3pro/groundflip/service/MyPlaceServiceTest.java @@ -0,0 +1,158 @@ +package com.m3pro.groundflip.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.m3pro.groundflip.domain.dto.myplace.MyPlaceRequest; +import com.m3pro.groundflip.domain.entity.MyPlace; +import com.m3pro.groundflip.domain.entity.User; +import com.m3pro.groundflip.enums.Place; +import com.m3pro.groundflip.exception.AppException; +import com.m3pro.groundflip.exception.ErrorCode; +import com.m3pro.groundflip.repository.MyPlaceRepository; +import com.m3pro.groundflip.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class MyPlaceServiceTest { + @Mock + private UserRepository userRepository; + + @Mock + private GeometryFactory geometryFactory; + + @Mock + private MyPlaceRepository myPlaceRepository; + + @InjectMocks + private MyPlaceService myPlaceService; + + private MyPlaceRequest myPlaceRequest; + + private User user; + + // @BeforeEach + // void setUp() { + // MockitoAnnotations.openMocks(this); + // //user = User.builder().id(1L).nickname("testUser").build(); + // + // } + + @Test + @DisplayName("[putMyPlace] 즐겨찾기 장소가 올바르게 업데이트 되는지") + void putMyPlaceTest() { + // Given + User user = User.builder() + .id(1L) + .nickname("testUser") + .build(); + + MyPlaceRequest myPlaceRequest = MyPlaceRequest.builder() + .userId(1L) + .placeName(Place.HOME) + .latitude(37.321147) + .longitude(127.093171) + .build(); + + Point mockPoint = mock(Point.class); + when(geometryFactory.createPoint(any(Coordinate.class))).thenReturn(mockPoint); + + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(myPlaceRepository.save(any(MyPlace.class))).thenReturn(null); + + // When + myPlaceService.putMyPlace(myPlaceRequest); + + // Then + verify(userRepository, times(1)).findById(myPlaceRequest.getUserId()); + verify(myPlaceRepository, times(1)).save(any(MyPlace.class)); + + //assertThat(user.getNickname()).isEqualTo("testUser"); + } + + @Test + @DisplayName("[getMyPlace] 즐겨찾기 장소를 올바르게 가져오는지") + void getMyPlaceTest() { + //Given + Long userId = 1L; + double latitude1 = 37.321147; + double latitude2 = 37.123456; + double longitude1 = 127.093171; + double longitude2 = 127.123456; + + Point point1 = geometryFactory.createPoint(new Coordinate(latitude1, longitude1)); + Point point2 = geometryFactory.createPoint(new Coordinate(latitude2, longitude2)); + + List myPlaceList = Arrays.asList( + MyPlace.builder().placeName(Place.HOME).placePoint(point1).build(), + MyPlace.builder().placeName(Place.HOME).placePoint(point2).build() + ); + + //When + when(myPlaceRepository.findByUserId(userId)).thenReturn(myPlaceList); + + List myPlaces = myPlaceRepository.findByUserId(userId); + + //Then + assertEquals(2, myPlaces.size()); + assertEquals(point1, myPlaces.get(0).getPlacePoint()); + assertEquals(point2, myPlaces.get(1).getPlacePoint()); + } + + @Test + @DisplayName("[deleteMyPlace] 즐겨찾기 삭제 동작 테스트") + void deleteMyPlaceTest() { + //Given + Long userId = 1L; + + myPlaceRequest = MyPlaceRequest.builder() + .userId(userId) + .placeName(Place.HOME) + .build(); + + List myPlaceList = Arrays.asList( + MyPlace.builder().placeName(Place.HOME).user(user).build(), + MyPlace.builder().placeName(Place.HOME).user(user).build() + ); + + //when(myPlaceRepository.findByUserIdAndPlaceName(userId, Place.HOME)).thenReturn(myPlaceList); + + myPlaceRepository.deleteAll(myPlaceList); + + verify(myPlaceRepository, times(1)).deleteAll(myPlaceList); + + } + + @Test + @DisplayName("[deleteMyPlace] 즐겨찾기 장소가 존재하지 않을 때 에러가 발생하는지") + void deleteMyPlace_NotFound() { + // Given + Long userId = 1L; + Place placeName = Place.HOME; + MyPlaceRequest myPlaceRequest = MyPlaceRequest.builder() + .userId(userId) + .placeName(placeName) + .build(); + + // When + + AppException thrown = assertThrows(AppException.class, () -> { + myPlaceService.deleteMyPlace(myPlaceRequest); + }); + + assertEquals(ErrorCode.PLACE_NOT_FOUND, thrown.getErrorCode()); + } +} diff --git a/src/test/java/com/m3pro/groundflip/service/PermissionServiceTest.java b/src/test/java/com/m3pro/groundflip/service/PermissionServiceTest.java new file mode 100644 index 00000000..568e771d --- /dev/null +++ b/src/test/java/com/m3pro/groundflip/service/PermissionServiceTest.java @@ -0,0 +1,93 @@ +package com.m3pro.groundflip.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.m3pro.groundflip.domain.dto.permission.PermissionRequest; +import com.m3pro.groundflip.domain.dto.permission.PermissionResponse; +import com.m3pro.groundflip.domain.entity.Permission; +import com.m3pro.groundflip.domain.entity.User; +import com.m3pro.groundflip.repository.PermissionRepository; +import com.m3pro.groundflip.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +class PermissionServiceTest { + @Mock + private UserRepository userRepository; + + @Mock + private PermissionRepository permissionRepository; + + @InjectMocks + private PermissionService permissionService; + + private User mockUser; + private Permission mockPermission; + + @BeforeEach + void setUp() { + mockUser = User.builder() + .id(1L) + .nickname("testUser") + .build(); + + mockPermission = Permission.builder() + .serviceNotificationsEnabled(true) + .marketingNotificationsEnabled(true) + .user(mockUser) + .build(); + } + + @Test + @DisplayName("[getAllPermissions] userId에 해당하는 권한의 정보를 가져온다.") + void testGetAllPermissions() { + when(userRepository.getReferenceById(anyLong())).thenReturn(mockUser); + when(permissionRepository.findByUser(any(User.class))).thenReturn(Optional.of(mockPermission)); + + PermissionResponse response = permissionService.getAllPermissions(1L); + + assertTrue(response.isServiceNotificationEnabled()); + assertTrue(response.isMarketingNotificationEnabled()); + + verify(userRepository, times(1)).getReferenceById(1L); + verify(permissionRepository, times(1)).findByUser(mockUser); + } + + @Test + @DisplayName("[updateServiceNotificationsPreference] service 권한을 정상적으로 변경한다.") + void testUpdateServiceNotificationsPreference() { + PermissionRequest request = new PermissionRequest(1L, false); + + when(userRepository.findById(anyLong())).thenReturn(Optional.of(mockUser)); + when(permissionRepository.findByUser(any(User.class))).thenReturn(Optional.of(mockPermission)); + + permissionService.updateServiceNotificationsPreference(request); + + verify(permissionRepository, times(1)).findByUser(mockUser); + assertEquals(false, mockPermission.getServiceNotificationsEnabled()); + } + + @Test + @DisplayName("[updateMarketingNotificationsPreference] marketing 권한을 정상적으로 변경한다.") + void testUpdateMarketingNotificationsPreference() { + PermissionRequest request = new PermissionRequest(1L, false); + + when(userRepository.findById(anyLong())).thenReturn(Optional.of(mockUser)); + when(permissionRepository.findByUser(any(User.class))).thenReturn(Optional.of(mockPermission)); + + permissionService.updateMarketingNotificationsPreference(request); + + verify(permissionRepository, times(1)).findByUser(mockUser); + assertEquals(false, mockPermission.getMarketingNotificationsEnabled()); + } +} \ No newline at end of file diff --git a/src/test/java/com/m3pro/groundflip/service/RankingServiceTest.java b/src/test/java/com/m3pro/groundflip/service/RankingServiceTest.java index 856a3f47..5113eb7f 100644 --- a/src/test/java/com/m3pro/groundflip/service/RankingServiceTest.java +++ b/src/test/java/com/m3pro/groundflip/service/RankingServiceTest.java @@ -21,6 +21,7 @@ import com.m3pro.groundflip.domain.dto.ranking.Ranking; import com.m3pro.groundflip.domain.dto.ranking.UserRankingResponse; +import com.m3pro.groundflip.domain.entity.Pixel; import com.m3pro.groundflip.domain.entity.RankingHistory; import com.m3pro.groundflip.domain.entity.User; import com.m3pro.groundflip.exception.AppException; @@ -28,6 +29,7 @@ import com.m3pro.groundflip.repository.RankingHistoryRepository; import com.m3pro.groundflip.repository.RankingRedisRepository; import com.m3pro.groundflip.repository.UserRepository; +import com.m3pro.groundflip.util.DateUtils; @ExtendWith(MockitoExtension.class) class RankingServiceTest { @@ -46,27 +48,67 @@ void init() { } @Test - @DisplayName("[increaseCurrentPixelCount] userId 에 해당하는 현재 소유 픽셀의 개수를 1 증가시킨다.") - void increaseCurrentPixelCountTest() { - Long userId = 1L; + @DisplayName("[updateRanking] 이번주에 이미 차지한 땅이라면 랭킹 업데이트 하지 않는다.") + void updateRanking_NoUpdate() { + Long occupyingUserId = 1L; + Pixel targetPixel = Pixel.builder().userId(occupyingUserId).build(); + targetPixel.updateModifiedAt(DateUtils.getThisWeekStartDate().atTime(0, 0, 0).plusDays(1)); - rankingService.increaseCurrentPixelCount(userId); + rankingService.updateRanking(targetPixel, occupyingUserId); - verify(rankingRedisRepository, times(1)).increaseCurrentPixelCount(userId); + verify(rankingRedisRepository, never()).increaseCurrentPixelCount(occupyingUserId); } @Test - @DisplayName("[decreasePixelCount] userId 에 해당하는 현재 소유 픽셀의 개수를 1 감소시킨다.") - void decreasePixelCountTest() { - Long userId = 1L; + @DisplayName("[updateRanking] 저번주에 차지한 땅이고 차지한 사람과 차지하려는 사람이 같다면 랭킹을 업데이트한다.") + void updateRanking_UpdateBeforeThisWeek() { + Long occupyingUserId = 1L; + Pixel targetPixel = Pixel.builder().userId(occupyingUserId).build(); + targetPixel.updateModifiedAt(DateUtils.getThisWeekStartDate().atTime(0, 0, 0).minusDays(3)); + + rankingService.updateRanking(targetPixel, occupyingUserId); + + verify(rankingRedisRepository, times(1)).increaseCurrentPixelCount(occupyingUserId); + } + + @Test + @DisplayName("[updateRanking] 아무도 차지 한 적이 없는 땅이라면 차지하려는 사람의 점수를 추가한다.") + void updateRanking_NeverOccupied() { + Long occupyingUserId = 1L; + Pixel targetPixel = Pixel.builder().userId(null).build(); + + rankingService.updateRanking(targetPixel, occupyingUserId); + + verify(rankingRedisRepository, times(1)).increaseCurrentPixelCount(occupyingUserId); + } - rankingService.decreaseCurrentPixelCount(userId); + @Test + @DisplayName("[updateRanking] 가장 최근에 차지한 시간이 저번주라면 지금 차지하려는 사람의 점수를 추가한다.") + void updateRanking_BeforeThisWeek() { + Long occupyingUserId = 1L; + Pixel targetPixel = Pixel.builder().userId(3L).build(); + targetPixel.updateModifiedAt(DateUtils.getThisWeekStartDate().atTime(0, 0, 0).minusDays(3)); - verify(rankingRedisRepository, times(1)).decreaseCurrentPixelCount(userId); + rankingService.updateRanking(targetPixel, occupyingUserId); + + verify(rankingRedisRepository, times(1)).increaseCurrentPixelCount(occupyingUserId); + } + + @Test + @DisplayName("[updateRanking] 가장 최근에 차지한 시간이 이번주이고 차지하려는 사람과 차지한 사람이 다르면 지금 차지하려는 사람의 점수를 추가하고 차지한 사람의 점수는 감소시킨다.") + void updateRanking_OccupyPixel() { + Long occupyingUserId = 1L; + Pixel targetPixel = Pixel.builder().userId(3L).build(); + targetPixel.updateModifiedAt(DateUtils.getThisWeekStartDate().atTime(0, 0, 0).plusDays(3)); + + rankingService.updateRanking(targetPixel, occupyingUserId); + + verify(rankingRedisRepository, times(1)).increaseCurrentPixelCount(occupyingUserId); + verify(rankingRedisRepository, times(1)).decreaseCurrentPixelCount(3L); } @Test - @DisplayName("[decreasePixelCount] occupyingUserId 에 해당하는 현재 소유 픽셀의 개수를 1 증가시키고 deprivedUserId 에 해당하는 현재 소유 픽셀의 개수를 1 감소시킨다.") + @DisplayName("[updateRankingAfterOccupy] occupyingUserId 에 해당하는 현재 소유 픽셀의 개수를 1 증가시키고 deprivedUserId 에 해당하는 현재 소유 픽셀의 개수를 1 감소시킨다.") void updateRankingAfterOccupyTest() { Long occupyingUserId = 1L; Long deprivedUserId = 2L; diff --git a/src/test/java/com/m3pro/groundflip/service/UserServiceTest.java b/src/test/java/com/m3pro/groundflip/service/UserServiceTest.java index 4a6ec5d2..10f41aa8 100644 --- a/src/test/java/com/m3pro/groundflip/service/UserServiceTest.java +++ b/src/test/java/com/m3pro/groundflip/service/UserServiceTest.java @@ -36,6 +36,7 @@ import com.m3pro.groundflip.exception.ErrorCode; import com.m3pro.groundflip.jwt.JwtProvider; import com.m3pro.groundflip.repository.AppleRefreshTokenRepository; +import com.m3pro.groundflip.repository.FcmTokenRepository; import com.m3pro.groundflip.repository.RankingRedisRepository; import com.m3pro.groundflip.repository.UserCommunityRepository; import com.m3pro.groundflip.repository.UserRepository; @@ -50,6 +51,9 @@ class UserServiceTest { @Mock private RankingRedisRepository rankingRedisRepository; + @Mock + private FcmTokenRepository fcmTokenRepository; + @Mock private S3Uploader s3Uploader; @@ -207,6 +211,7 @@ void deleteUserTest() { Calendar calendar = Calendar.getInstance(); calendar.setTime(deleteUser.getBirthYear()); + verify(fcmTokenRepository, times(1)).deleteByUser(any()); assertThat(deleteUser.getNickname()).isEqualTo(null); assertThat(deleteUser.getProfileImage()).isEqualTo(null); assertThat(calendar.get(Calendar.YEAR)).isEqualTo(1900); @@ -236,6 +241,7 @@ void deleteUserTestInApple() { Calendar calendar = Calendar.getInstance(); calendar.setTime(deleteUser.getBirthYear()); + verify(fcmTokenRepository, times(1)).deleteByUser(any()); verify(appleApiClient, times(1)).revokeToken(any()); verify(appleRefreshTokenRepository, times(1)).delete(any()); assertThat(deleteUser.getNickname()).isEqualTo(null); diff --git a/src/test/java/com/m3pro/groundflip/service/VersionServiceTest.java b/src/test/java/com/m3pro/groundflip/service/VersionServiceTest.java new file mode 100644 index 00000000..5af741c6 --- /dev/null +++ b/src/test/java/com/m3pro/groundflip/service/VersionServiceTest.java @@ -0,0 +1,54 @@ +package com.m3pro.groundflip.service; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import com.m3pro.groundflip.domain.dto.version.VersionResponse; +import com.m3pro.groundflip.enums.Version; + +@ExtendWith(MockitoExtension.class) +public class VersionServiceTest { + + @InjectMocks + private VersionService versionService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(versionService, "latestVersion", "2.0.3"); + ReflectionTestUtils.setField(versionService, "recommendUpdate", "1.0.5"); + } + + @Test + @DisplayName("[getVersion] version이 잘 get 되는지") + void getVersionTest() { + //Given + String currentVersion = "1.0.3"; + String currentVersion2 = "2.0.1"; + String currentVersion3 = "2.0.4"; + + //When + VersionResponse versionResponse = versionService.getVersion(currentVersion); + VersionResponse versionResponse2 = versionService.getVersion(currentVersion2); + VersionResponse versionResponse3 = versionService.getVersion(currentVersion3); + + //Then + assertThat(versionResponse).isNotNull(); + assertThat(versionResponse.getVersion()).isEqualTo("2.0.3"); + assertThat(versionResponse.getNeedUpdate()).isEqualTo(Version.FORCE); + + assertThat(versionResponse2.getVersion()).isEqualTo("2.0.3"); + assertThat(versionResponse2.getNeedUpdate()).isEqualTo(Version.NEED); + + assertThat(versionResponse3.getVersion()).isEqualTo("2.0.3"); + assertThat(versionResponse3.getNeedUpdate()).isEqualTo(Version.OK); + + } + +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 6d43d30d..98be1973 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -80,3 +80,7 @@ cloud: secret-key: test-value s3: bucket: test-value + +version: + update: 1.0.3 + recommend: 1.0.0 \ No newline at end of file