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 - 땅따먹기 게임 기반 걷기 앱
+
+
+### 👀 프로젝트 소개
+내가 가는 길이 내 것이 되는 즐거움, 그라운드 플립!
+운동이 재미가 없어 꾸준히 하기 어려웠다구요?
+
+그라운드 플립과 함께 걸어봐요!
+
+그라운드 플립은 땅따먹기 게임에서 아이디어를 얻어 현대인들의 운동 부족을 해결하기 위해 제작되었습니다.
+사용자는 지도 상에 존재하는 수많은 픽셀들을 걸어나가며 차지할 수 있습니다.
+실시간 랭킹, 그룹 기능을 제공합니다.
+
+---
+### 개발 환경
+- Java 17
+- Spring Boot 3.3.0
+
+---
+
+### 기술 세부 스택
+- Spring Boot
+
+- Spring Data JPA
+- Spring Data Redis
+- Spring Security
+그 외
+
+---
+
+### 📚 기능 소개
+BE Architecture
+
+
+ERD
+
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