diff --git a/README.md b/README.md
new file mode 100644
index 00000000..34f80336
--- /dev/null
+++ b/README.md
@@ -0,0 +1,133 @@
+# 찾아유 - 유기동물 관련 종합 애플리케이션
+
+## 👋 Project Overview
+**WHAT?**
+유실동물을 찾고 보호하는 것을 돕는 통합 플랫폼입니다. 실종 신고, 목격 제보부터 보호 센터 정보, AI 품종 분석까지 반려동물을 잃어버린 슬픔을 덜고 빠른 구조를 돕습니다.
+
+**HOW?**
+공공데이터 포털의 전국 유기동물/보호소 데이터와 사용자의 실시간 제보(위치, 사진)를 매핑하여 지도 기반 서비스를 제공합니다. OpenAI 기반의 품종 자동 분석을 도입하여 검색 정확도를 높였습니다.
+
+**WHY?**
+가족 같은 반려동물을 잃어버렸을 때의 막막함을 해소하고, 골든타임을 놓치지 않도록 기술적으로 지원하며, 나아가 성숙한 반려동물 문화를 만들기 위해 시작되었습니다.
+
+
+
+## 🧑🧑🧒 Team Member
+
+
+
+
+## ⚒️ Tech Stack
+
+
+### Backend
+| Tech | Version / Description |
+| --- | --- |
+| **Java** | JDK 17 |
+| **Spring Boot** | 3.5.0 |
+| **Spring Data JPA** | ORM / Hibernate |
+| **Spring Security** | Authentication & Authorization |
+| **Spring AI** | OpenAI (GPT-4o) Integration |
+| **Gradle** | Build Tool |
+
+### Database & Storage
+| Tech | Description |
+| --- | --- |
+| **MySQL** | Main RDBMS (v8.0) |
+| **Redis** | Cache & Session Store |
+| **Flyway** | DB Migration Tool |
+| **AWS S3** | Image Storage |
+
+### Infra & DevOps
+| Tech | Description |
+| --- | --- |
+| **Docker** | Containerization |
+| **AWS EC2** | (Expected Deployment Target) |
+| **GitHub Actions** | CI/CD (Workflows configured) |
+
+### Tools
+- **Swagger (OpenAPI)**: API Documentation
+- **Actuator & Prometheus/Loki/Grafana/Alloy**: Monitoring
+
+
+
+## 🏗️ Architecture
+
+
+
+
+## 📍 ERD
+
+
+
+
+## 📜 Convention
+
+### Code Convention
+| 항목 | 규칙 | 예시 |
+| --- | --- | --- |
+| **Class** | PascalCase | `UserProfile` |
+| **Function** | camelCase | `getUserInfo` |
+| **Variable** | camelCase | `userId` |
+| **DB Table** | snake_case | `user_profile` |
+| **Enum / Constant** | UPPER_SNAKE_CASE | `MAX_RETRY` |
+
+### Git Convention
+
+#### Commit Message
+`[Prefix] #IssueNumber Description`
+예시: `[Feat] #123 로그인 API 구현`
+
+| Prefix | Description |
+| --- | --- |
+| `Feat` | 새로운 기능 추가 |
+| `Fix` | 버그 수정 |
+| `Refactor` | 코드 리팩토링 |
+| `Chore` | 빌드 업무 수정, 패키지 매니저 수정 |
+| `Docs` | 문서 수정 |
+| `Infra` | 인프라 설정 |
+| `Test` | 테스트 코드 |
+
+#### Branch Strategy
+`prefix/#issue-description`
+예시: `feat/#123-login-api`, `fix/#45-bug-fix`
+
+
+
+## 🗂️ Project Structure
+```
+com.kuit.findyou
+├── 📂 domain // 도메인별 비즈니스 로직 (Feature Packaging)
+│ ├── 📂 auth // 인증 (Login, Token)
+│ ├── 📂 breed // 품종 정보
+│ ├── 📂 city // 시/도, 시/군/구 지역 정보
+│ ├── 📂 home // 홈 화면 (통계, 추천)
+│ ├── 📂 image // 이미지 업로드/처리
+│ ├── 📂 information // 동물보호센터, 봉사활동 정보
+│ ├── 📂 inquiry // 문의하기
+│ ├── 📂 notification // 알림
+│ ├── 📂 report // 실종/목격 신고
+│ └── 📂 user // 사용자 관리 (MyPage)
+│
+└── 📂 global // 전역 공유 모듈
+ ├── 📂 common // 공통 Response, Exception
+ ├── 📂 config // 설정 (Security, Swagger, S3, Redis...)
+ ├── 📂 external // 외부 API 클라이언트 (Kakao, Public Data...)
+ ├── 📂 infrastructure // 인프라 구현체 (ImageUploader...)
+ ├── 📂 jwt // JWT 관련 유틸리티 및 필터
+ └── 📂 logging // 로깅 설정
+```
\ No newline at end of file
diff --git a/assets/erd.png b/assets/erd.png
new file mode 100644
index 00000000..f6023034
Binary files /dev/null and b/assets/erd.png differ
diff --git a/assets/system_architecture.png b/assets/system_architecture.png
new file mode 100644
index 00000000..7a323119
Binary files /dev/null and b/assets/system_architecture.png differ
diff --git a/assets/tech_stack.png b/assets/tech_stack.png
new file mode 100644
index 00000000..720c9c51
Binary files /dev/null and b/assets/tech_stack.png differ
diff --git a/build.gradle b/build.gradle
index d91e4aaa..978b5bd9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,6 +16,12 @@ java {
}
}
+test {
+ testLogging {
+ showStandardStreams = project.hasProperty("showLogs")
+ }
+}
+
configurations {
compileOnly {
extendsFrom annotationProcessor
diff --git a/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java b/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java
index af5be38f..647c564f 100644
--- a/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java
+++ b/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java
@@ -1,10 +1,12 @@
package com.kuit.findyou.domain.auth.controller;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse;
import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
-import com.kuit.findyou.domain.auth.service.AuthService;
+import com.kuit.findyou.domain.auth.service.AuthServiceFacade;
import com.kuit.findyou.global.common.annotation.CustomExceptionDescription;
import com.kuit.findyou.global.common.response.BaseResponse;
import io.swagger.v3.oas.annotations.Operation;
@@ -16,8 +18,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.GUEST_LOGIN;
-import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.KAKAO_LOGIN;
+import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.*;
@Tag(name = "Login", description = "로그인 관련 API")
@Slf4j
@@ -25,27 +26,36 @@
@RequestMapping("api/v2/auth")
@RestController
public class AuthController {
- private final AuthService authService;
+ private final AuthServiceFacade authServiceFacade;
@Operation(
summary = "카카오 로그인 API",
description = "카카오 사용자 식별자를 이용해서 유저 정보와 엑세스 토큰을 얻을 수 있습니다. 가입된 회원인지 여부를 반환합니다."
)
- @PostMapping("/login/kakao")
@CustomExceptionDescription(KAKAO_LOGIN)
+ @PostMapping("/login/kakao")
public BaseResponse kakaoLogin(@RequestBody KakaoLoginRequest request){
- log.info("[kakaoLogin]");
- return BaseResponse.ok(authService.kakaoLogin(request));
+ return BaseResponse.ok(authServiceFacade.kakaoLogin(request));
}
@Operation(
summary = "게스트 로그인 API",
description = "디바이스id 식별자를 이용해서 유저 정보와 엑세스 토큰을 얻을 수 있습니다. 기존 게스트가 아니면 별도의 가입 API 호출 없이 정보가 자동으로 저장됩니다."
)
- @PostMapping("/login/guest")
@CustomExceptionDescription(GUEST_LOGIN)
+ @PostMapping("/login/guest")
public BaseResponse guestLogin(@RequestBody GuestLoginRequest request){
log.info("[guestLogin] deviceId = {}", request.deviceId());
- return BaseResponse.ok(authService.guestLogin(request));
+ return BaseResponse.ok(authServiceFacade.guestLogin(request));
+ }
+
+ @Operation(
+ summary = "토큰 재발급 API",
+ description = "만료되지 않은 리프레시 토큰으로 요청하면 새로운 엑세스 토큰과 리프레시 토큰을 얻을 수 있습니다."
+ )
+ @CustomExceptionDescription(REISSUE_TOKEN)
+ @PostMapping("/reissue/token")
+ public BaseResponse reissueToken(@RequestBody ReissueTokenRequest request){
+ return BaseResponse.ok(authServiceFacade.reissueToken(request));
}
}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/ReissueTokenRequest.java b/src/main/java/com/kuit/findyou/domain/auth/dto/ReissueTokenRequest.java
new file mode 100644
index 00000000..5882c20c
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/dto/ReissueTokenRequest.java
@@ -0,0 +1,6 @@
+package com.kuit.findyou.domain.auth.dto;
+
+public record ReissueTokenRequest(
+ String refreshToken
+) {
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/ReissueTokenResponse.java b/src/main/java/com/kuit/findyou/domain/auth/dto/ReissueTokenResponse.java
new file mode 100644
index 00000000..deec42df
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/dto/ReissueTokenResponse.java
@@ -0,0 +1,7 @@
+package com.kuit.findyou.domain.auth.dto;
+
+public record ReissueTokenResponse(
+ String accessToken,
+ String refreshToken
+) {
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/response/GuestLoginResponse.java b/src/main/java/com/kuit/findyou/domain/auth/dto/response/GuestLoginResponse.java
index f25e6a72..8e725206 100644
--- a/src/main/java/com/kuit/findyou/domain/auth/dto/response/GuestLoginResponse.java
+++ b/src/main/java/com/kuit/findyou/domain/auth/dto/response/GuestLoginResponse.java
@@ -7,6 +7,8 @@ public record GuestLoginResponse (
@Schema(description = "유저 식별자")
Long userId,
@Schema(description = "엑세스 토큰")
- String accessToken
+ String accessToken,
+ @Schema(description = "리프레시 토큰")
+ String refreshToken
){
}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/response/KakaoLoginResponse.java b/src/main/java/com/kuit/findyou/domain/auth/dto/response/KakaoLoginResponse.java
index 2893c137..7c911c5a 100644
--- a/src/main/java/com/kuit/findyou/domain/auth/dto/response/KakaoLoginResponse.java
+++ b/src/main/java/com/kuit/findyou/domain/auth/dto/response/KakaoLoginResponse.java
@@ -10,12 +10,12 @@ public record KakaoLoginResponse(
@Schema(description = "첫 로그인 여부 = 회원가입 여부", example = "false")
Boolean isFirstLogin
) {
- public static KakaoLoginResponse fromUserAndAccessToken(User user, String accessToken) {
- UserInfoDto userInfo = new UserInfoDto(user.getId(), user.getName(), accessToken);
+ public static KakaoLoginResponse fromUserAndTokens(User user, String accessToken, String refreshToken) {
+ UserInfoDto userInfo = new UserInfoDto(user.getId(), user.getName(), accessToken, refreshToken);
return new KakaoLoginResponse(userInfo, false);
}
- public static KakaoLoginResponse notFound() {
+ public static KakaoLoginResponse firstLogin() {
return new KakaoLoginResponse(null, true);
}
@@ -25,7 +25,9 @@ public record UserInfoDto(
@Schema(description = "사용자 닉네임", example = "유저1")
String nickname,
@Schema(description = "찾아유 엑세스 토큰", example = "token1234token1234token1234")
- String accessToken
+ String accessToken,
+ @Schema(description = "찾아유 리프레시 토큰", example = "token1234token1234token1234")
+ String refreshToken
) {
}
}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java
new file mode 100644
index 00000000..7cec75a2
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java
@@ -0,0 +1,9 @@
+package com.kuit.findyou.domain.auth.repository;
+
+import java.util.Optional;
+
+public interface RedisRefreshTokenRepository {
+ Optional findByUserId(Long userId);
+
+ void save(Long id, String refreshToken);
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java
new file mode 100644
index 00000000..4d077b69
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java
@@ -0,0 +1,33 @@
+package com.kuit.findyou.domain.auth.repository;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.Duration;
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Repository
+public class RedisRefreshTokenRepositoryImpl implements RedisRefreshTokenRepository {
+ private final RedisTemplate redisTemplate;
+ private final String refreshTokenKeyPrefix = "refresh-token:";
+ @Value("${findyou.jwt.expiration-ms.refresh-token}")
+ private Long refreshTokenExpireMs;
+
+ private String key(Long userId) {
+ return refreshTokenKeyPrefix + userId;
+ }
+
+ @Override
+ public Optional findByUserId(Long userId) {
+ String value = redisTemplate.opsForValue().get(key(userId));
+ return Optional.ofNullable(value);
+ }
+
+ @Override
+ public void save(Long userId, String refreshToken) {
+ redisTemplate.opsForValue().set(key(userId), refreshToken, Duration.ofMillis(refreshTokenExpireMs));
+ }
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java b/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java
new file mode 100644
index 00000000..d6e73592
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java
@@ -0,0 +1,29 @@
+package com.kuit.findyou.domain.auth.service;
+
+import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
+import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest;
+import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
+import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse;
+import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class AuthServiceFacade {
+ private final LoginService loginService;
+ private final ReissueTokenService reissueTokenService;
+
+ public KakaoLoginResponse kakaoLogin(KakaoLoginRequest request) {
+ return loginService.kakaoLogin(request);
+ }
+
+ public GuestLoginResponse guestLogin(GuestLoginRequest request) {
+ return loginService.guestLogin(request);
+ }
+
+ public ReissueTokenResponse reissueToken(ReissueTokenRequest request) {
+ return reissueTokenService.reissueToken(request);
+ }
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/IssueTokenService.java b/src/main/java/com/kuit/findyou/domain/auth/service/IssueTokenService.java
new file mode 100644
index 00000000..d51302eb
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/IssueTokenService.java
@@ -0,0 +1,8 @@
+package com.kuit.findyou.domain.auth.service;
+
+import com.kuit.findyou.domain.user.model.Role;
+
+public interface IssueTokenService {
+ String issueAccessToken(Long userId, Role role);
+ String issueRefreshToken(Long userId);
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/IssueTokenServiceImpl.java b/src/main/java/com/kuit/findyou/domain/auth/service/IssueTokenServiceImpl.java
new file mode 100644
index 00000000..cabc0028
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/IssueTokenServiceImpl.java
@@ -0,0 +1,26 @@
+package com.kuit.findyou.domain.auth.service;
+
+import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository;
+import com.kuit.findyou.domain.user.model.Role;
+import com.kuit.findyou.global.jwt.util.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class IssueTokenServiceImpl implements IssueTokenService {
+ private final JwtUtil jwtUtil;
+ private final RedisRefreshTokenRepository redisRefreshTokenRepository;
+
+ @Override
+ public String issueAccessToken(Long userId, Role role) {
+ return jwtUtil.createAccessJwt(userId, role);
+ }
+
+ @Override
+ public String issueRefreshToken(Long userId) {
+ String refreshToken = jwtUtil.createRefreshJwt(userId);
+ redisRefreshTokenRepository.save(userId, refreshToken);
+ return refreshToken;
+ }
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AuthService.java b/src/main/java/com/kuit/findyou/domain/auth/service/LoginService.java
similarity index 93%
rename from src/main/java/com/kuit/findyou/domain/auth/service/AuthService.java
rename to src/main/java/com/kuit/findyou/domain/auth/service/LoginService.java
index 01828b79..536b2d52 100644
--- a/src/main/java/com/kuit/findyou/domain/auth/service/AuthService.java
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/LoginService.java
@@ -5,7 +5,7 @@
import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
-public interface AuthService {
+public interface LoginService {
KakaoLoginResponse kakaoLogin(KakaoLoginRequest request);
GuestLoginResponse guestLogin(GuestLoginRequest request);
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/kuit/findyou/domain/auth/service/LoginServiceImpl.java
similarity index 64%
rename from src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceImpl.java
rename to src/main/java/com/kuit/findyou/domain/auth/service/LoginServiceImpl.java
index ba7f167c..4e22c425 100644
--- a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceImpl.java
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/LoginServiceImpl.java
@@ -9,7 +9,6 @@
import com.kuit.findyou.domain.user.model.User;
import com.kuit.findyou.domain.user.repository.UserRepository;
import com.kuit.findyou.global.common.exception.CustomException;
-import com.kuit.findyou.global.jwt.util.JwtUtil;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -20,21 +19,23 @@
@Slf4j
@RequiredArgsConstructor
@Service
-public class AuthServiceImpl implements AuthService {
+public class LoginServiceImpl implements LoginService {
private final UserRepository userRepository;
- private final JwtUtil jwtUtil;
+ private final IssueTokenService issueTokenService;
+
public KakaoLoginResponse kakaoLogin(KakaoLoginRequest request) {
log.info("[kakaoLogin] kakaoId = {}", request.kakaoId());
return userRepository.findByKakaoId(request.kakaoId())
- .map(loginUser -> {
- log.info("[kakaoLogin] user found");
- String token = jwtUtil.createAccessJwt(loginUser.getId(), loginUser.getRole());
- return KakaoLoginResponse.fromUserAndAccessToken(loginUser, token);
+ .map(user -> {
+ String accessToken = issueTokenService.issueAccessToken(user.getId(), user.getRole());
+ String refreshToken = issueTokenService.issueRefreshToken(user.getId());
+ log.info("[kakaoLogin] 카카오 로그인 성공");
+ return KakaoLoginResponse.fromUserAndTokens(user, accessToken, refreshToken);
})
.orElseGet(() -> {
- log.info("[kakaoLogin] user not found");
- return KakaoLoginResponse.notFound();
+ log.info("[kakaoLogin] 일치하는 유저가 없어서 카카오 로그인 실패");
+ return KakaoLoginResponse.firstLogin();
});
}
@@ -46,6 +47,7 @@ public GuestLoginResponse guestLogin(GuestLoginRequest request) {
User user = userRepository.findByDeviceId(request.deviceId())
.orElseGet(()->{
// 디바이스 id에 해당하는 유저가 없으면 게스트 추가
+ log.info("[guestLogin] 새로운 게스트 추가");
User build = User.builder()
.name("게스트")
.profileImageUrl(DefaultProfileImage.DEFAULT.getName())
@@ -57,11 +59,14 @@ public GuestLoginResponse guestLogin(GuestLoginRequest request) {
// 게스트가 아니면 로그인 실패
if(!user.isGuest()){
+ log.info("[guestLogin] 게스트 권한이 없어서 게스트 로그인 실패");
throw new CustomException(GUEST_LOGIN_FAILED);
}
- // 응답 반환
- String accessToken = jwtUtil.createAccessJwt(user.getId(), user.getRole());
- return new GuestLoginResponse(user.getId(), accessToken);
+ // 토큰 생성
+ String accessToken = issueTokenService.issueAccessToken(user.getId(), user.getRole());
+ String refreshToken = issueTokenService.issueRefreshToken(user.getId());
+ log.info("[guestLogin] 게스트 로그인 성공");
+ return new GuestLoginResponse(user.getId(), accessToken, refreshToken);
}
}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/ReissueTokenService.java b/src/main/java/com/kuit/findyou/domain/auth/service/ReissueTokenService.java
new file mode 100644
index 00000000..e32c986e
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/ReissueTokenService.java
@@ -0,0 +1,8 @@
+package com.kuit.findyou.domain.auth.service;
+
+import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
+
+public interface ReissueTokenService {
+ ReissueTokenResponse reissueToken(ReissueTokenRequest request);
+}
diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/ReissueTokenServiceImpl.java b/src/main/java/com/kuit/findyou/domain/auth/service/ReissueTokenServiceImpl.java
new file mode 100644
index 00000000..7e848d21
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/auth/service/ReissueTokenServiceImpl.java
@@ -0,0 +1,55 @@
+package com.kuit.findyou.domain.auth.service;
+
+import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
+import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository;
+import com.kuit.findyou.domain.user.model.User;
+import com.kuit.findyou.domain.user.repository.UserRepository;
+import com.kuit.findyou.global.common.exception.CustomException;
+import com.kuit.findyou.global.jwt.util.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ReissueTokenServiceImpl implements ReissueTokenService {
+ private final JwtUtil jwtUtil;
+ private final RedisRefreshTokenRepository redisRefreshTokenRepository;
+ private final UserRepository userRepository;
+ @Override
+ public ReissueTokenResponse reissueToken(ReissueTokenRequest request) {
+ log.info("[reissueToken] 토큰 재발급 시작");
+ // 리프레시 토큰 검증
+ jwtUtil.validateJwt(request.refreshToken());
+
+ Long userId = jwtUtil.getUserId(request.refreshToken());
+
+ // 저장된 리프레시 토큰이 없으면 에러
+ String foundRefreshToken = redisRefreshTokenRepository.findByUserId(userId)
+ .orElseThrow(() -> {
+ log.info("[reissueToken] 토큰이 존재하지 않음");
+ return new CustomException(REFRESH_TOKEN_NOT_FOUND);
+ });
+
+ // 토큰이 일차히지 않으면 에러
+ if(!foundRefreshToken.equals(request.refreshToken())){
+ log.info("[reissueToken] 토큰이 일치하지 않음");
+ throw new CustomException(REFRESH_TOKEN_NOT_FOUND);
+ }
+
+ // 토큰이 일치하면 토큰 재발급
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ String accessToken = jwtUtil.createAccessJwt(user.getId(), user.getRole());
+ String refreshToken = jwtUtil.createRefreshJwt(user.getId());
+
+ redisRefreshTokenRepository.save(user.getId(), refreshToken);
+
+ log.info("[reissueToken] 토큰 재발급 완료");
+ return new ReissueTokenResponse(accessToken, refreshToken);
+ }
+}
diff --git a/src/main/java/com/kuit/findyou/domain/report/controller/ReportController.java b/src/main/java/com/kuit/findyou/domain/report/controller/ReportController.java
index e8c79d76..9e69ac00 100644
--- a/src/main/java/com/kuit/findyou/domain/report/controller/ReportController.java
+++ b/src/main/java/com/kuit/findyou/domain/report/controller/ReportController.java
@@ -7,8 +7,10 @@
import com.kuit.findyou.domain.report.dto.response.MissingReportDetailResponseDTO;
import com.kuit.findyou.domain.report.dto.response.ProtectingReportDetailResponseDTO;
import com.kuit.findyou.domain.report.dto.response.WitnessReportDetailResponseDTO;
-import com.kuit.findyou.domain.report.model.*;
+import com.kuit.findyou.domain.report.model.ReportTag;
import com.kuit.findyou.domain.report.service.facade.ReportServiceFacade;
+import com.kuit.findyou.domain.report.service.retrieve.MissingReportRetrieveWithS3Service;
+import com.kuit.findyou.domain.report.service.retrieve.ProtectingReportRetrieveWithS3Service;
import com.kuit.findyou.global.common.annotation.CustomExceptionDescription;
import com.kuit.findyou.global.common.response.BaseResponse;
import com.kuit.findyou.global.jwt.annotation.LoginUserId;
@@ -16,11 +18,17 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.*;
@RestController
@@ -28,9 +36,12 @@
@RequestMapping("/api/v2/reports")
@Tag(name = "Report", description = "글 관련 API")
@RequiredArgsConstructor
+@Validated
public class ReportController {
private final ReportServiceFacade reportServiceFacade;
+ private final ProtectingReportRetrieveWithS3Service protectingReportRetrieveWithS3Service;
+ private final MissingReportRetrieveWithS3Service missingReportRetrieveWithS3Service;
@Operation(summary = "보호글 상세 조회 API", description = "보호글의 정보를 상세 조회하기 위한 API")
@GetMapping("/protecting-reports/{reportId}")
@@ -108,5 +119,39 @@ public BaseResponse deleteReport(
return BaseResponse.ok(null);
}
+ @Operation(summary = "보호글 S3 이미지 포함 랜덤 조회 API", description = "랜덤으로 보호글을 선택하여 원본 이미지를 S3에 업로드 후, S3 URL 포함 보호글을 리스트로 반환합니다.")
+ @GetMapping("/protecting-reports/random-s3")
+ @CustomExceptionDescription(DEFAULT)
+ public ResponseEntity> getRandomProtectingReportsWithS3(
+ @RequestParam(name = "count", defaultValue = "1")
+ @Min(1) @Max(10) int count
+ ) {
+ List details =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(count);
+
+ if (details.isEmpty()) {
+ return ResponseEntity.noContent().build();
+ }
+ return ResponseEntity.ok(BaseResponse.ok(details));
+ }
+
+ @Operation(
+ summary = "실종글 S3 이미지 포함 랜덤 조회 API", description = "랜덤으로 실종글을 선택하여 원본 이미지를 S3에 업로드 후, S3 URL 포함 실종글을 리스트로 반환합니다."
+ )
+ @GetMapping("/missing-reports/random-s3")
+ @CustomExceptionDescription(DEFAULT)
+ public ResponseEntity> getRandomMissingReportsWithS3(
+ @RequestParam(name = "count", defaultValue = "1")
+ @Min(1) @Max(10) int count
+ ) {
+ List details =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(count);
+
+ if (details.isEmpty()) {
+ return ResponseEntity.noContent().build();
+ }
+ return ResponseEntity.ok(BaseResponse.ok(details));
+ }
+
}
diff --git a/src/main/java/com/kuit/findyou/domain/report/dto/response/Card.java b/src/main/java/com/kuit/findyou/domain/report/dto/response/Card.java
index d9dbeba8..e576ebfa 100644
--- a/src/main/java/com/kuit/findyou/domain/report/dto/response/Card.java
+++ b/src/main/java/com/kuit/findyou/domain/report/dto/response/Card.java
@@ -14,6 +14,8 @@ public record Card(
String tag,
@Schema(description = "날짜 (발견 날짜/분실 날짜/목격 날짜)", example = "2025-07-01")
String date,
+ @Schema(description = "데이터 생성일", example = "2025-12-11")
+ String createdAt,
@Schema(description = "장소 (발견 장소/분실 장소/목격 장소)", example = "성산구 내동 628-1")
String location,
@Schema(description = "관심 여부", example = "true")
diff --git a/src/main/java/com/kuit/findyou/domain/report/dto/response/CardResponseDTO.java b/src/main/java/com/kuit/findyou/domain/report/dto/response/CardResponseDTO.java
index 43213a59..8660047c 100644
--- a/src/main/java/com/kuit/findyou/domain/report/dto/response/CardResponseDTO.java
+++ b/src/main/java/com/kuit/findyou/domain/report/dto/response/CardResponseDTO.java
@@ -18,6 +18,7 @@ public record CardResponseDTO(
"title": "말티즈",
"tag": "보호중",
"date": "2025-07-01",
+ "createdAt": "2025-12-11",
"location": "성산구 내동 628-1",
"interest": true
},
@@ -27,6 +28,7 @@ public record CardResponseDTO(
"title": "푸들",
"tag": "실종신고",
"date": "2025-06-30",
+ "createdAt": "2025-12-11",
"location": "강남구 논현동",
"interest": false
}
diff --git a/src/main/java/com/kuit/findyou/domain/report/dto/response/ReportProjection.java b/src/main/java/com/kuit/findyou/domain/report/dto/response/ReportProjection.java
index 091ec01a..fb4930d1 100644
--- a/src/main/java/com/kuit/findyou/domain/report/dto/response/ReportProjection.java
+++ b/src/main/java/com/kuit/findyou/domain/report/dto/response/ReportProjection.java
@@ -1,6 +1,7 @@
package com.kuit.findyou.domain.report.dto.response;
import java.time.LocalDate;
+import java.time.LocalDateTime;
public interface ReportProjection {
Long getReportId();
@@ -8,5 +9,6 @@ public interface ReportProjection {
String getTitle();
String getTag();
LocalDate getDate();
+ LocalDateTime getCreatedAt();
String getAddress();
}
diff --git a/src/main/java/com/kuit/findyou/domain/report/factory/CardFactory.java b/src/main/java/com/kuit/findyou/domain/report/factory/CardFactory.java
index bab12941..86ac1ac5 100644
--- a/src/main/java/com/kuit/findyou/domain/report/factory/CardFactory.java
+++ b/src/main/java/com/kuit/findyou/domain/report/factory/CardFactory.java
@@ -27,6 +27,7 @@ public CardResponseDTO createCardResponse(
p.getTitle(),
ReportTag.valueOf(p.getTag()).getValue(),
ReportFormatUtil.safeDate(p.getDate()),
+ p.getCreatedAt().toLocalDate().toString(),
p.getAddress(),
interestIds.contains(p.getReportId())
))
diff --git a/src/main/java/com/kuit/findyou/domain/report/repository/InterestReportRepository.java b/src/main/java/com/kuit/findyou/domain/report/repository/InterestReportRepository.java
index 53c0be35..d9462667 100644
--- a/src/main/java/com/kuit/findyou/domain/report/repository/InterestReportRepository.java
+++ b/src/main/java/com/kuit/findyou/domain/report/repository/InterestReportRepository.java
@@ -33,6 +33,7 @@ boolean existsByReportIdAndUserId(@Param("reportId") Long reportId,
ir.report.breed AS breed,
ir.report.tag AS tag,
ir.report.date AS date,
+ ir.report.createdAt AS createdAt,
ir.report.address AS address
FROM InterestReport ir JOIN ir.report
WHERE ir.id < :lastId AND ir.user.id = :userId
diff --git a/src/main/java/com/kuit/findyou/domain/report/repository/MissingReportRepository.java b/src/main/java/com/kuit/findyou/domain/report/repository/MissingReportRepository.java
index c48d3c1e..a21bd0dc 100644
--- a/src/main/java/com/kuit/findyou/domain/report/repository/MissingReportRepository.java
+++ b/src/main/java/com/kuit/findyou/domain/report/repository/MissingReportRepository.java
@@ -1,11 +1,12 @@
package com.kuit.findyou.domain.report.repository;
import com.kuit.findyou.domain.report.model.MissingReport;
-import com.kuit.findyou.domain.report.model.ProtectingReport;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
+import java.time.LocalDate;
+import java.util.List;
import java.util.Optional;
@@ -14,4 +15,6 @@ public interface MissingReportRepository extends JpaRepository findWithImagesById(Long id);
+
+ List findByDate(LocalDate date);
}
diff --git a/src/main/java/com/kuit/findyou/domain/report/repository/ProtectingReportRepository.java b/src/main/java/com/kuit/findyou/domain/report/repository/ProtectingReportRepository.java
index 833634eb..5f507529 100644
--- a/src/main/java/com/kuit/findyou/domain/report/repository/ProtectingReportRepository.java
+++ b/src/main/java/com/kuit/findyou/domain/report/repository/ProtectingReportRepository.java
@@ -5,6 +5,8 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -16,5 +18,7 @@ public interface ProtectingReportRepository extends JpaRepository findWithImagesById(Long id);
List findByNoticeNumberIn(Set noticeNumbers);
+
+ List findByDate(LocalDate date);
}
diff --git a/src/main/java/com/kuit/findyou/domain/report/repository/ReportRepository.java b/src/main/java/com/kuit/findyou/domain/report/repository/ReportRepository.java
index a700af31..287f5e04 100644
--- a/src/main/java/com/kuit/findyou/domain/report/repository/ReportRepository.java
+++ b/src/main/java/com/kuit/findyou/domain/report/repository/ReportRepository.java
@@ -32,6 +32,7 @@ public interface ReportRepository extends JpaRepository {
r.breed AS title,
r.tag AS tag,
r.date AS date,
+ r.createdAt AS createdAt,
r.address AS address
FROM Report r
WHERE r.id < :lastId
@@ -67,6 +68,7 @@ Slice findReportsWithFilters(
r.breed AS title,
r.tag AS tag,
r.date AS date,
+ r.createdAt AS createdAt,
r.address AS address
FROM Report r
WHERE r.id IN :ids
@@ -80,6 +82,7 @@ Slice findReportsWithFilters(
r.breed AS title,
r.tag AS tag,
r.date AS date,
+ r.createdAt AS createdAt,
r.address AS address
FROM Report r
WHERE r.latitude IS NOT NULL AND r.longitude IS NOT NULL AND r.tag IN :tags
@@ -105,6 +108,7 @@ List findNearestReports(
r.breed AS title,
r.tag AS tag,
r.date AS date,
+ r.createdAt AS createdAt,
r.address AS address
FROM Report r
WHERE r.user.id = :userId AND r.id < :lastId
diff --git a/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/MissingReportDetailStrategy.java b/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/MissingReportDetailStrategy.java
index 60fe0424..ca63bea0 100644
--- a/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/MissingReportDetailStrategy.java
+++ b/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/MissingReportDetailStrategy.java
@@ -8,7 +8,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
-import java.math.BigDecimal;
+import java.util.List;
import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.MISSING_REPORT_NOT_FOUND;
@@ -39,6 +39,25 @@ public MissingReportDetailResponseDTO getDetail(MissingReport report, boolean in
interest
);
}
+ public MissingReportDetailResponseDTO toDetailDto(MissingReport report, List imageUrls, boolean interest) {
+ return new MissingReportDetailResponseDTO(
+ imageUrls,
+ ReportFormatUtil.safeValue(report.getBreed()),
+ report.getTag().getValue(),
+ ReportFormatUtil.safeValue(report.getAge()),
+ ReportFormatUtil.safeSex(report.getSex()),
+ ReportFormatUtil.safeDate(report.getDate()),
+ ReportFormatUtil.safeValue(report.getRfid()),
+ ReportFormatUtil.safeValue(report.getSignificant()),
+ ReportFormatUtil.safeValue(report.getLandmark()),
+ ReportFormatUtil.safeValue(report.getAddress()),
+ ReportFormatUtil.formatCoordinate(report.getLatitude()),
+ ReportFormatUtil.formatCoordinate(report.getLongitude()),
+ ReportFormatUtil.safeValue(report.getReporterName()),
+ ReportFormatUtil.safeValue(report.getReporterTel()),
+ interest
+ );
+ }
@Override
public MissingReport getReport(Long reportId) {
diff --git a/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/ProtectingReportDetailStrategy.java b/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/ProtectingReportDetailStrategy.java
index fa4151e5..4648861b 100644
--- a/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/ProtectingReportDetailStrategy.java
+++ b/src/main/java/com/kuit/findyou/domain/report/service/detail/strategy/ProtectingReportDetailStrategy.java
@@ -2,14 +2,13 @@
import com.kuit.findyou.domain.report.dto.response.ProtectingReportDetailResponseDTO;
import com.kuit.findyou.domain.report.model.ProtectingReport;
-import com.kuit.findyou.domain.report.model.Report;
import com.kuit.findyou.domain.report.repository.ProtectingReportRepository;
import com.kuit.findyou.domain.report.util.ReportFormatUtil;
import com.kuit.findyou.global.common.exception.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
-import java.math.BigDecimal;
+import java.util.List;
import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.PROTECTING_REPORT_NOT_FOUND;
@@ -21,9 +20,17 @@ public class ProtectingReportDetailStrategy implements ReportDetailStrategy imageUrls,
+ boolean interest
+ ) {
return new ProtectingReportDetailResponseDTO(
- report.getReportImagesUrlList(),
+ imageUrls,
ReportFormatUtil.safeValue(report.getBreed()),
report.getTag().getValue(),
ReportFormatUtil.formatAge(report.getAge()),
@@ -46,7 +53,6 @@ public ProtectingReportDetailResponseDTO getDetail(ProtectingReport report, bool
);
}
-
@Override
public ProtectingReport getReport(Long reportId) {
return protectingReportRepository.findWithImagesById(reportId)
diff --git a/src/main/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3Service.java b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3Service.java
new file mode 100644
index 00000000..69586e93
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3Service.java
@@ -0,0 +1,9 @@
+package com.kuit.findyou.domain.report.service.retrieve;
+
+import com.kuit.findyou.domain.report.dto.response.MissingReportDetailResponseDTO;
+
+import java.util.List;
+
+public interface MissingReportRetrieveWithS3Service {
+ List getRandomMissingReportsWithS3(int count);
+}
diff --git a/src/main/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3ServiceImpl.java b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3ServiceImpl.java
new file mode 100644
index 00000000..66e20409
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3ServiceImpl.java
@@ -0,0 +1,59 @@
+package com.kuit.findyou.domain.report.service.retrieve;
+
+import com.kuit.findyou.domain.image.model.ReportImage;
+import com.kuit.findyou.domain.report.dto.response.MissingReportDetailResponseDTO;
+import com.kuit.findyou.domain.report.model.MissingReport;
+import com.kuit.findyou.domain.report.repository.MissingReportRepository;
+import com.kuit.findyou.domain.report.service.detail.strategy.MissingReportDetailStrategy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class MissingReportRetrieveWithS3ServiceImpl implements MissingReportRetrieveWithS3Service{
+ private final MissingReportRepository missingReportRepository;
+ private final MissingReportDetailStrategy missingReportDetailStrategy;
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getRandomMissingReportsWithS3(int count) {
+ LocalDate yesterday = LocalDate.now().minusDays(1);
+
+ List allReports = missingReportRepository.findByDate(yesterday);
+
+ if (allReports.isEmpty()) {
+ // 204 no content
+ return Collections.emptyList();
+ }
+
+ Collections.shuffle(allReports);
+
+ int limit = Math.max(1, count);
+ List selectedReports = allReports.stream().limit(limit).toList();
+
+ List result = new ArrayList<>();
+
+ for (MissingReport report : selectedReports) {
+
+ List imageUrls = report.getReportImages().stream()
+ .map(ReportImage::getImageUrl)
+ .filter(url -> url != null && !url.isBlank())
+ .distinct()
+ .toList();
+
+ MissingReportDetailResponseDTO dto =
+ missingReportDetailStrategy.toDetailDto(report, imageUrls,false);
+ result.add(dto);
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3Service.java b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3Service.java
new file mode 100644
index 00000000..1757a277
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3Service.java
@@ -0,0 +1,9 @@
+package com.kuit.findyou.domain.report.service.retrieve;
+
+import com.kuit.findyou.domain.report.dto.response.ProtectingReportDetailResponseDTO;
+
+import java.util.List;
+
+public interface ProtectingReportRetrieveWithS3Service {
+ List getRandomProtectingReportsWithS3(int count);
+}
diff --git a/src/main/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3ServiceImpl.java b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3ServiceImpl.java
new file mode 100644
index 00000000..7b2cf666
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3ServiceImpl.java
@@ -0,0 +1,91 @@
+package com.kuit.findyou.domain.report.service.retrieve;
+
+import com.kuit.findyou.domain.image.model.ReportImage;
+import com.kuit.findyou.domain.report.dto.response.ProtectingReportDetailResponseDTO;
+import com.kuit.findyou.domain.report.model.ProtectingReport;
+import com.kuit.findyou.domain.report.repository.ProtectingReportRepository;
+import com.kuit.findyou.domain.report.service.detail.strategy.ProtectingReportDetailStrategy;
+import com.kuit.findyou.global.infrastructure.FileUploadingFailedException;
+import com.kuit.findyou.global.infrastructure.ImageUploader;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class ProtectingReportRetrieveWithS3ServiceImpl implements ProtectingReportRetrieveWithS3Service{
+ private final ProtectingReportRepository protectingReportRepository;
+ private final ImageUploader imageUploader;
+ private final ProtectingReportDetailStrategy protectingReportDetailStrategy;
+
+ //외부 URL에서 이미지를 받아오기 위한 HTTP 클라이언트
+ private final RestTemplate restTemplate;
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getRandomProtectingReportsWithS3(int count) {
+
+ LocalDate yesterday = LocalDate.now().minusDays(1);
+ List allReports = protectingReportRepository.findByDate(yesterday);
+
+ if(allReports.isEmpty()) {
+ //204 no content
+ return Collections.emptyList();
+ }
+
+ Collections.shuffle(allReports);
+
+ int limit = Math.max(1, count);
+ List selectedReports = allReports.stream().limit(limit).toList();
+
+ List result = new ArrayList<>();
+
+ for(ProtectingReport report : selectedReports) {
+
+ //보호글에 연결된 원본 이미지 가져오기
+ List reportImages = report.getReportImages();
+ List originalUrls = reportImages.stream()
+ .map(ReportImage::getImageUrl)
+ .toList();
+
+ //S3에 업로드허기
+ List s3Urls = new ArrayList<>();
+ for (String url : originalUrls) {
+ try {
+ byte[] imageBytes = restTemplate.getForObject(url, byte[].class);
+ if (imageBytes == null || imageBytes.length == 0) {
+ log.warn("[ProtectingReportS3] 빈 이미지 응답: url={}", url);
+ continue;
+ }
+
+ String key = "protecting/" + report.getId() + "/" + UUID.randomUUID() + ".jpg";
+ String s3Url = imageUploader.upload(imageBytes, key, "image/jpeg"); // 너네 S3 업로더 방식대로
+ s3Urls.add(s3Url);
+
+ } catch (FileUploadingFailedException e) {
+ //S3 자체 장애, 권한 등의 문제 발생 시
+ log.error("[ProtectingReportS3] S3 업로드 실패 - reportId={}, url={}, message={}",
+ report.getId(), url, e.getMessage(), e);
+ } catch (Exception e) {
+ log.error("[ProtectingReportS3] 이미지 처리 중 예기치 못한 오류 발생 - reportId={}, url={}",
+ report.getId(), url, e);
+ }
+ }
+
+ ProtectingReportDetailResponseDTO dto =
+ protectingReportDetailStrategy.toDetailDto(report, s3Urls, false);
+
+ result.add(dto);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/kuit/findyou/domain/user/controller/UserController.java b/src/main/java/com/kuit/findyou/domain/user/controller/UserController.java
index 4099b0bd..7f9c9b1e 100644
--- a/src/main/java/com/kuit/findyou/domain/user/controller/UserController.java
+++ b/src/main/java/com/kuit/findyou/domain/user/controller/UserController.java
@@ -3,6 +3,7 @@
import com.kuit.findyou.domain.report.dto.response.CardResponseDTO;
import com.kuit.findyou.domain.user.dto.request.*;
import com.kuit.findyou.domain.user.dto.request.AddInterestAnimalRequest;
+import com.kuit.findyou.domain.user.dto.response.CheckGuestResponse;
import com.kuit.findyou.domain.user.dto.response.GetUserProfileResponse;
import com.kuit.findyou.domain.user.dto.request.ChangeNicknameRequestDTO;
import com.kuit.findyou.domain.user.dto.request.CheckDuplicateNicknameRequest;
@@ -24,7 +25,6 @@
import lombok.extern.slf4j.Slf4j;
import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.*;
-import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
@Slf4j
@RestController
@@ -64,15 +64,12 @@ public BaseResponse retrieveViewedAnimals (
@Operation(
summary = "회원정보 등록 API",
description = """
- 회원 정보를 등록합니다. 회원 등록에 성공하면 유저 정보(식별자와 닉네임)와 엑세스 토큰을 얻을 수 있습니다. \n
- **[중요] profileImageFile과 defaultProfileImageName 중 하나만 선택해야 합니다.** \n
- - profileImageFile을 업로드하면 defaultProfileImageName은 무시됩니다. \n
- - 둘 다 null이면 에러가 발생합니다.
+ 회원 정보를 등록합니다. 회원 등록에 성공하면 유저 정보(식별자와 닉네임)와 엑세스 토큰을 얻을 수 있습니다.
"""
)
@CustomExceptionDescription(REGISTER_USER)
- @PostMapping(consumes = MULTIPART_FORM_DATA_VALUE)
- public BaseResponse registerUser(@ModelAttribute RegisterUserRequest request){
+ @PostMapping
+ public BaseResponse registerUser(@RequestBody RegisterUserRequest request){
log.info("[registerUser] kakaoId = {}", request.kakaoId());
return new BaseResponse<>(userServiceFacade.registerUser(request));
}
@@ -141,21 +138,6 @@ public BaseResponse deleteInterestAnimal(@Parameter(hidden = true) @LoginU
return BaseResponse.ok(null);
}
- @Operation(
- summary = "프로필 이미지 변경 API",
- description = "프로필 이미지 변경을 수행합니다. 기본이미지는 enum값 이름으로 저장, 사용자 업로드 이미지는 cdn url로 저장됩니다."
- )
- @CustomExceptionDescription(CHANGE_PROFILE_IMAGE)
- @PreAuthorize("hasRole('ROLE_USER')")
- @PatchMapping(value = "/me/profile-image", consumes = MULTIPART_FORM_DATA_VALUE)
- public BaseResponse changeProfileImage(
- @LoginUserId Long userId,
- @Valid @ModelAttribute ChangeProfileImageRequest req
- ) {
- userServiceFacade.changeProfileImage(userId, req);
- return BaseResponse.ok(null);
- }
-
@Operation(
summary = "신고 내역 조회 API",
description = """
@@ -180,4 +162,14 @@ public BaseResponse retrieveUserReports(@Parameter(hidden = tru
public BaseResponse getUserProfile(@Parameter(hidden = true) @LoginUserId Long userId){
return BaseResponse.ok(userServiceFacade.getUserProfile(userId));
}
+
+ @Operation(
+ summary = "게스트 여부 조회 API",
+ description = "사용자가 게스트인지 여부를 조회합니다. 게스트이면 true를 반환합니다"
+ )
+ @CustomExceptionDescription(DEFAULT)
+ @PostMapping("/me/check/guest")
+ public BaseResponse checkGuest(@LoginUserId Long userId){
+ return BaseResponse.ok(userServiceFacade.checkGuest(userId));
+ }
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/dto/request/RegisterUserRequest.java b/src/main/java/com/kuit/findyou/domain/user/dto/request/RegisterUserRequest.java
index b9861154..57de8b1c 100644
--- a/src/main/java/com/kuit/findyou/domain/user/dto/request/RegisterUserRequest.java
+++ b/src/main/java/com/kuit/findyou/domain/user/dto/request/RegisterUserRequest.java
@@ -9,19 +9,6 @@
""" )
@Builder
public record RegisterUserRequest(
- @Schema(description = """
- 사용자 지정 프로필 이미지 파일 (기본 프로필을 사용하지 않을 경우 업로드)
- """,
- type = "string",
- format = "binary",
- nullable = true)
- MultipartFile profileImageFile,
- @Schema(description = """
- 기본 프로필 이미지 이름 (profileImageFile이 없을 경우 선택)
- """,
- example = "default",
- nullable = true)
- String defaultProfileImageName,
@Schema(description = "회원 닉네임",
example = "찾아유",
required = true)
diff --git a/src/main/java/com/kuit/findyou/domain/user/dto/response/CheckGuestResponse.java b/src/main/java/com/kuit/findyou/domain/user/dto/response/CheckGuestResponse.java
new file mode 100644
index 00000000..93b64cfe
--- /dev/null
+++ b/src/main/java/com/kuit/findyou/domain/user/dto/response/CheckGuestResponse.java
@@ -0,0 +1,6 @@
+package com.kuit.findyou.domain.user.dto.response;
+
+public record CheckGuestResponse(
+ Boolean isGuest
+) {
+}
diff --git a/src/main/java/com/kuit/findyou/domain/user/dto/response/GetUserProfileResponse.java b/src/main/java/com/kuit/findyou/domain/user/dto/response/GetUserProfileResponse.java
index 81edb438..96f99403 100644
--- a/src/main/java/com/kuit/findyou/domain/user/dto/response/GetUserProfileResponse.java
+++ b/src/main/java/com/kuit/findyou/domain/user/dto/response/GetUserProfileResponse.java
@@ -5,8 +5,6 @@
@Schema(description = "마이페이지 프로필 조회 API 응답 DTO ")
public record GetUserProfileResponse(
@Schema(description = "닉네임", example = "유저1")
- String nickname,
- @Schema(description = "프로필 이미지", example = "image.png")
- String profileImage
+ String nickname
) {
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/dto/response/RegisterUserResponse.java b/src/main/java/com/kuit/findyou/domain/user/dto/response/RegisterUserResponse.java
index fe5f4db5..18a622cf 100644
--- a/src/main/java/com/kuit/findyou/domain/user/dto/response/RegisterUserResponse.java
+++ b/src/main/java/com/kuit/findyou/domain/user/dto/response/RegisterUserResponse.java
@@ -11,7 +11,10 @@ public record RegisterUserResponse(
example = "찾아유")
String nickname,
@Schema(description = "엑세스 토큰",
- example = "accessToken")
- String accessToken
+ example = "aaaa.bbbb.ccc")
+ String accessToken,
+ @Schema(description = "리프레시 토큰",
+ example = "aaaa.bbbb.ccc")
+ String refreshToken
) {
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/model/User.java b/src/main/java/com/kuit/findyou/domain/user/model/User.java
index 962345f4..04c01d27 100644
--- a/src/main/java/com/kuit/findyou/domain/user/model/User.java
+++ b/src/main/java/com/kuit/findyou/domain/user/model/User.java
@@ -11,8 +11,6 @@
import com.kuit.findyou.global.common.model.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.SQLRestriction;
import java.util.ArrayList;
import java.util.List;
@@ -100,7 +98,7 @@ public void addInterestReport(InterestReport interestReport) {
public void addSubscribe(Subscribe subscribe) { subscribes.add(subscribe); }
public void setFcmToken(FcmToken fcmToken) {this.fcmToken = fcmToken;}
- public void upgradeToMember(Long kakaoId, String nickname, String profileImageUrl){
+ public void upgradeToMember(Long kakaoId, String nickname){
// 비회원이어야 회원이 될 수 있음
if(this.role != Role.GUEST){
throw new CustomException(ALREADY_REGISTERED_USER);
@@ -108,7 +106,6 @@ public void upgradeToMember(Long kakaoId, String nickname, String profileImageUr
this.kakaoId = kakaoId;
this.name = nickname;
- this.profileImageUrl = profileImageUrl;
this.role = Role.USER;
}
@@ -119,5 +116,4 @@ public boolean isGuest(){
public void changeNickname(String newNickname) {
this.name = newNickname;
}
- public void changeProfileImage(String newImage) {this.profileImageUrl = newImage;}
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/service/change_profileImage/ChangeProfileImageService.java b/src/main/java/com/kuit/findyou/domain/user/service/change_profileImage/ChangeProfileImageService.java
deleted file mode 100644
index bf093d26..00000000
--- a/src/main/java/com/kuit/findyou/domain/user/service/change_profileImage/ChangeProfileImageService.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.kuit.findyou.domain.user.service.change_profileImage;
-
-import com.kuit.findyou.domain.user.dto.request.ChangeProfileImageRequest;
-
-public interface ChangeProfileImageService {
- void changeProfileImage(Long userId, ChangeProfileImageRequest request);
-}
diff --git a/src/main/java/com/kuit/findyou/domain/user/service/change_profileImage/ChangeProfileImageServiceImpl.java b/src/main/java/com/kuit/findyou/domain/user/service/change_profileImage/ChangeProfileImageServiceImpl.java
deleted file mode 100644
index f3a2d885..00000000
--- a/src/main/java/com/kuit/findyou/domain/user/service/change_profileImage/ChangeProfileImageServiceImpl.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.kuit.findyou.domain.user.service.change_profileImage;
-
-import com.kuit.findyou.domain.user.constant.DefaultProfileImage;
-import com.kuit.findyou.domain.user.dto.request.ChangeProfileImageRequest;
-import com.kuit.findyou.domain.user.model.User;
-import com.kuit.findyou.domain.user.repository.UserRepository;
-import com.kuit.findyou.global.common.exception.CustomException;
-import com.kuit.findyou.global.infrastructure.FileUploadingFailedException;
-import com.kuit.findyou.global.infrastructure.ImageUploader;
-import jakarta.persistence.EntityNotFoundException;
-import jakarta.transaction.Transactional;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Arrays;
-
-import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
-
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class ChangeProfileImageServiceImpl implements ChangeProfileImageService {
- private final UserRepository userRepository;
- private final ImageUploader imageUploader;
-
-
- @Override
- @Transactional
- public void changeProfileImage(Long userId, ChangeProfileImageRequest request) {
- try {
- User user = userRepository.getReferenceById(userId);
-
- String toSave;
- if (request.profileImageFile() != null) {
- try {
- toSave = imageUploader.upload(request.profileImageFile());
- } catch (FileUploadingFailedException e) {
- throw new CustomException(IMAGE_UPLOAD_FAILED);
- }
- } else {
- //enum 이름을 소문자로 저장
- toSave = Arrays.stream(DefaultProfileImage.values())
- .filter(v -> v.getName().equalsIgnoreCase(request.defaultProfileImageName()))
- .findFirst()
- .orElseThrow(() -> new CustomException(BAD_REQUEST))
- .getName();
- }
- String oldImageUrl = user.getProfileImageUrl();
- user.changeProfileImage(toSave);
- if (isUploadedFile(oldImageUrl)) {
- String imageKey = extractImageKeyFromUrl(oldImageUrl);
- imageUploader.delete(imageKey);
- }
- }catch (EntityNotFoundException e) {
- throw new CustomException(USER_NOT_FOUND);
- }
- }
-
- private boolean isUploadedFile(String url) {
- if (url == null) return false;
- return Arrays.stream(DefaultProfileImage.values())
- .noneMatch(defaultImage -> defaultImage.getName().equalsIgnoreCase(url));
- }
-
- private String extractImageKeyFromUrl(String url) {
- try {
- return new URI(url).getPath().substring(1);
- } catch (URISyntaxException e) {
- throw new CustomException(BAD_REQUEST);
- }
- }
-}
diff --git a/src/main/java/com/kuit/findyou/domain/user/service/facade/UserServiceFacade.java b/src/main/java/com/kuit/findyou/domain/user/service/facade/UserServiceFacade.java
index 0adb16d5..8136655c 100644
--- a/src/main/java/com/kuit/findyou/domain/user/service/facade/UserServiceFacade.java
+++ b/src/main/java/com/kuit/findyou/domain/user/service/facade/UserServiceFacade.java
@@ -1,14 +1,13 @@
package com.kuit.findyou.domain.user.service.facade;
import com.kuit.findyou.domain.report.dto.response.CardResponseDTO;
+import com.kuit.findyou.domain.user.dto.response.CheckGuestResponse;
import com.kuit.findyou.domain.user.dto.response.GetUserProfileResponse;
-import com.kuit.findyou.domain.user.dto.request.ChangeProfileImageRequest;
import com.kuit.findyou.domain.user.dto.request.CheckDuplicateNicknameRequest;
import com.kuit.findyou.domain.user.dto.request.RegisterUserRequest;
import com.kuit.findyou.domain.user.dto.response.CheckDuplicateNicknameResponse;
import com.kuit.findyou.domain.user.dto.response.RegisterUserResponse;
import com.kuit.findyou.domain.user.service.change_nickname.ChangeNicknameService;
-import com.kuit.findyou.domain.user.service.change_profileImage.ChangeProfileImageService;
import com.kuit.findyou.domain.user.service.delete.DeleteUserService;
import com.kuit.findyou.domain.user.service.interest_report.InterestReportService;
import com.kuit.findyou.domain.user.service.query.QueryUserService;
@@ -27,7 +26,6 @@ public class UserServiceFacade {
private final QueryUserService queryUserService;
private final ChangeNicknameService changeNicknameService;
private final DeleteUserService deleteUserService;
- private final ChangeProfileImageService changeProfileImageService;
private final UserReportService userReportService;
public CardResponseDTO retrieveViewedAnimals(Long lastId, Long userId) {
@@ -62,8 +60,6 @@ public void deleteUser(Long userId) {
deleteUserService.deleteUser(userId);
}
- public void changeProfileImage(Long userId, ChangeProfileImageRequest request){ changeProfileImageService.changeProfileImage(userId, request); }
-
public CardResponseDTO retrieveUserReports(Long userId, Long lastId){
return userReportService.retrieveUserReports(userId, lastId, 20);
}
@@ -71,4 +67,8 @@ public CardResponseDTO retrieveUserReports(Long userId, Long lastId){
public GetUserProfileResponse getUserProfile(Long userId) {
return queryUserService.getUserProfile(userId);
}
+
+ public CheckGuestResponse checkGuest(Long userId) {
+ return queryUserService.checkGuest(userId);
+ }
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserService.java b/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserService.java
index fccfcc7a..7d0690d6 100644
--- a/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserService.java
+++ b/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserService.java
@@ -1,5 +1,6 @@
package com.kuit.findyou.domain.user.service.query;
+import com.kuit.findyou.domain.user.dto.response.CheckGuestResponse;
import com.kuit.findyou.domain.user.dto.response.GetUserProfileResponse;
import com.kuit.findyou.domain.user.dto.request.CheckDuplicateNicknameRequest;
import com.kuit.findyou.domain.user.dto.response.CheckDuplicateNicknameResponse;
@@ -8,4 +9,6 @@ public interface QueryUserService {
CheckDuplicateNicknameResponse checkDuplicateNickname(CheckDuplicateNicknameRequest request);
GetUserProfileResponse getUserProfile(Long userId);
+
+ CheckGuestResponse checkGuest(Long userId);
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceImpl.java b/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceImpl.java
index 25252012..fd3927f9 100644
--- a/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceImpl.java
+++ b/src/main/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceImpl.java
@@ -1,5 +1,6 @@
package com.kuit.findyou.domain.user.service.query;
+import com.kuit.findyou.domain.user.dto.response.CheckGuestResponse;
import com.kuit.findyou.domain.user.dto.response.GetUserProfileResponse;
import com.kuit.findyou.domain.user.dto.request.CheckDuplicateNicknameRequest;
import com.kuit.findyou.domain.user.dto.response.CheckDuplicateNicknameResponse;
@@ -21,14 +22,19 @@ public class QueryUserServiceImpl implements QueryUserService {
@Override
public CheckDuplicateNicknameResponse checkDuplicateNickname(CheckDuplicateNicknameRequest request) {
boolean exists = userRepository.existsByName(request.nickname());
- log.info("[checkDuplicateNickname] result = {}", exists);
return new CheckDuplicateNicknameResponse(exists);
}
@Override
public GetUserProfileResponse getUserProfile(Long userId) {
- log.info("[getUserProfile] userId = {}", userId);
User user = userRepository.getReferenceById(userId);
- return new GetUserProfileResponse(user.getName(), user.getProfileImageUrl());
+ return new GetUserProfileResponse(user.getName());
+ }
+
+ @Override
+ public CheckGuestResponse checkGuest(Long userId) {
+ User user = userRepository.getReferenceById(userId);
+ boolean isGuest = user.isGuest();
+ return new CheckGuestResponse(isGuest);
}
}
diff --git a/src/main/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceImpl.java b/src/main/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceImpl.java
index 55f1f2d0..4322b002 100644
--- a/src/main/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceImpl.java
+++ b/src/main/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceImpl.java
@@ -1,19 +1,15 @@
package com.kuit.findyou.domain.user.service.register;
-import com.kuit.findyou.domain.user.constant.DefaultProfileImage;
+import com.kuit.findyou.domain.auth.service.IssueTokenService;
import com.kuit.findyou.domain.user.dto.request.RegisterUserRequest;
import com.kuit.findyou.domain.user.dto.response.RegisterUserResponse;
import com.kuit.findyou.domain.user.model.Role;
import com.kuit.findyou.domain.user.model.User;
import com.kuit.findyou.domain.user.repository.UserRepository;
import com.kuit.findyou.global.common.exception.CustomException;
-import com.kuit.findyou.global.infrastructure.FileUploadingFailedException;
-import com.kuit.findyou.global.infrastructure.ImageUploader;
-import com.kuit.findyou.global.jwt.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
-import org.springframework.web.multipart.MultipartFile;
import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
@@ -22,9 +18,7 @@
@Service
public class RegisterUserServiceImpl implements RegisterUserService {
private final UserRepository userRepository;
- private final ImageUploader imageUploader;
- private final JwtUtil jwtUtil;
-
+ private final IssueTokenService issueTokenService;
@Override
public RegisterUserResponse registerUser(RegisterUserRequest request) {
// 카카오 Id가 중복되는 사용자가 있는지 확인
@@ -33,73 +27,32 @@ public RegisterUserResponse registerUser(RegisterUserRequest request) {
throw new CustomException(ALREADY_REGISTERED_USER);
}
- // 비회원이었는지 확인한 후에 회원 정보 저장
- String profileImageUrl = getProfileImageUrl(request);
-
User user = userRepository.findByDeviceId(request.deviceId())
.map(existing -> {
log.info("[registerUser] user with deviceId {} alreay exists", request.deviceId());
- existing.upgradeToMember(request.kakaoId(), request.nickname(), profileImageUrl);
+ existing.upgradeToMember(request.kakaoId(), request.nickname());
return existing;
})
.orElseGet(()->{
log.info("[registerUser] user not found");
- return mapToUser(request, profileImageUrl);
+ return mapToUser(request);
});
User save = userRepository.save(user);
// 회원가입 완료 응답하기
- String accessToken = jwtUtil.createAccessJwt(save.getId(), save.getRole());
- return new RegisterUserResponse(save.getId(), save.getName(), accessToken);
+ String accessToken = issueTokenService.issueAccessToken(save.getId(), save.getRole());
+ String refreshToken = issueTokenService.issueRefreshToken(save.getId());
+
+ return new RegisterUserResponse(save.getId(), save.getName(), accessToken, refreshToken);
}
- private User mapToUser(RegisterUserRequest request, String profileImageUrl) {
+ private User mapToUser(RegisterUserRequest request) {
return User.builder()
.kakaoId(request.kakaoId())
.name(request.nickname())
- .profileImageUrl(profileImageUrl)
.role(Role.USER)
.deviceId(request.deviceId())
.build();
}
-
- private String getProfileImageUrl(RegisterUserRequest request) {
- // 프로필 이미지 설정 관련 검증
- MultipartFile profileImage = request.profileImageFile();
- String defaultProfileImageName = request.defaultProfileImageName();
-
- if(validateProfileImage(profileImage, defaultProfileImageName)){
- // 요청이 잘못되었음
- throw new CustomException(BAD_REQUEST);
- }
-
- // 인프라에 이미지 업로드
- if(!isEmptyProfileImageFile(profileImage)){
- try{
- return imageUploader.upload(profileImage);
- }
- catch (FileUploadingFailedException e){
- throw new CustomException(IMAGE_UPLOAD_FAILED);
- }
- }
-
- // 기본 이미지 이름 반환
- return defaultProfileImageName;
- }
-
- private boolean validateProfileImage(MultipartFile profileFile, String defaultName) {
- // 둘 다 잘못된 값이거나, 둘 다 올바른 값이면 잘못된 요청으로 간주
- boolean invalidName = isInvalidDefaultProfileImageName(defaultName);
- boolean emptyFile = isEmptyProfileImageFile(profileFile);
- return invalidName && emptyFile || !invalidName && !emptyFile;
- }
-
- private boolean isInvalidDefaultProfileImageName(String name) {
- return name == null || !DefaultProfileImage.validate(name);
- }
-
- private boolean isEmptyProfileImageFile(MultipartFile file) {
- return file == null || file.isEmpty();
- }
}
diff --git a/src/main/java/com/kuit/findyou/global/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/com/kuit/findyou/global/common/response/status/BaseExceptionResponseStatus.java
index bfeb0cd2..2775c56c 100644
--- a/src/main/java/com/kuit/findyou/global/common/response/status/BaseExceptionResponseStatus.java
+++ b/src/main/java/com/kuit/findyou/global/common/response/status/BaseExceptionResponseStatus.java
@@ -35,6 +35,9 @@ public enum BaseExceptionResponseStatus implements ResponseStatus{
// 홈
HOME_STATISTICS_UPDATE_FAILED(502, "홈화면 통계 업데이트에 실패했습니다."),
+ // 인증인가 - Auth
+ REFRESH_TOKEN_NOT_FOUND(404, "일치하는 리프레시 토큰이 없습니다"),
+
// 유저 - User
USER_NOT_FOUND(404, "존재하지 않는 유저입니다."),
DUPLICATE_INTEREST_REPORT(400, "이미 관심글로 등록된 신고글입니다."),
diff --git a/src/main/java/com/kuit/findyou/global/common/swagger/SwaggerResponseDescription.java b/src/main/java/com/kuit/findyou/global/common/swagger/SwaggerResponseDescription.java
index d382b590..1427203b 100644
--- a/src/main/java/com/kuit/findyou/global/common/swagger/SwaggerResponseDescription.java
+++ b/src/main/java/com/kuit/findyou/global/common/swagger/SwaggerResponseDescription.java
@@ -30,6 +30,14 @@ public enum SwaggerResponseDescription {
GUEST_LOGIN_FAILED
))),
+ REISSUE_TOKEN(new LinkedHashSet<>(Set.of(
+ INVALID_JWT,
+ EXPIRED_JWT,
+ JWT_NOT_FOUND,
+ REFRESH_TOKEN_NOT_FOUND,
+ USER_NOT_FOUND
+ ))),
+
KAKAO_LOGIN(new LinkedHashSet<>(Set.of())),
TEST(new LinkedHashSet<>(Set.of(
diff --git a/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java b/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java
index bf8c4939..6a566986 100644
--- a/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java
+++ b/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java
@@ -62,6 +62,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
.requestMatchers("/api/v2/auth/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v2/users").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v2/users/check/duplicate-nickname").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/v2/reports/protecting-reports/random-s3").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/v2/reports/missing-reports/random-s3").permitAll()
.anyRequest().authenticated());
// 토큰 검증 필터 추가
diff --git a/src/main/java/com/kuit/findyou/global/config/WebConfig.java b/src/main/java/com/kuit/findyou/global/config/WebConfig.java
index b6555f50..5ad40b64 100644
--- a/src/main/java/com/kuit/findyou/global/config/WebConfig.java
+++ b/src/main/java/com/kuit/findyou/global/config/WebConfig.java
@@ -3,7 +3,9 @@
import com.kuit.findyou.global.jwt.argument_resolver.LoginUserIdArgumentResolver;
import com.kuit.findyou.global.jwt.util.JwtUtil;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -17,4 +19,9 @@ public class WebConfig implements WebMvcConfigurer {
public void addArgumentResolvers(List resolvers) {
resolvers.add(new LoginUserIdArgumentResolver(jwtUtil));
}
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/kuit/findyou/global/infrastructure/ImageUploader.java b/src/main/java/com/kuit/findyou/global/infrastructure/ImageUploader.java
index afab2dd9..13cd3ba5 100644
--- a/src/main/java/com/kuit/findyou/global/infrastructure/ImageUploader.java
+++ b/src/main/java/com/kuit/findyou/global/infrastructure/ImageUploader.java
@@ -4,5 +4,6 @@
public interface ImageUploader {
String upload(MultipartFile file) throws FileUploadingFailedException;
+ String upload(byte[] content, String originalFileName, String contentType) throws FileUploadingFailedException;
void delete(String s3ObjectKey);
}
diff --git a/src/main/java/com/kuit/findyou/global/infrastructure/S3ImageUploader.java b/src/main/java/com/kuit/findyou/global/infrastructure/S3ImageUploader.java
index d3ed95d7..5d0d6f92 100644
--- a/src/main/java/com/kuit/findyou/global/infrastructure/S3ImageUploader.java
+++ b/src/main/java/com/kuit/findyou/global/infrastructure/S3ImageUploader.java
@@ -58,6 +58,36 @@ public String upload(MultipartFile file) throws FileUploadingFailedException{
}
}
+ @Override
+ public String upload(byte[] content, String originalFileName, String contentType) throws FileUploadingFailedException {
+
+ if (content == null || content.length == 0) {
+ throw new IllegalArgumentException("업로드할 파일이 비어 있을 수 없습니다");
+ }
+
+ String safeOriginalName = (originalFileName == null || originalFileName.isBlank())
+ ? "image"
+ : originalFileName;
+
+ String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
+ String fileName = datePath + "/" + UUID.randomUUID() + "_" + safeOriginalName;
+
+ try {
+ PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+ .bucket(bucket)
+ .key(fileName)
+ .contentType(contentType != null ? contentType : "image/jpeg")
+ .build();
+
+ s3Client.putObject(putObjectRequest, RequestBody.fromBytes(content));
+
+ return getFileUrl(fileName);
+
+ } catch (S3Exception e) {
+ throw new FileUploadingFailedException("S3 업로드 실패: " + e.awsErrorDetails().errorMessage());
+ }
+ }
+
@Override
public void delete(String s3ObjectKey) {
if (s3ObjectKey == null || s3ObjectKey.isBlank()) {
diff --git a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java
index 84bedca3..bce4f5b1 100644
--- a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java
+++ b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java
@@ -16,6 +16,7 @@
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
+import java.util.UUID;
import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
@@ -24,9 +25,12 @@
public class JwtUtil {
private final SecretKey secretKey;
- @Value("${findyou.jwt.access.expire-ms}")
+ @Value("${findyou.jwt.expiration-ms.access-token}")
private long accessTokenExpireMs;
+ @Value("${findyou.jwt.expiration-ms.refresh-token}")
+ private long refreshTokenExpireMs;
+
public JwtUtil(@Value("${findyou.jwt.secret-key}") String secret) {
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
@@ -57,6 +61,17 @@ public String createAccessJwt(Long userId, Role role) {
.compact();
}
+ public String createRefreshJwt(Long userId) {
+ return Jwts.builder()
+ .id(UUID.randomUUID().toString())
+ .claim(JwtClaimKey.USER_ID.getKey(), userId)
+ .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.REFRESH_TOKEN)
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + refreshTokenExpireMs))
+ .signWith(secretKey)
+ .compact();
+ }
+
public void validateJwt(String token){
log.info("validateJwt");
try{
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index d7aa53ce..1c1a7505 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -6,7 +6,7 @@ spring:
local : local-db, dev-port, common
dev: dev-db, dev-port, common
prod: prod-db, prod-port, common
- active: prod
+ active: local
web:
resources:
add-mappings: false
@@ -153,8 +153,9 @@ logging:
findyou:
jwt:
- access:
- expire-ms: ${JWT_ACCESS_EXPIRE_MS}
+ expiration-ms:
+ access-token : ${JWT_ACCESS_EXPIRE_MS}
+ refresh-token : ${JWT_REFRESH_EXPIRE_MS}
secret-key : ${JWT_SECRET_KEY}
home-stats:
parsing-timeout-sec: ${HOME_STATS_PARSING_TIMEOUT_SEC}
diff --git a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java
index 4a901fe9..649607c1 100644
--- a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java
+++ b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java
@@ -1,36 +1,49 @@
package com.kuit.findyou.domain.auth.controller;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse;
import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
+import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository;
import com.kuit.findyou.domain.user.model.Role;
import com.kuit.findyou.domain.user.model.User;
import com.kuit.findyou.domain.user.repository.UserRepository;
import com.kuit.findyou.global.common.response.BaseErrorResponse;
import com.kuit.findyou.global.common.response.BaseResponse;
import com.kuit.findyou.global.common.util.DatabaseCleaner;
+import com.kuit.findyou.global.config.RedisTestContainersConfig;
import com.kuit.findyou.global.config.TestDatabaseConfig;
+import com.kuit.findyou.global.jwt.util.JwtClaimKey;
import com.kuit.findyou.global.jwt.util.JwtTokenType;
import com.kuit.findyou.global.jwt.util.JwtUtil;
+import io.jsonwebtoken.Jwts;
import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
-import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.GUEST_LOGIN_FAILED;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.UUID;
+
+import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("test")
-@Import(TestDatabaseConfig.class)
+@Import({RedisTestContainersConfig.class, TestDatabaseConfig.class})
class AuthControllerTest {
@LocalServerPort
int port;
@@ -38,12 +51,18 @@ class AuthControllerTest {
@Autowired
private UserRepository userRepository;
+ @Autowired
+ private RedisRefreshTokenRepository redisRefreshTokenRepository;
+
@Autowired
private JwtUtil jwtUtil;
@Autowired
private DatabaseCleaner databaseCleaner;
+ @Value("${findyou.jwt.secret-key}")
+ String secret;
+
@BeforeEach
void setUp() {
databaseCleaner.execute();
@@ -53,7 +72,7 @@ void setUp() {
@DisplayName("기존 회원이 로그인하면 유저 정보를 반환한다")
@Test
- void should_ReturnUserInfo_When_ExistingUserLogsIn(){
+ void kakaoLogin_shouldReturnUserInfo_WhenAlreadyRegisteredUser(){
// given
final String NAME = "유저";
final Role ROLE = Role.USER;
@@ -98,7 +117,7 @@ private User createUser(String name, Role role, Long kakaoId, String deviceId){
@DisplayName("게스트가 로그인하면 성공한다.")
@Test
- void should_Succeed_When_GuestLogsIn(){
+ void guestLogin_shouldSucceed_WhenAlreadyRegisteredGuest(){
// given
final String deviceId = "asdf-1234-asdf";
@@ -122,9 +141,10 @@ void should_Succeed_When_GuestLogsIn(){
assertThat(jwtUtil.getUserId(response.accessToken())).isEqualTo(user.getId());
assertThat(jwtUtil.getRole(response.accessToken())).isEqualTo(user.getRole());
}
+
@DisplayName("게스트가 아닌 유저가 로그인하면 실패한다.")
@Test
- void should_Fail_When_NonGuestUserLogsIn(){
+ void guestLogin_shouldFail_WhenUnknownUser(){
// given
final String deviceId = "asdf-1234-asdf";
@@ -138,13 +158,101 @@ void should_Fail_When_NonGuestUserLogsIn(){
.when()
.post("/api/v2/auth/login/guest")
.then()
- .statusCode(200)
.extract()
- .as(new TypeRef() {});
+ .as(new TypeRef() { });
// then
assertThat(response.getCode()).isEqualTo(GUEST_LOGIN_FAILED.getCode());
assertThat(response.getMessage()).isEqualTo(GUEST_LOGIN_FAILED.getMessage());
assertThat(response.getSuccess()).isFalse();
}
+
+ @DisplayName("올바른 리프레시 토큰으로 요청하면 200 코드를 응답한다")
+ @Test
+ void reissueToken_shouldReturnSuccess_WhenValidRefreshToken(){
+ // given
+ User user = createUser("회원", Role.USER, null, "asdf-1234-asdf");
+ String refreshToken = jwtUtil.createRefreshJwt(user.getId());
+ redisRefreshTokenRepository.save(user.getId(), refreshToken);
+
+ // when
+ ReissueTokenResponse response = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(new ReissueTokenRequest(refreshToken))
+ .when()
+ .post("/api/v2/auth/reissue/token")
+ .then()
+ .extract()
+ .jsonPath()
+ .getObject("data", ReissueTokenResponse.class);
+
+ // then
+ assertThat(response.accessToken()).isNotBlank();
+ assertThat(response.refreshToken()).isNotBlank();
+ }
+
+ @DisplayName("리프레시 토큰이 만료되었으면 401 코드를 응답한다")
+ @Test
+ void reissueToken_shouldReturnUnauthorized_WhenRefreshTokenIsNotMatched(){
+ // given
+ User user = createUser("회원", Role.USER, null, "asdf-1234-asdf");
+ String expiredRefreshToken = createExpiredRefreshJwt(user.getId());
+
+ // when
+ BaseErrorResponse response = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(new ReissueTokenRequest(expiredRefreshToken))
+ .when()
+ .post("/api/v2/auth/reissue/token")
+ .then()
+ .log().all()
+ .extract()
+ .as(new TypeRef() {
+ });
+
+ // then
+ assertThat(response.getCode()).isEqualTo(EXPIRED_JWT.getCode());
+ assertThat(response.getMessage()).isEqualTo(EXPIRED_JWT.getMessage());
+ }
+
+ private String createExpiredRefreshJwt(Long userId) {
+ SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
+ return Jwts.builder()
+ .id(UUID.randomUUID().toString())
+ .claim(JwtClaimKey.USER_ID.getKey(), userId)
+ .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.REFRESH_TOKEN)
+ .issuedAt(new Date(System.currentTimeMillis() - 100))
+ .expiration(new Date(System.currentTimeMillis()))
+ .signWith(secretKey)
+ .compact();
+ }
+
+ @DisplayName("리프레시 토큰이 일치하지 않으면 404 코드를 응답한다")
+ @Test
+ void reissueToken_shouldReturnNotFound_WhenRefreshTokenIsNotMatched(){
+ // given
+ User user = createUser("회원", Role.USER, null, "asdf-1234-asdf");
+ String unknownRefreshToken = jwtUtil.createRefreshJwt(user.getId());
+ String savedRefreshToken = jwtUtil.createRefreshJwt(user.getId());
+ redisRefreshTokenRepository.save(user.getId(), savedRefreshToken);
+
+ // when
+ BaseErrorResponse response = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(new ReissueTokenRequest(unknownRefreshToken))
+ .when()
+ .post("/api/v2/auth/reissue/token")
+ .then()
+ .log().all()
+ .extract()
+ .as(new TypeRef() {
+ });
+
+ // then
+ assertThat(response.getCode()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getCode());
+ assertThat(response.getMessage()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getMessage());
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/auth/service/AuthServiceTest.java b/src/test/java/com/kuit/findyou/domain/auth/service/LoginServiceTest.java
similarity index 74%
rename from src/test/java/com/kuit/findyou/domain/auth/service/AuthServiceTest.java
rename to src/test/java/com/kuit/findyou/domain/auth/service/LoginServiceTest.java
index e983c0d8..e971bb58 100644
--- a/src/test/java/com/kuit/findyou/domain/auth/service/AuthServiceTest.java
+++ b/src/test/java/com/kuit/findyou/domain/auth/service/LoginServiceTest.java
@@ -4,6 +4,7 @@
import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse;
import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
+import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository;
import com.kuit.findyou.domain.user.model.Role;
import com.kuit.findyou.domain.user.model.User;
import com.kuit.findyou.domain.user.repository.UserRepository;
@@ -26,14 +27,15 @@
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
-class AuthServiceTest {
+class LoginServiceTest {
@InjectMocks
- private AuthServiceImpl authService;
+ private LoginServiceImpl authService;
@Mock
private UserRepository userRepository;
@Mock
- private JwtUtil jwtUtil;
+ private IssueTokenService issueTokenService;
+ @DisplayName("카카오 id와 일치하는 사용자가 없다면 isFirstLogin을 true로 반환하여 회원가입을 유도한다")
@Test
void should_ReturnfirstLoginWithTrue_When_UserWithKakaoIdNotFound(){
// given
@@ -48,16 +50,19 @@ void should_ReturnfirstLoginWithTrue_When_UserWithKakaoIdNotFound(){
assertThat(response.userInfo()).isNull();
}
+ @DisplayName("카카오 id와 일치하는 사용자가 있다면 정보를 리턴한다")
@Test
void should_ReturnUserInfo_When_UserWithKakaoIdExists(){
// given
final Long KAKAO_ID = 1234L;
- final String ACCESS_TOKEN = "accessToken";
- final String NAME = "유저";
+ String ACCESS_TOKEN = "accessToken";
+ String REFRESH_TOKEN = "accessToken";
+ String NAME = "유저";
User user = mockUser(NAME, Role.USER, KAKAO_ID);
when(userRepository.findByKakaoId(KAKAO_ID)).thenReturn(Optional.of(user));
- when(jwtUtil.createAccessJwt(user.getId(), user.getRole())).thenReturn(ACCESS_TOKEN);
+ when(issueTokenService.issueAccessToken(user.getId(), user.getRole())).thenReturn(ACCESS_TOKEN);
+ when(issueTokenService.issueRefreshToken(user.getId())).thenReturn(REFRESH_TOKEN);
// when
KakaoLoginResponse response = authService.kakaoLogin(new KakaoLoginRequest(KAKAO_ID));
@@ -67,6 +72,7 @@ void should_ReturnUserInfo_When_UserWithKakaoIdExists(){
assertThat(response.userInfo()).isNotNull();
assertThat(response.userInfo().userId()).isEqualTo(user.getId());
assertThat(response.userInfo().accessToken()).isEqualTo(ACCESS_TOKEN);
+ assertThat(response.userInfo().refreshToken()).isEqualTo(REFRESH_TOKEN);
assertThat(response.userInfo().nickname()).isEqualTo(NAME);
}
@@ -85,49 +91,56 @@ private User mockUser(String name, Role role, Long kakaoId){
@Test()
void should_DoesNotSaveNewGuest_When_UserWithDeviceIdExists(){
// given
- final String deviceId = "asdf-1234-asdf";
- final String accessToken = "accessToken";
+ String deviceId = "asdf-1234-asdf";
+ String accessToken = "accessToken";
+ String refreshToken = "refreshToken";
User user = mockUser("게스트", Role.GUEST, null);
when(userRepository.findByDeviceId(eq(deviceId))).thenReturn(Optional.of(user));
- when(jwtUtil.createAccessJwt(user.getId(), user.getRole())).thenReturn(accessToken);
+ when(issueTokenService.issueAccessToken(anyLong(), any(Role.class))).thenReturn(accessToken);
+ when(issueTokenService.issueRefreshToken(anyLong())).thenReturn(refreshToken);
// when
GuestLoginResponse response = authService.guestLogin(new GuestLoginRequest(deviceId));
// then
verify(userRepository, never()).save(any());
+
assertThat(response.userId()).isEqualTo(user.getId());
assertThat(response.accessToken()).isEqualTo(accessToken);
+ assertThat(response.refreshToken()).isEqualTo(refreshToken);
}
@DisplayName("디바이스 id가 일치하는 유저가 없으면 새로 게스트를 저장한다")
@Test()
void should_SaveNewGuest_When_UserWithDeviceIdDoesNotExists(){
// given
- final String deviceId = "asdf-1234-asdf";
- final String accessToken = "accessToken";
+ String deviceId = "asdf-1234-asdf";
+ String accessToken = "accessToken";
+ String refreshToken = "refreshToken";
User user = mockUser("게스트", Role.GUEST, null);
when(userRepository.findByDeviceId(eq(deviceId))).thenReturn(Optional.empty());
when(userRepository.save(any())).thenReturn(user);
- when(jwtUtil.createAccessJwt(user.getId(), user.getRole())).thenReturn(accessToken);
+ when(issueTokenService.issueAccessToken(anyLong(), any(Role.class))).thenReturn(accessToken);
+ when(issueTokenService.issueRefreshToken(anyLong())).thenReturn(refreshToken);
// when
GuestLoginResponse response = authService.guestLogin(new GuestLoginRequest(deviceId));
// then
verify(userRepository).save(any(User.class));
+
assertThat(response.userId()).isEqualTo(user.getId());
assertThat(response.accessToken()).isEqualTo(accessToken);
+ assertThat(response.refreshToken()).isEqualTo(refreshToken);
}
@DisplayName("게스트가 아니면 예외가 발생한다.")
@Test()
void should_ThrowException_When_NonGuestUserLogsIn(){
// given
- final String deviceId = "asdf-1234-asdf";
- final String accessToken = "accessToken";
+ String deviceId = "asdf-1234-asdf";
User user = mockUser("게스트", Role.USER, null);
when(userRepository.findByDeviceId(eq(deviceId))).thenReturn(Optional.of(user));
diff --git a/src/test/java/com/kuit/findyou/domain/auth/service/ReissueTokenServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/auth/service/ReissueTokenServiceImplTest.java
new file mode 100644
index 00000000..0922de4a
--- /dev/null
+++ b/src/test/java/com/kuit/findyou/domain/auth/service/ReissueTokenServiceImplTest.java
@@ -0,0 +1,124 @@
+package com.kuit.findyou.domain.auth.service;
+
+import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
+import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
+import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository;
+import com.kuit.findyou.domain.user.model.Role;
+import com.kuit.findyou.domain.user.model.User;
+import com.kuit.findyou.domain.user.repository.UserRepository;
+import com.kuit.findyou.global.common.exception.CustomException;
+import com.kuit.findyou.global.jwt.exception.JwtExpiredException;
+import com.kuit.findyou.global.jwt.util.JwtUtil;
+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 java.util.Optional;
+
+import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.EXPIRED_JWT;
+import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.REFRESH_TOKEN_NOT_FOUND;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class ReissueTokenServiceImplTest {
+ @InjectMocks
+ ReissueTokenServiceImpl reissueTokenService;
+
+ @Mock
+ JwtUtil jwtUtil;
+
+ @Mock
+ RedisRefreshTokenRepository redisRefreshTokenRepository;
+
+ @Mock
+ UserRepository userRepository;
+
+ @DisplayName("올바른 리프레시 토큰이면 토큰을 반환한다")
+ @Test
+ void reissueToken_shouldReturnToken_whenValidRefreshToken(){
+ // given
+ Long userId = 1L;
+ String existingRefreshToken = "valid refresh";
+ String newAccessToken = "new access";
+ String newRefreshToken = "new refresh";
+ User user = User.builder().id(userId).role(Role.USER).build();
+
+ ReissueTokenRequest request = new ReissueTokenRequest(existingRefreshToken);
+
+ when(jwtUtil.getUserId(any(String.class))).thenReturn(userId);
+ when(redisRefreshTokenRepository.findByUserId(any(Long.class))).thenReturn(Optional.of(existingRefreshToken));
+ when(userRepository.findById(any(Long.class))).thenReturn(Optional.of(user));
+ when(jwtUtil.createAccessJwt(any(Long.class), any(Role.class))).thenReturn(newAccessToken);
+ when(jwtUtil.createRefreshJwt(any(Long.class))).thenReturn(newRefreshToken);
+
+ // when
+ ReissueTokenResponse response = reissueTokenService.reissueToken(request);
+
+ // then
+ verify(redisRefreshTokenRepository, times(1)).save(anyLong(), anyString());
+
+ assertThat(response.accessToken()).isEqualTo(newAccessToken);
+ assertThat(response.refreshToken()).isEqualTo(newRefreshToken);
+ }
+
+ @DisplayName("만료된 리프레시 토큰이면 예외를 발생시킨다")
+ @Test
+ void reissueToken_shouldThrowException_whenInvalidRefreshToken(){
+ // given
+ ReissueTokenRequest request = new ReissueTokenRequest("expired refresh");
+
+ doThrow(new JwtExpiredException(EXPIRED_JWT)).when(jwtUtil).validateJwt(anyString());
+
+ // when & then
+ assertThatThrownBy(() -> reissueTokenService.reissueToken(request))
+ .isInstanceOf(CustomException.class)
+ .hasMessage(EXPIRED_JWT.getMessage());
+
+ verify(redisRefreshTokenRepository, never()).save(anyLong(), anyString());
+ }
+
+ @DisplayName("리프레시 토큰이 저장된 리프레시 토큰과 일치하지 않으면 예외를 발생시킨다")
+ @Test
+ void reissueToken_shouldThrowException_whenRefreshTokenIsNotMatched(){
+ // given
+ Long userId = 1L;
+
+ ReissueTokenRequest request = new ReissueTokenRequest("old refresh");
+
+ when(jwtUtil.getUserId(any(String.class))).thenReturn(userId);
+ when(redisRefreshTokenRepository.findByUserId(any(Long.class))).thenReturn(Optional.of("valid refresh"));
+
+ // when & then
+ assertThatThrownBy(() -> reissueTokenService.reissueToken(request))
+ .isInstanceOf(CustomException.class)
+ .hasMessage(REFRESH_TOKEN_NOT_FOUND.getMessage());
+
+ verify(redisRefreshTokenRepository, never()).save(anyLong(), anyString());
+
+ }
+
+ @DisplayName("저장된 리프레시 토큰이 없으면 예외를 발생시킨다")
+ @Test
+ void reissueToken_shouldThrowException_whenNoSavedRefreshToken(){
+ // given
+ Long userId = 1L;
+
+ ReissueTokenRequest request = new ReissueTokenRequest("refresh");
+
+ when(jwtUtil.getUserId(any(String.class))).thenReturn(userId);
+ when(redisRefreshTokenRepository.findByUserId(any(Long.class))).thenReturn(Optional.empty());
+
+ // when & then
+ assertThatThrownBy(() -> reissueTokenService.reissueToken(request))
+ .isInstanceOf(CustomException.class)
+ .hasMessage(REFRESH_TOKEN_NOT_FOUND.getMessage());
+
+ verify(redisRefreshTokenRepository, never()).save(anyLong(), anyString());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/report/controller/ReportControllerTest.java b/src/test/java/com/kuit/findyou/domain/report/controller/ReportControllerTest.java
index b83ad70d..93a0b512 100644
--- a/src/test/java/com/kuit/findyou/domain/report/controller/ReportControllerTest.java
+++ b/src/test/java/com/kuit/findyou/domain/report/controller/ReportControllerTest.java
@@ -3,6 +3,10 @@
import com.kuit.findyou.domain.report.dto.request.CreateMissingReportRequest;
import com.kuit.findyou.domain.report.dto.request.CreateWitnessReportRequest;
import com.kuit.findyou.domain.report.dto.request.ReportViewType;
+import com.kuit.findyou.domain.report.model.MissingReport;
+import com.kuit.findyou.domain.report.model.ProtectingReport;
+import com.kuit.findyou.domain.report.repository.MissingReportRepository;
+import com.kuit.findyou.domain.report.repository.ProtectingReportRepository;
import com.kuit.findyou.domain.user.model.User;
import com.kuit.findyou.global.common.util.DatabaseCleaner;
import com.kuit.findyou.global.common.util.TestInitializer;
@@ -21,6 +25,8 @@
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.RestTemplate;
import java.time.LocalDate;
import java.util.List;
@@ -28,6 +34,9 @@
import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.FORBIDDEN;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -38,6 +47,16 @@ class ReportControllerTest {
@MockitoBean
private ImageUploader imageUploader;
+ @MockitoBean
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ProtectingReportRepository protectingReportRepository;
+
+ @Autowired
+ private MissingReportRepository missingReportRepository;
+
+
@LocalServerPort
int port;
@@ -180,6 +199,7 @@ void retrieveAllReports() {
.body("data.cards[0].title", equalTo("진돗개"))
.body("data.cards[0].tag", equalTo("목격신고"))
.body("data.cards[0].date", equalTo("2024-08-10"))
+ .body("data.cards[0].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[0].location", equalTo("부산시 해운대구"))
.body("data.cards[0].interest", equalTo(true))
.body("data.cards[1].reportId", equalTo(2))
@@ -187,6 +207,7 @@ void retrieveAllReports() {
.body("data.cards[1].title", equalTo("포메라니안"))
.body("data.cards[1].tag", equalTo("실종신고"))
.body("data.cards[1].date", equalTo("2024-10-05"))
+ .body("data.cards[1].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[1].location", equalTo("서울시 강남구"))
.body("data.cards[1].interest", equalTo(true))
.body("data.cards[2].reportId", equalTo(1))
@@ -194,6 +215,7 @@ void retrieveAllReports() {
.body("data.cards[2].title", equalTo("믹스견"))
.body("data.cards[2].tag", equalTo("보호중"))
.body("data.cards[2].date", equalTo(LocalDate.now().toString()))
+ .body("data.cards[2].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[2].location", equalTo("서울"))
.body("data.cards[2].interest", equalTo(true))
.body("data.lastId", equalTo(1))
@@ -223,6 +245,7 @@ void retrieveProtectingReports() {
.body("data.cards[0].title", equalTo("믹스견"))
.body("data.cards[0].tag", equalTo("보호중"))
.body("data.cards[0].date", equalTo(LocalDate.now().toString()))
+ .body("data.cards[0].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[0].location", equalTo("서울"))
.body("data.cards[0].interest", equalTo(true))
.body("data.lastId", equalTo(1))
@@ -252,6 +275,7 @@ void retrieveReportingReports() {
.body("data.cards[0].title", equalTo("진돗개"))
.body("data.cards[0].tag", equalTo("목격신고"))
.body("data.cards[0].date", equalTo("2024-08-10"))
+ .body("data.cards[0].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[0].location", equalTo("부산시 해운대구"))
.body("data.cards[0].interest", equalTo(true))
.body("data.cards[1].reportId", equalTo(2))
@@ -259,6 +283,7 @@ void retrieveReportingReports() {
.body("data.cards[1].title", equalTo("포메라니안"))
.body("data.cards[1].tag", equalTo("실종신고"))
.body("data.cards[1].date", equalTo("2024-10-05"))
+ .body("data.cards[1].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[1].location", equalTo("서울시 강남구"))
.body("data.cards[1].interest", equalTo(true))
.body("data.lastId", equalTo(2))
@@ -537,4 +562,105 @@ private CreateWitnessReportRequest createBasicWitnessReportRequest() {
"건국대학교"
);
}
+ @Test
+ @DisplayName("보호글 랜덤 조회 -> S3 URL 포함 응답")
+ void getRandomProtectingReportsWithS3_success() {
+ // given
+ User user = testInitializer.createTestUser();
+ ProtectingReport report = testInitializer.createTestProtectingReportWithImage(user);
+ ReflectionTestUtils.setField(report, "date", LocalDate.now().minusDays(1));
+ protectingReportRepository.saveAndFlush(report);
+
+ String originalImageUrl = "https://img.com/1.png";
+
+ when(restTemplate.getForObject(eq(originalImageUrl),eq(byte[].class)))
+ .thenReturn(new byte[]{1, 2, 3});
+
+ when(imageUploader.upload(any(byte[].class), anyString(), eq("image/jpeg")))
+ .thenReturn("https://cdn.findyou.store/random1.jpg");
+
+ // when & then
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .param("count", 1)
+ .when()
+ .get("/api/v2/reports/protecting-reports/random-s3")
+ .then()
+ .log().all()
+ .statusCode(200)
+ .body("success", equalTo(true))
+ .body("code", equalTo(200))
+ .body("data.size()", equalTo(1))
+ .body( "data[0].imageUrls[0]", equalTo("https://cdn.findyou.store/random1.jpg"))
+ .body("data[0].breed", equalTo("믹스견"))
+ .body("data[0].tag", equalTo("보호중"))
+ .body("data[0].careName", equalTo("광진보호소"));
+ }
+
+ @Test
+ @DisplayName("보호글이 없을 경우 -> 204 No Content 응답 (Body 없음)")
+ void getRandomProtectingReportsWithS3_noContent() {
+ // given
+ //DB에 아무것도 저장하지 않음 (빈 상태)
+
+ // when & then
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .param("count", 1)
+ .when()
+ .get("/api/v2/reports/protecting-reports/random-s3")
+ .then()
+ .log().all()
+ .statusCode(204);
+ }
+ @Test
+ @DisplayName("실종글 랜덤 조회 -> S3 URL 포함 응답")
+ void getRandomMissingReportsWithS3_success() {
+ // given
+ User user = testInitializer.createTestUser();
+ MissingReport report =
+ testInitializer.createTestMissingReportWithImage(user);
+
+ ReflectionTestUtils.setField(report, "date", LocalDate.now().minusDays(1));
+
+ missingReportRepository.saveAndFlush(report);
+
+ // when & then
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .param("count", 1)
+ .when()
+ .get("/api/v2/reports/missing-reports/random-s3")
+ .then()
+ .log().all()
+ .statusCode(200)
+ .body("success", equalTo(true))
+ .body("code", equalTo(200))
+ .body("data.size()", equalTo(1))
+ .body("data[0].imageUrls[0]", equalTo("https://img.com/missing.png"))
+ .body("data[0].breed", equalTo("포메라니안"))
+ .body("data[0].tag", equalTo("실종신고"));
+ }
+
+ @Test
+ @DisplayName("실종글이 없을 경우 -> 204 No Content 응답 (Body 없음)")
+ void getRandomMissingReportsWithS3_noContent() {
+ // given
+ // DB에 아무것도 저장하지 않음 (빈 상태)
+
+ // when & then
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .param("count", 1)
+ .when()
+ .get("/api/v2/reports/missing-reports/random-s3")
+ .then()
+ .log().all()
+ .statusCode(204);
+ }
+
}
diff --git a/src/test/java/com/kuit/findyou/domain/report/factory/CardFactoryTest.java b/src/test/java/com/kuit/findyou/domain/report/factory/CardFactoryTest.java
index 2efd1eed..3ebc0549 100644
--- a/src/test/java/com/kuit/findyou/domain/report/factory/CardFactoryTest.java
+++ b/src/test/java/com/kuit/findyou/domain/report/factory/CardFactoryTest.java
@@ -14,6 +14,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
@@ -40,8 +41,8 @@ void createCardResponseTest() {
boolean isLast = true;
- ReportProjection projection1 = mockProjection(101L, "http://image1.jpg", "제목1", "MISSING", LocalDate.of(2024, 7, 20), "서울시 강남구");
- ReportProjection projection2 = mockProjection(102L, "http://image2.jpg", "제목2", "PROTECTING", LocalDate.of(2024, 7, 21), "서울시 마포구");
+ ReportProjection projection1 = mockProjection(101L, "http://image1.jpg", "제목1", "MISSING", LocalDate.of(2024, 7, 20),LocalDateTime.now(), "서울시 강남구");
+ ReportProjection projection2 = mockProjection(102L, "http://image2.jpg", "제목2", "PROTECTING", LocalDate.of(2024, 7, 21),LocalDateTime.now(), "서울시 마포구");
List projections = List.of(projection1, projection2);
@@ -68,13 +69,14 @@ void createCardResponseTest() {
- private ReportProjection mockProjection(Long id, String imageUrl, String title, String tag, LocalDate date, String address) {
+ private ReportProjection mockProjection(Long id, String imageUrl, String title, String tag, LocalDate date, LocalDateTime createdAt, String address) {
ReportProjection mock = mock(ReportProjection.class);
when(mock.getReportId()).thenReturn(id);
when(mock.getThumbnailImageUrl()).thenReturn(imageUrl);
when(mock.getTitle()).thenReturn(title);
when(mock.getTag()).thenReturn(tag);
when(mock.getDate()).thenReturn(date);
+ when(mock.getCreatedAt()).thenReturn(createdAt);
when(mock.getAddress()).thenReturn(address);
return mock;
}
diff --git a/src/test/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3ServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3ServiceImplTest.java
new file mode 100644
index 00000000..9a53897d
--- /dev/null
+++ b/src/test/java/com/kuit/findyou/domain/report/service/retrieve/MissingReportRetrieveWithS3ServiceImplTest.java
@@ -0,0 +1,255 @@
+package com.kuit.findyou.domain.report.service.retrieve;
+
+import com.kuit.findyou.domain.image.model.ReportImage;
+import com.kuit.findyou.domain.report.dto.response.MissingReportDetailResponseDTO;
+import com.kuit.findyou.domain.report.model.MissingReport;
+import com.kuit.findyou.domain.report.repository.MissingReportRepository;
+import com.kuit.findyou.domain.report.service.detail.strategy.MissingReportDetailStrategy;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+@Transactional
+@ActiveProfiles("test")
+@MockitoSettings(strictness = Strictness.LENIENT)
+class MissingReportRetrieveWithS3ServiceImplTest {
+
+ @Mock
+ private MissingReportRepository missingReportRepository;
+
+
+ @Mock
+ private MissingReportDetailStrategy missingReportDetailStrategy;
+
+
+ @InjectMocks
+ private MissingReportRetrieveWithS3ServiceImpl missingReportRetrieveWithS3Service;
+
+ @Test
+ @DisplayName("어제 날짜 실종글이 없으면 빈 리스트 반환")
+ void getRandomMissingReportsWithS3_whenNoReports_thenReturnEmpty() {
+ // given
+ when(missingReportRepository.findByDate(any(LocalDate.class)))
+ .thenReturn(Collections.emptyList());
+
+ // when
+ List result =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(3);
+
+ // then
+ assertThat(result).isEmpty();
+
+ verify(missingReportRepository, times(1)).findByDate(any(LocalDate.class));
+ }
+
+ @Test
+ @DisplayName("요청 개수보다 실종글이 적으면 전체 개수만큼만 반환")
+ void getRandomMissingReportsWithS3_whenCountGreaterThanSize_thenReturnAllReports() {
+ // given
+ MissingReport report1 = mock(MissingReport.class);
+ MissingReport report2 = mock(MissingReport.class);
+
+ when(report1.getReportImages()).thenReturn(Collections.emptyList());
+ when(report2.getReportImages()).thenReturn(Collections.emptyList());
+
+ when(missingReportRepository.findByDate(any(LocalDate.class)))
+ .thenReturn(new ArrayList<>(List.of(report1, report2)));
+
+ MissingReportDetailResponseDTO dto1 = mock(MissingReportDetailResponseDTO.class);
+ MissingReportDetailResponseDTO dto2 = mock(MissingReportDetailResponseDTO.class);
+
+ when(missingReportDetailStrategy.toDetailDto(eq(report1), anyList(), eq(false))).thenReturn(dto1);
+ when(missingReportDetailStrategy.toDetailDto(eq(report2), anyList(), eq(false))).thenReturn(dto2);
+
+ // when
+ List result =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(10);
+
+ // then
+ assertThat(result).hasSize(2).containsExactlyInAnyOrder(dto1, dto2);
+
+ verify(missingReportRepository, times(1)).findByDate(any(LocalDate.class));
+ verify(missingReportDetailStrategy, times(2))
+ .toDetailDto(any(MissingReport.class), anyList(), eq(false));
+
+ }
+
+ @Test
+ @DisplayName("요청 개수가 전체 개수보다 적으면 요청 개수만큼만 반환")
+ void getRandomMissingReportsWithS3_whenCountLessThanSize_thenReturnCountReports() {
+ // given
+ MissingReport r1 = mock(MissingReport.class);
+ MissingReport r2 = mock(MissingReport.class);
+ MissingReport r3 = mock(MissingReport.class);
+
+ when(r1.getReportImages()).thenReturn(Collections.emptyList());
+ when(r2.getReportImages()).thenReturn(Collections.emptyList());
+ when(r3.getReportImages()).thenReturn(Collections.emptyList());
+
+ when(missingReportRepository.findByDate(any(LocalDate.class)))
+ .thenReturn(new ArrayList<>(List.of(r1, r2, r3)));
+
+ when(missingReportDetailStrategy.toDetailDto(any(), anyList(), eq(false)))
+ .thenReturn(mock(MissingReportDetailResponseDTO.class));
+
+ // when
+ List result =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(2);
+
+ // then
+ assertThat(result).hasSize(2);
+
+ verify(missingReportRepository, times(1)).findByDate(any(LocalDate.class));
+ verify(missingReportDetailStrategy, times(2))
+ .toDetailDto(any(MissingReport.class), anyList(), eq(false));
+ }
+
+ @Test
+ @DisplayName("Repository 조회 날짜가 '어제'로 들어간다")
+ void getRandomMissingReportsWithS3_verifyRepositoryDateIsYesterday() {
+ // given
+ when(missingReportRepository.findByDate(any(LocalDate.class)))
+ .thenReturn(Collections.emptyList());
+
+ ArgumentCaptor dateCaptor = ArgumentCaptor.forClass(LocalDate.class);
+
+ // when
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(1);
+
+ // then
+ verify(missingReportRepository).findByDate(dateCaptor.capture());
+ assertThat(dateCaptor.getValue()).isEqualTo(LocalDate.now().minusDays(1));
+ }
+ @Test
+ @DisplayName("DB에 저장된 imageUrl을 그대로 DTO에 전달한다")
+ void getRandomMissingReportsWithS3_returnsStoredImageUrls() {
+ MissingReport report = mock(MissingReport.class);
+
+ ReportImage img1 = mock(ReportImage.class);
+ when(img1.getImageUrl()).thenReturn("https://cdn.findyou.store/a.jpg");
+
+ ReportImage img2 = mock(ReportImage.class);
+ when(img2.getImageUrl()).thenReturn("https://cdn.findyou.store/b.jpg");
+
+ when(report.getReportImages()).thenReturn(List.of(img1, img2));
+ when(missingReportRepository.findByDate(any()))
+ .thenReturn(List.of(report));
+
+ MissingReportDetailResponseDTO dto = mock(MissingReportDetailResponseDTO.class);
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+ when(missingReportDetailStrategy.toDetailDto(eq(report), captor.capture(), eq(false)))
+ .thenReturn(dto);
+
+ List result =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(1);
+
+ assertThat(result).hasSize(1);
+ assertThat(captor.getValue())
+ .containsExactly(
+ "https://cdn.findyou.store/a.jpg",
+ "https://cdn.findyou.store/b.jpg"
+ );
+ }
+ @Test
+ @DisplayName("count가 0 이하이면 최소 1개를 반환한다")
+ void getRandomMissingReportsWithS3_whenCountZeroOrNegative_thenReturnAtLeastOne() {
+ // given
+ MissingReport r1 = mock(MissingReport.class);
+ MissingReport r2 = mock(MissingReport.class);
+ MissingReport r3 = mock(MissingReport.class);
+
+ when(r1.getReportImages()).thenReturn(Collections.emptyList());
+ when(r2.getReportImages()).thenReturn(Collections.emptyList());
+ when(r3.getReportImages()).thenReturn(Collections.emptyList());
+
+ when(missingReportRepository.findByDate(any(LocalDate.class)))
+ .thenReturn(new ArrayList<>(List.of(r1, r2, r3)));
+
+ when(missingReportDetailStrategy.toDetailDto(any(MissingReport.class), anyList(), eq(false)))
+ .thenReturn(mock(MissingReportDetailResponseDTO.class));
+
+ // when
+ List resultZero =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(0);
+
+ List resultNegative =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(-5);
+
+ // then
+ assertThat(resultZero).hasSize(1);
+ assertThat(resultNegative).hasSize(1);
+
+ verify(missingReportRepository, times(2)).findByDate(any(LocalDate.class));
+ verify(missingReportDetailStrategy, times(2))
+ .toDetailDto(any(MissingReport.class), anyList(), eq(false));
+ }
+
+ @Test
+ @DisplayName("imageUrl에서 null/blank/중복 제거 후 DTO로 전달한다")
+ void getRandomMissingReportsWithS3_filtersNullBlankAndDistinctImageUrls() {
+ // given
+ MissingReport report = mock(MissingReport.class);
+
+ ReportImage imgNull = mock(ReportImage.class);
+ when(imgNull.getImageUrl()).thenReturn(null);
+
+ ReportImage imgBlank = mock(ReportImage.class);
+ when(imgBlank.getImageUrl()).thenReturn(" ");
+
+ ReportImage imgA1 = mock(ReportImage.class);
+ when(imgA1.getImageUrl()).thenReturn("https://cdn.findyou.store/a.jpg");
+
+ ReportImage imgA2 = mock(ReportImage.class);
+ when(imgA2.getImageUrl()).thenReturn("https://cdn.findyou.store/a.jpg");
+
+ ReportImage imgB = mock(ReportImage.class);
+ when(imgB.getImageUrl()).thenReturn("https://cdn.findyou.store/b.jpg");
+
+ when(report.getReportImages()).thenReturn(List.of(imgNull, imgBlank, imgA1, imgA2, imgB));
+
+ when(missingReportRepository.findByDate(any(LocalDate.class)))
+ .thenReturn(new ArrayList<>(List.of(report)));
+
+ MissingReportDetailResponseDTO dto = mock(MissingReportDetailResponseDTO.class);
+
+ ArgumentCaptor> urlCaptor = ArgumentCaptor.forClass(List.class);
+ when(missingReportDetailStrategy.toDetailDto(eq(report), urlCaptor.capture(), eq(false)))
+ .thenReturn(dto);
+
+ // when
+ List result =
+ missingReportRetrieveWithS3Service.getRandomMissingReportsWithS3(1);
+
+ // then
+ assertThat(result).hasSize(1).containsExactly(dto);
+ assertThat(urlCaptor.getValue())
+ .containsExactly(
+ "https://cdn.findyou.store/a.jpg",
+ "https://cdn.findyou.store/b.jpg"
+ );
+
+ verify(missingReportRepository, times(1)).findByDate(any(LocalDate.class));
+ verify(missingReportDetailStrategy, times(1))
+ .toDetailDto(eq(report), anyList(), eq(false));
+ }
+
+}
diff --git a/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3ServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3ServiceImplTest.java
new file mode 100644
index 00000000..0780552d
--- /dev/null
+++ b/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ProtectingReportRetrieveWithS3ServiceImplTest.java
@@ -0,0 +1,278 @@
+package com.kuit.findyou.domain.report.service.retrieve;
+
+import com.kuit.findyou.domain.image.model.ReportImage;
+import com.kuit.findyou.domain.report.dto.response.ProtectingReportDetailResponseDTO;
+import com.kuit.findyou.domain.report.model.ProtectingReport;
+import com.kuit.findyou.domain.report.repository.ProtectingReportRepository;
+import com.kuit.findyou.domain.report.service.detail.strategy.ProtectingReportDetailStrategy;
+import com.kuit.findyou.global.infrastructure.FileUploadingFailedException;
+import com.kuit.findyou.global.infrastructure.ImageUploader;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+@Transactional
+@ActiveProfiles("test")
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class ProtectingReportRetrieveWithS3ServiceImplTest {
+
+ @Mock
+ private ProtectingReportRepository protectingReportRepository;
+
+ @Mock
+ private ImageUploader imageUploader;
+
+ @Mock
+ private ProtectingReportDetailStrategy protectingReportDetailStrategy;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private ProtectingReportRetrieveWithS3ServiceImpl protectingReportRetrieveWithS3Service;
+
+ @Test
+ @DisplayName("어제, 오늘 게시된 보호글이 하나도 없으면 빈 리스트를 반환")
+ void getRandomProtectingReportsWithS3_whenNoReports_thenThrow() {
+ // given
+ when(protectingReportRepository.findByDate(any(LocalDate.class))).thenReturn(Collections.emptyList());
+
+ // when
+ List result =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(3);
+
+ // then
+ assertThat(result).isEmpty();
+
+ verify(protectingReportRepository, times(1))
+ .findByDate(any(LocalDate.class));
+
+ verifyNoInteractions(imageUploader, protectingReportDetailStrategy);
+ }
+
+ @Test
+ @DisplayName("요청 개수보다 보호글이 적으면 전체 개수만큼만 DTO를 반환한다")
+ void getRandomProtectingReportsWithS3_whenCountGreaterThanSize_thenReturnAllReports() {
+ // given
+ ProtectingReport report1 = mock(ProtectingReport.class);
+ ProtectingReport report2 = mock(ProtectingReport.class);
+
+ // 이미지 없다고 가정
+ when(report1.getReportImages()).thenReturn(Collections.emptyList());
+ when(report2.getReportImages()).thenReturn(Collections.emptyList());
+
+ when(protectingReportRepository.findByDate(any())).thenReturn(new ArrayList<>(List.of(report1, report2)));
+
+ ProtectingReportDetailResponseDTO dto1 = mock(ProtectingReportDetailResponseDTO.class);
+ ProtectingReportDetailResponseDTO dto2 = mock(ProtectingReportDetailResponseDTO.class);
+
+ when(protectingReportDetailStrategy.toDetailDto(eq(report1), anyList(), eq(false)))
+ .thenReturn(dto1);
+ when(protectingReportDetailStrategy.toDetailDto(eq(report2), anyList(), eq(false)))
+ .thenReturn(dto2);
+
+ // when
+ List result =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(10);
+
+ // then
+ assertThat(result)
+ .hasSize(2)
+ .containsExactlyInAnyOrder(dto1, dto2);
+
+ verify(protectingReportRepository, times(1)).findByDate(any());
+ verify(protectingReportDetailStrategy, times(2))
+ .toDetailDto(any(ProtectingReport.class), anyList(), eq(false));
+
+ // 이미지가 없어서 upload는 호출 X
+ verifyNoInteractions(imageUploader);
+ }
+
+ @Test
+ @DisplayName("요청 개수가 전체 개수보다 적으면 요청 개수만큼만 반환한다")
+ void getRandomProtectingReportsWithS3_whenCountLessThanSize_thenReturnCountReports() {
+ // given (보호글 3개, count=2)
+ ProtectingReport r1 = mock(ProtectingReport.class);
+ ProtectingReport r2 = mock(ProtectingReport.class);
+ ProtectingReport r3 = mock(ProtectingReport.class);
+
+ when(r1.getReportImages()).thenReturn(Collections.emptyList());
+ when(r2.getReportImages()).thenReturn(Collections.emptyList());
+ when(r3.getReportImages()).thenReturn(Collections.emptyList());
+
+ when(protectingReportRepository.findByDate(any()))
+ .thenReturn(new ArrayList<>(List.of(r1, r2, r3)));
+
+ when(protectingReportDetailStrategy.toDetailDto(any(), anyList(), eq(false)))
+ .thenReturn(mock(ProtectingReportDetailResponseDTO.class));
+
+ // when
+ List result =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(2);
+
+ // then
+ assertThat(result).hasSize(2);
+
+ verify(protectingReportRepository).findByDate(any());
+ verify(protectingReportDetailStrategy, times(2))
+ .toDetailDto(any(ProtectingReport.class), anyList(), eq(false));
+ }
+
+ @Test
+ @DisplayName("이미지 처리 중 예기치 못한 예외가 나도 전체 API는 실패하지 않는다")
+ void getRandomProtectingReportsWithS3_whenUnexpectedException_thenContinue() {
+ // given
+ ProtectingReport report = mock(ProtectingReport.class);
+ when(report.getId()).thenReturn(1L);
+
+ // 존재하지 않는 로컬 포트로 URL 세팅 → RestTemplate 호출 시 예외 발생
+ ReportImage img1 = mock(ReportImage.class);
+ when(img1.getImageUrl()).thenReturn("http://localhost:65535/nonexistent");
+
+ when(report.getReportImages()).thenReturn(List.of(img1));
+ when(protectingReportRepository.findByDate(any()))
+ .thenReturn(new ArrayList<>(List.of(report)));
+
+ ProtectingReportDetailResponseDTO dto = mock(ProtectingReportDetailResponseDTO.class);
+ when(protectingReportDetailStrategy.toDetailDto(eq(report), anyList(), eq(false)))
+ .thenReturn(dto);
+
+ // when
+ List result =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(1);
+
+ // then (예외 X, DTO는 정상적으로 하나 반환)
+ assertThat(result)
+ .hasSize(1)
+ .containsExactly(dto);
+ }
+ @Test
+ @DisplayName("이미지를 정상적으로 받으면 S3에 업로드 & 업로드된 URL로 DTO 생성")
+ void getRandomProtectingReportsWithS3_successUploadFlow() {
+ // given
+ ProtectingReport report = mock(ProtectingReport.class);
+ when(report.getId()).thenReturn(100L);
+
+ ReportImage img1 = mock(ReportImage.class);
+ ReportImage img2 = mock(ReportImage.class);
+ when(img1.getImageUrl()).thenReturn("http://example.com/1.jpg");
+ when(img2.getImageUrl()).thenReturn("http://example.com/2.jpg");
+ when(report.getReportImages()).thenReturn(List.of(img1, img2));
+
+ when(protectingReportRepository.findByDate(any()))
+ .thenReturn(new ArrayList<>(List.of(report)));
+
+ // RestTemplate 가 바이트 배열 내려줌
+ when(restTemplate.getForObject(eq("http://example.com/1.jpg"), eq(byte[].class)))
+ .thenReturn(new byte[]{1, 2, 3});
+ when(restTemplate.getForObject(eq("http://example.com/2.jpg"), eq(byte[].class)))
+ .thenReturn(new byte[]{4, 5, 6});
+
+ // S3 업로더는 순서대로 두 번 URL을 반환
+ when(imageUploader.upload(any(byte[].class), anyString(), eq("image/jpeg")))
+ .thenReturn(
+ "https://s3.findyou.store/1.jpg",
+ "https://s3.findyou.store/2.jpg"
+ );
+
+ ProtectingReportDetailResponseDTO dto = mock(ProtectingReportDetailResponseDTO.class);
+
+ ArgumentCaptor> s3UrlsCaptor = ArgumentCaptor.forClass(List.class);
+ when(protectingReportDetailStrategy.toDetailDto(eq(report), s3UrlsCaptor.capture(), eq(false)))
+ .thenReturn(dto);
+
+ // when
+ List result =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(1);
+
+ // then
+ assertThat(result).hasSize(1).containsExactly(dto);
+
+ List capturedUrls = s3UrlsCaptor.getValue();
+ assertThat(capturedUrls)
+ .containsExactly(
+ "https://s3.findyou.store/1.jpg",
+ "https://s3.findyou.store/2.jpg"
+ );
+
+ verify(restTemplate, times(1))
+ .getForObject("http://example.com/1.jpg", byte[].class);
+ verify(restTemplate, times(1))
+ .getForObject("http://example.com/2.jpg", byte[].class);
+ verify(imageUploader, times(2))
+ .upload(any(byte[].class), anyString(), eq("image/jpeg"));
+ }
+
+ @Test
+ @DisplayName("S3 업로드나 이미지 처리에서 예외가 발생해도 DTO는 정상 반환된다")
+ void getRandomProtectingReportsWithS3_whenUploadFails_thenContinuePerImage() {
+ // given
+ ProtectingReport report = mock(ProtectingReport.class);
+ when(report.getId()).thenReturn(200L);
+
+ //첫 번째는 S3 업로드 실패, 두 번째는 다운로드 단계 예외
+ ReportImage img1 = mock(ReportImage.class);
+ ReportImage img2 = mock(ReportImage.class);
+ when(img1.getImageUrl()).thenReturn("http://example.com/1.jpg");
+ when(img2.getImageUrl()).thenReturn("http://example.com/2.jpg");
+ when(report.getReportImages()).thenReturn(List.of(img1, img2));
+
+ when(protectingReportRepository.findByDate(any()))
+ .thenReturn(new ArrayList<>(List.of(report)));
+
+ //1번 이미지는 S3 업로드 시 FileUploadingFailedException 발생
+ when(restTemplate.getForObject("http://example.com/1.jpg", byte[].class))
+ .thenReturn(new byte[]{1, 2, 3});
+ when(imageUploader.upload(any(byte[].class), anyString(), eq("image/jpeg")))
+ .thenThrow(new FileUploadingFailedException("업로드 실패"));
+
+ //2번 이미지는 다운로드 단계에서 바로 RuntimeException
+ when(restTemplate.getForObject("http://example.com/2.jpg", byte[].class))
+ .thenThrow(new RuntimeException("다운로드 오류"));
+
+ ProtectingReportDetailResponseDTO dto = mock(ProtectingReportDetailResponseDTO.class);
+ ArgumentCaptor> s3UrlsCaptor = ArgumentCaptor.forClass(List.class);
+ when(protectingReportDetailStrategy.toDetailDto(eq(report), s3UrlsCaptor.capture(), eq(false)))
+ .thenReturn(dto);
+
+ // when
+ List result =
+ protectingReportRetrieveWithS3Service.getRandomProtectingReportsWithS3(1);
+
+ // then
+ //전체 API는 죽지 않고 DTO 하나는 정상 반환
+ assertThat(result).hasSize(1).containsExactly(dto);
+
+ //두 이미지 모두 실패했으므로 이미지는 빈값
+ assertThat(s3UrlsCaptor.getValue()).isEmpty();
+
+ //예외가 던져지지 않음
+ verify(restTemplate, times(1))
+ .getForObject("http://example.com/1.jpg", byte[].class);
+ verify(restTemplate, times(1))
+ .getForObject("http://example.com/2.jpg", byte[].class);
+ verify(imageUploader, times(1))
+ .upload(any(byte[].class), anyString(), eq("image/jpeg"));
+ }
+
+}
diff --git a/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ReportRetrieveServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ReportRetrieveServiceImplTest.java
index 8e184878..4cac6c68 100644
--- a/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ReportRetrieveServiceImplTest.java
+++ b/src/test/java/com/kuit/findyou/domain/report/service/retrieve/ReportRetrieveServiceImplTest.java
@@ -69,6 +69,7 @@ void retrieveReportsWithFilters_success() {
"골든 리트리버",
"MISSING",
"2025-07-10",
+ "2025-12-31",
"서울시 강남구",
true
);
diff --git a/src/test/java/com/kuit/findyou/domain/user/controller/UserControllerTest.java b/src/test/java/com/kuit/findyou/domain/user/controller/UserControllerTest.java
index fd70511e..15b1a966 100644
--- a/src/test/java/com/kuit/findyou/domain/user/controller/UserControllerTest.java
+++ b/src/test/java/com/kuit/findyou/domain/user/controller/UserControllerTest.java
@@ -5,7 +5,9 @@
import com.kuit.findyou.domain.report.model.WitnessReport;
import com.kuit.findyou.domain.report.repository.InterestReportRepository;
import com.kuit.findyou.domain.user.dto.request.CheckDuplicateNicknameRequest;
+import com.kuit.findyou.domain.user.dto.request.RegisterUserRequest;
import com.kuit.findyou.domain.user.dto.response.CheckDuplicateNicknameResponse;
+import com.kuit.findyou.domain.user.dto.response.CheckGuestResponse;
import com.kuit.findyou.domain.user.dto.response.GetUserProfileResponse;
import com.kuit.findyou.domain.user.dto.request.AddInterestAnimalRequest;
import com.kuit.findyou.domain.user.dto.response.RegisterUserResponse;
@@ -14,6 +16,7 @@
import com.kuit.findyou.domain.user.repository.UserRepository;
import com.kuit.findyou.global.common.util.DatabaseCleaner;
import com.kuit.findyou.global.common.util.TestInitializer;
+import com.kuit.findyou.global.config.RedisTestContainersConfig;
import com.kuit.findyou.global.config.TestDatabaseConfig;
import com.kuit.findyou.global.jwt.util.JwtUtil;
import io.restassured.RestAssured;
@@ -28,29 +31,22 @@
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
-import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
-import software.amazon.awssdk.services.s3.model.PutObjectRequest;
-import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import java.time.LocalDate;
import java.util.Map;
-import static com.kuit.findyou.domain.user.constant.DefaultProfileImage.PUPPY;
import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
-import static com.kuit.findyou.global.common.util.RestAssuredUtils.multipartText;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("test")
-@Import(TestDatabaseConfig.class)
+@Import({RedisTestContainersConfig.class, TestDatabaseConfig.class})
class UserControllerTest {
@LocalServerPort
@@ -102,6 +98,7 @@ void retrieveViewedAnimals() {
.body("data.cards[0].title", equalTo("포메라니안"))
.body("data.cards[0].tag", equalTo("실종신고"))
.body("data.cards[0].date", equalTo("2024-10-05"))
+ .body("data.cards[0].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[0].location", equalTo("서울시 강남구"))
.body("data.cards[0].interest", equalTo(true))
.body("data.cards[1].reportId", equalTo(1))
@@ -109,6 +106,7 @@ void retrieveViewedAnimals() {
.body("data.cards[1].title", equalTo("믹스견"))
.body("data.cards[1].tag", equalTo("보호중"))
.body("data.cards[1].date", equalTo(LocalDate.now().toString()))
+ .body("data.cards[1].createdAt", equalTo(LocalDate.now().toString()))
.body("data.cards[1].location", equalTo("서울"))
.body("data.cards[1].interest", equalTo(true))
.body("data.lastId", equalTo(1))
@@ -119,16 +117,16 @@ void retrieveViewedAnimals() {
@Test
void should_Succeed_When_registerAnyoneWhoFirstLoggedIn() {
// given
- final String NICKNAME = "유저1";
+ String nickname = "유저1";
+ Long kakaoId = 123456L;
+ String deviceId= "device-01";
// when
RegisterUserResponse response = given()
-// .log().all()
- .contentType(ContentType.MULTIPART)
- .multiPart(multipartText("defaultProfileImageName", "default"))
- .multiPart(multipartText("nickname", NICKNAME))
- .multiPart(multipartText("kakaoId", "123456"))
- .multiPart(multipartText("deviceId", "device-001"))
+ .log().all()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(new RegisterUserRequest(nickname, kakaoId, deviceId))
.when()
.post("/api/v2/users")
.then()
@@ -138,9 +136,11 @@ void should_Succeed_When_registerAnyoneWhoFirstLoggedIn() {
.getObject("data", RegisterUserResponse.class);
// then
- Role role = jwtUtil.getRole(response.accessToken());
+ assertThat(response.nickname()).isEqualTo(nickname);
+ assertThat(response.accessToken()).isNotBlank();
+ assertThat(response.refreshToken()).isNotBlank();
- assertThat(response.nickname()).isEqualTo(NICKNAME);
+ Role role = jwtUtil.getRole(response.accessToken());
assertThat(role).isEqualTo(Role.USER);
}
@@ -494,116 +494,6 @@ void shouldSucceedToDeleteInterestAnimal_WhenItDoesNotExist() {
assertThat(interestReportRepository.existsByReportIdAndUserId(report.getId(), user.getId())).isFalse();
}
- @Test
- @DisplayName("기본 이미지로 변경 성공")
- void changeProfileImage_Default_Success() {
- // given
- User user = testInitializer.createTestUser();
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- // when & then
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart(multipartText("defaultProfileImageName", "chick"))
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("code", equalTo(SUCCESS.getCode()))
- .body("message", equalTo(SUCCESS.getMessage()))
- .body("data", nullValue());
-
- User updated = userRepository.findById(user.getId()).orElseThrow();
- assertThat(updated.getProfileImageUrl()).isEqualTo("chick");
- }
-
- @Test
- @DisplayName("파일 업로드로 변경 성공")
- void changeProfileImage_File_Success() {
- // given
- User user = testInitializer.createTestUser();
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- // when & then
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart("profileImageFile", "p.jpg", "fake".getBytes(), "image/jpeg")
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("code", equalTo(SUCCESS.getCode()))
- .body("message", equalTo(SUCCESS.getMessage()))
- .body("data", nullValue());
-
- User updated = userRepository.findById(user.getId()).orElseThrow();
- assertThat(updated.getProfileImageUrl()).startsWith("base-url");
- assertThat(updated.getProfileImageUrl()).endsWith("_p.jpg");
- }
-
- @Test
- @DisplayName("둘 다 제공(파일+기본명) → 400")
- void changeProfileImage_BothProvided_BadRequest() {
- // given
- User user = testInitializer.createTestUser();
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart(multipartText("defaultProfileImageName", "puppy"))
- .multiPart("profileImageFile", "p.jpg", "fake".getBytes(), "image/jpeg")
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("success", equalTo(false))
- .body("code", equalTo(400))
- .body("message", equalTo("Invalid request"));
- }
-
- @Test
- @DisplayName("둘 다 제공 X → 400")
- void changeProfileImage_NoneProvided_BadRequest() {
- // given
- User user = testInitializer.createTestUser();
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart("dummy", "dummy")
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("success", equalTo(false))
- .body("code", equalTo(400))
- .body("message", equalTo("Invalid request"));
- }
-
- @Test
- @DisplayName("잘못된 기본이미지 이름 → 400")
- void changeProfileImage_InvalidDefaultName_BadRequest() {
- // given
- User user = testInitializer.createTestUser();
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart(multipartText("defaultProfileImageName", "cat"))
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("success", equalTo(false))
- .body("code", equalTo(BAD_REQUEST.getCode()))
- .body("message", equalTo("Invalid request"));
- }
-
@Test
@DisplayName("사용자가 신고한 내역이 있다면 리턴한다.")
void shouldReturnUserReports_WhenTheyExist() {
@@ -684,93 +574,6 @@ void shouldReturnProfile_WhenUserExists() {
// then
assertThat(response.nickname()).isEqualTo(nickname);
- assertThat(response.profileImage()).isEqualTo(profileImage);
- }
-
- @Test
- @DisplayName("프로필 이미지 변경 후, 마이페이지 조회 시 변경된 URL이 반환")
- void changeProfileImage_and_VerifyWithMypageApi() {
- User user = testInitializer.createTestUser();
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
- .thenReturn(PutObjectResponse.builder().build());
-
- // === 프로필 이미지 변경 API 호출 ===
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart("profileImageFile", "p.jpg", "fake".getBytes(), "image/jpeg")
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("success", equalTo(true));
-
- // === 마이페이지 조회 API 호출 ===
- String profileImageUrl = given()
- .header("Authorization", "Bearer " + token)
- .when()
- .get("/api/v2/users/me") // 마이페이지 조회 API
- .then()
- .log().all()
- .statusCode(200)
- .extract()
- .jsonPath()
- .getString("data.profileImage");
-
- // === 반환된 URL이 CDN 주소 형식을 따르는지 확인 ===
- assertThat(profileImageUrl).startsWith("base-url");
- }
-
- @Test
- @DisplayName("프로필 - 기본이미지 -> 업로드 변경 시, 삭제 호출 없음")
- void changeProfileImage_DefaultToUploaded_NoDelete() {
- User user = testInitializer.createUserWithDefaultProfileImage(PUPPY);
-
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
- .thenReturn(PutObjectResponse.builder().build());
-
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart("profileImageFile", "p.jpg", "fake".getBytes(), "image/jpeg")
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("success", equalTo(true));
-
- verify(s3Client, times(0)).deleteObject(any(DeleteObjectRequest.class));
- }
-
- @Test
- @DisplayName("프로필 - 업로드된 파일 -> 새 업로드 변경 시, 기존 파일 삭제 호출됨")
- void changeProfileImage_FileToFile_DeleteOldFile() {
- // given
- //기존 프로필 이미지 존재
- User user = testInitializer.createUserWithUploadedProfileImage("base-url/old_profile.jpg");
- String token = jwtUtil.createAccessJwt(user.getId(), user.getRole());
-
- when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
- .thenReturn(PutObjectResponse.builder().build());
-
- // when
- //새 프로필 업로드
- given()
- .header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart("profileImageFile", "new.jpg", "fake".getBytes(), "image/jpeg")
- .when()
- .patch("/api/v2/users/me/profile-image")
- .then()
- .statusCode(200)
- .body("success", equalTo(true));
-
- // then
- verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest.class));
}
@Test
@@ -867,23 +670,46 @@ void shouldDenyRequest_WhenGuestDeletesAccount(){
}
@Test
- @DisplayName("비회원은 프로필 이미지를 변경할 수 없다")
- void shouldDenyRequest_WhenGuestChangesProfileImage() {
+ @DisplayName("게스트는 게스트로 조회된다")
+ void checkGuest_shouldReturnTrue_WhenGuest() {
// given
User guest = testInitializer.createTestGuest();
String token = jwtUtil.createAccessJwt(guest.getId(), guest.getRole());
- // when & then
- given()
+ // when
+ CheckGuestResponse response = given()
.header("Authorization", "Bearer " + token)
- .contentType(ContentType.MULTIPART)
- .multiPart(multipartText("defaultProfileImageName", "chick"))
.when()
- .patch("/api/v2/users/me/profile-image")
+ .post("/api/v2/users/me/check/guest")
.then()
- .statusCode(403)
- .body("success", equalTo(FORBIDDEN.getSuccess()))
- .body("code", equalTo(FORBIDDEN.getCode()))
- .body("message", equalTo(FORBIDDEN.getMessage()));
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getObject("data", CheckGuestResponse.class);
+
+ // then
+ assertThat(response.isGuest()).isTrue();
+ }
+
+ @Test
+ @DisplayName("게스트가 아닌 사용자는 게스트가 아니라고 조회된다 ")
+ void checkGuest_shouldReturnFalse_WhenNonGuest() {
+ // given
+ User guest = testInitializer.createTestUser();
+ String token = jwtUtil.createAccessJwt(guest.getId(), guest.getRole());
+
+ // when
+ CheckGuestResponse response = given()
+ .header("Authorization", "Bearer " + token)
+ .when()
+ .post("/api/v2/users/me/check/guest")
+ .then()
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getObject("data", CheckGuestResponse.class);
+
+ // then
+ assertThat(response.isGuest()).isFalse();
}
}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/user/repository/UserRepositoryTest.java b/src/test/java/com/kuit/findyou/domain/user/repository/UserRepositoryTest.java
index 32fa2d8d..78be189e 100644
--- a/src/test/java/com/kuit/findyou/domain/user/repository/UserRepositoryTest.java
+++ b/src/test/java/com/kuit/findyou/domain/user/repository/UserRepositoryTest.java
@@ -225,44 +225,4 @@ private User createUser(String name, Role role, Long kakaoId, String deviceId) {
.build();
return userRepository.save(build);
}
-
- @Test
- @DisplayName("더티체킹으로 기본 프로필 이미지(enum 문자열) 저장")
- void dirtyChecking_SaveDefaultProfileName() {
- // given
- User user = userRepository.save(User.builder()
- .name("유저")
- .role(Role.USER)
- .deviceId("dev-1")
- .build());
-
- // when
- user.changeProfileImage("puppy");
- em.flush();
- em.clear();
-
- // then
- User found = userRepository.findById(user.getId()).orElseThrow();
- assertThat(found.getProfileImageUrl()).isEqualTo("puppy");
- }
-
- @Test
- @DisplayName("더티체킹으로 CDN URL이 저장")
- void dirtyChecking_SaveCdnUrl() {
- // given
- User user = userRepository.save(User.builder()
- .name("유저")
- .role(Role.USER)
- .deviceId("dev-2")
- .build());
-
- // when
- user.changeProfileImage("https://cdn.example/profile.jpg");
- em.flush();
- em.clear();
-
- // then
- User found = userRepository.findById(user.getId()).orElseThrow();
- assertThat(found.getProfileImageUrl()).isEqualTo("https://cdn.example/profile.jpg");
- }
}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/user/service/ChangeProfileImageServiceTest.java b/src/test/java/com/kuit/findyou/domain/user/service/ChangeProfileImageServiceTest.java
deleted file mode 100644
index beda325b..00000000
--- a/src/test/java/com/kuit/findyou/domain/user/service/ChangeProfileImageServiceTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.kuit.findyou.domain.user.service;
-
-import com.kuit.findyou.domain.user.dto.request.ChangeProfileImageRequest;
-import com.kuit.findyou.domain.user.model.Role;
-import com.kuit.findyou.domain.user.model.User;
-import com.kuit.findyou.domain.user.repository.UserRepository;
-import com.kuit.findyou.domain.user.service.change_profileImage.ChangeProfileImageServiceImpl;
-import com.kuit.findyou.global.common.exception.CustomException;
-import com.kuit.findyou.global.infrastructure.FileUploadingFailedException;
-import com.kuit.findyou.global.infrastructure.ImageUploader;
-import jakarta.persistence.EntityNotFoundException;
-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 org.springframework.mock.web.MockMultipartFile;
-
-import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.IMAGE_UPLOAD_FAILED;
-import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-public class ChangeProfileImageServiceTest {
- @InjectMocks
- ChangeProfileImageServiceImpl service;
- @Mock
- UserRepository userRepository;
- @Mock
- ImageUploader imageUploader;
-
- @Test
- @DisplayName("기본 프로필(enum)로 변경 성공")
- void changeToDefaultProfileImage_Success() {
- // given
- User user = User.builder()
- .id(1L).name("유저").role(Role.USER).deviceId("1234").build();
-
- when(userRepository.getReferenceById(1L)).thenReturn(user);
-
- ChangeProfileImageRequest req = new ChangeProfileImageRequest(null, "chick");
-
- // when
- service.changeProfileImage(1L, req);
-
- // then
- assertThat(user.getProfileImageUrl()).isEqualTo("chick");
- }
-
- @Test
- @DisplayName("파일 업로드로 변경 성공")
- void changeToUploadedImage_Success() {
- // given
- User user = User.builder()
- .id(1L).name("유저").role(Role.USER).deviceId("dev").build();
-
- when(userRepository.getReferenceById(1L)).thenReturn(user);
- when(imageUploader.upload(any())).thenReturn("https://cdn.test/uploaded.jpg");
-
- MockMultipartFile file = new MockMultipartFile(
- "profileImageFile", "p.jpg", "image/jpeg", "x".getBytes());
- ChangeProfileImageRequest req = new ChangeProfileImageRequest(file, null);
-
- // when
- service.changeProfileImage(1L, req);
-
- // then
- assertThat(user.getProfileImageUrl()).isEqualTo("https://cdn.test/uploaded.jpg");
- }
-
- @Test
- @DisplayName("존재하지 않는 사용자이면 USER_NOT_FOUND")
- void userNotFound_Throws() {
- when(userRepository.getReferenceById(99L)).thenThrow(new EntityNotFoundException());
- ChangeProfileImageRequest req = new ChangeProfileImageRequest(null, "puppy");
-
- assertThatThrownBy(() -> service.changeProfileImage(99L, req))
- .isInstanceOf(CustomException.class)
- .hasMessageContaining(USER_NOT_FOUND.getMessage());
- }
-
- @Test
- @DisplayName("이미지 업로드 실패 시 IMAGE_UPLOAD_FAILED")
- void uploadFailed_Throws() {
- User user = User.builder()
- .id(1L).name("유저").role(Role.USER).deviceId("1234").build();
- when(userRepository.getReferenceById(1L)).thenReturn(user);
- when(imageUploader.upload(any())).thenThrow(new FileUploadingFailedException("S3 업로드 실패"));
-
- MockMultipartFile file = new MockMultipartFile(
- "profileImageFile", "p.jpg", "image/jpeg", "x".getBytes());
- ChangeProfileImageRequest req = new ChangeProfileImageRequest(file, null);
-
- assertThatThrownBy(() -> service.changeProfileImage(1L, req))
- .isInstanceOf(CustomException.class)
- .hasMessageContaining(IMAGE_UPLOAD_FAILED.getMessage());
- }
-}
diff --git a/src/test/java/com/kuit/findyou/domain/user/service/UserServiceTest.java b/src/test/java/com/kuit/findyou/domain/user/service/UserServiceTest.java
deleted file mode 100644
index 0e97f9c9..00000000
--- a/src/test/java/com/kuit/findyou/domain/user/service/UserServiceTest.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package com.kuit.findyou.domain.user.service;
-
-import com.kuit.findyou.domain.user.dto.request.RegisterUserRequest;
-import com.kuit.findyou.domain.user.dto.response.RegisterUserResponse;
-import com.kuit.findyou.domain.user.model.User;
-import com.kuit.findyou.domain.user.repository.UserRepository;
-import com.kuit.findyou.domain.user.service.register.RegisterUserServiceImpl;
-import com.kuit.findyou.global.common.exception.CustomException;
-import com.kuit.findyou.global.infrastructure.FileUploadingFailedException;
-import com.kuit.findyou.global.infrastructure.ImageUploader;
-import com.kuit.findyou.global.jwt.util.JwtUtil;
-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 org.springframework.mock.web.MockMultipartFile;
-
-import java.util.Optional;
-
-import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.*;
-
-@ExtendWith(MockitoExtension.class)
-class UserServiceTest {
- @InjectMocks
- private RegisterUserServiceImpl userService;
- @Mock
- private UserRepository userRepository;
- @Mock
- private ImageUploader imageUploader;
- @Mock
- private JwtUtil jwtUtil;
-
- @DisplayName("처음 로그인한 사용자가 회원등록을 하면 성공한다")
- @Test
- void should_Succeed_When_AnyoneWhoFirstLoggedInRegister(){
- // given
- final Long USER_ID = 1L;
- final String ACCESS_TOKEN = "accessToken";
-
- RegisterUserRequest request = getRegisterUserRequestWithoutImage();
-
- when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
- when(userRepository.findByDeviceId(request.deviceId())).thenReturn(Optional.empty());
- when(userRepository.save(any())).thenReturn(User.builder()
- .id(USER_ID)
- .name(request.nickname())
- .build());
-
- when(jwtUtil.createAccessJwt(any(), any())).thenReturn(ACCESS_TOKEN);
-
- // when
- RegisterUserResponse response = userService.registerUser(request);
-
- // then
- assertThat(response.userId()).isEqualTo(USER_ID);
- assertThat(response.nickname()).isEqualTo(request.nickname());
- assertThat(response.accessToken()).isEqualTo(ACCESS_TOKEN);
- }
-
- private static RegisterUserRequest getRegisterUserRequestWithoutImage() {
- RegisterUserRequest request = RegisterUserRequest.builder()
- .profileImageFile(null)
- .defaultProfileImageName("default")
- .nickname("유저1")
- .kakaoId(1234L)
- .deviceId("1234")
- .build();
- return request;
- }
-
- @DisplayName("비회원이 회원등록을 하면 성공한다")
- @Test
- void should_Succeed_When_GuestRegister(){
- // given
- final Long USER_ID = 1L;
- final String ACCESS_TOKEN = "accessToken";
-
- RegisterUserRequest request = getRegisterUserRequestWithImage();
-
- User user = mock(User.class);
-
- when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
- when(userRepository.findByDeviceId(request.deviceId())).thenReturn(Optional.of(user));
- when(userRepository.save(any())).thenReturn(User.builder()
- .id(USER_ID)
- .name(request.nickname())
- .build());
-
- when(imageUploader.upload(any())).thenReturn("image-url");
-
- when(jwtUtil.createAccessJwt(any(), any())).thenReturn(ACCESS_TOKEN);
-
- // when
- RegisterUserResponse response = userService.registerUser(request);
-
- // then
- assertThat(response.userId()).isEqualTo(USER_ID);
- assertThat(response.nickname()).isEqualTo(request.nickname());
- assertThat(response.accessToken()).isEqualTo(ACCESS_TOKEN);
-
- verify(user).upgradeToMember(eq(request.kakaoId()), eq(request.nickname()), eq("image-url"));
- }
-
- @DisplayName("이미 가입한 회원이 회원등록을 하면 예외를 발생시킨다")
- @Test
- void should_ThrowException_When_ExistingUserRegister(){
- // given
- final Long USER_ID = 1L;
-
- RegisterUserRequest request = getRegisterUserRequestWithoutImage();
-
- User user = User.builder()
- .id(USER_ID)
- .build();
-
- when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.of(user));
-
- // when
- // then
- assertThatThrownBy(() -> userService.registerUser(request))
- .isInstanceOf(CustomException.class)
- .hasMessageContaining(ALREADY_REGISTERED_USER.getMessage());
- }
-
- @DisplayName("올바르지 않은 기본 프로필로 요청하면 예외가 발생한다")
- @Test
- void should_ThrowException_When_RequestContainsInvalidDefaultProfile(){
- // given
- RegisterUserRequest request = getRegisterUserRequestWithWrongDefaultImageName();
-
- when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
-
- // when
- // then
- assertThatThrownBy(() -> userService.registerUser(request))
- .isInstanceOf(CustomException.class)
- .hasMessageContaining(BAD_REQUEST.getMessage());
- }
-
- private static RegisterUserRequest getRegisterUserRequestWithWrongDefaultImageName() {
- RegisterUserRequest request = RegisterUserRequest.builder()
- .profileImageFile(null)
- .defaultProfileImageName("default-image")
- .nickname("유저1")
- .kakaoId(1234L)
- .deviceId("1234")
- .build();
- return request;
- }
-
- @DisplayName("프로필 관련 내용 없이 요청하면 예외가 발생한다")
- @Test
- void should_ThrowException_When_RequestDoesNotContainProfile(){
- // given
- RegisterUserRequest request = getRegisterUserRequestWithoutProfile();
-
- when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
-
- // when
- // then
- assertThatThrownBy(() -> userService.registerUser(request))
- .isInstanceOf(CustomException.class)
- .hasMessageContaining(BAD_REQUEST.getMessage());
- }
-
- private static RegisterUserRequest getRegisterUserRequestWithoutProfile() {
- return RegisterUserRequest.builder()
- .profileImageFile(null)
- .defaultProfileImageName(null)
- .nickname("유저1")
- .kakaoId(1234L)
- .deviceId("1234")
- .build();
- }
-
- @DisplayName("이미지 업로드에 실패하면 예외가 발생한다")
- @Test
- void should_ThrowException_When_ImageUploadingFailed(){
- // given
- RegisterUserRequest request = getRegisterUserRequestWithImage();
-
- when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
- when(imageUploader.upload(any())).thenThrow(new FileUploadingFailedException("S3 업로드 실패"));
-
- // when
- // then
- assertThatThrownBy(() -> userService.registerUser(request))
- .isInstanceOf(CustomException.class)
- .hasMessageContaining(IMAGE_UPLOAD_FAILED.getMessage());
- }
-
- private static RegisterUserRequest getRegisterUserRequestWithImage() {
- MockMultipartFile profileImage = new MockMultipartFile(
- "profileImageFile",
- "test.jpg",
- "image/jpeg",
- "fake-image-content".getBytes()
- );
-
- RegisterUserRequest request = RegisterUserRequest.builder()
- .profileImageFile(profileImage)
- .defaultProfileImageName(null)
- .nickname("유저1")
- .kakaoId(1234L)
- .deviceId("1234")
- .build();
- return request;
- }
-}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/user/service/interest_report/InterestReportServiceTest.java b/src/test/java/com/kuit/findyou/domain/user/service/interest_report/InterestReportServiceTest.java
index 099510da..2fb7661c 100644
--- a/src/test/java/com/kuit/findyou/domain/user/service/interest_report/InterestReportServiceTest.java
+++ b/src/test/java/com/kuit/findyou/domain/user/service/interest_report/InterestReportServiceTest.java
@@ -22,6 +22,7 @@
import org.springframework.test.context.ActiveProfiles;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -158,6 +159,7 @@ private List getReportProjections(int size) {
"breed" + i,
ReportTag.PROTECTING.getValue(),
LocalDate.of(2025, 1, 1),
+ LocalDateTime.now(),
"city"
))
.collect(Collectors.toList());
diff --git a/src/test/java/com/kuit/findyou/domain/user/service/interest_report/ReportProjectionImpl.java b/src/test/java/com/kuit/findyou/domain/user/service/interest_report/ReportProjectionImpl.java
index c21b35ff..0e6bfc85 100644
--- a/src/test/java/com/kuit/findyou/domain/user/service/interest_report/ReportProjectionImpl.java
+++ b/src/test/java/com/kuit/findyou/domain/user/service/interest_report/ReportProjectionImpl.java
@@ -5,6 +5,7 @@
import lombok.Getter;
import java.time.LocalDate;
+import java.time.LocalDateTime;
@AllArgsConstructor
public class ReportProjectionImpl implements ReportProjection {
@@ -13,6 +14,7 @@ public class ReportProjectionImpl implements ReportProjection {
private final String breed;
private final String tag;
private final LocalDate date;
+ private final LocalDateTime createdAt;
private final String address;
public Long getReportId() { return reportId; }
@@ -20,5 +22,6 @@ public class ReportProjectionImpl implements ReportProjection {
public String getTitle() { return breed; }
public String getTag() { return tag; }
public LocalDate getDate() { return date; }
+ public LocalDateTime getCreatedAt() { return createdAt; }
public String getAddress() { return address; }
}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceTest.java b/src/test/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceTest.java
index 9226a370..f16e0d40 100644
--- a/src/test/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceTest.java
+++ b/src/test/java/com/kuit/findyou/domain/user/service/query/QueryUserServiceTest.java
@@ -27,17 +27,14 @@ void shouldReturnUserProfile_WhenUserExists(){
// given
final long userId = 1L;
final String name = "name";
- final String profileImage = "default";
User mockUser = mock(User.class);
when(userRepository.getReferenceById(anyLong())).thenReturn(mockUser);
when(mockUser.getName()).thenReturn(name);
- when(mockUser.getProfileImageUrl()).thenReturn(profileImage);
// when
GetUserProfileResponse userProfile = queryUserService.getUserProfile(userId);
// then
assertThat(userProfile.nickname()).isEqualTo(name);
- assertThat(userProfile.profileImage()).isEqualTo(profileImage);
}
}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceTest.java b/src/test/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceTest.java
new file mode 100644
index 00000000..8f5ae88c
--- /dev/null
+++ b/src/test/java/com/kuit/findyou/domain/user/service/register/RegisterUserServiceTest.java
@@ -0,0 +1,127 @@
+package com.kuit.findyou.domain.user.service.register;
+
+import com.kuit.findyou.domain.auth.service.IssueTokenService;
+import com.kuit.findyou.domain.user.dto.request.RegisterUserRequest;
+import com.kuit.findyou.domain.user.dto.response.RegisterUserResponse;
+import com.kuit.findyou.domain.user.model.User;
+import com.kuit.findyou.domain.user.repository.UserRepository;
+import com.kuit.findyou.global.common.exception.CustomException;
+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 java.util.Optional;
+
+import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class RegisterUserServiceTest {
+ @InjectMocks
+ private RegisterUserServiceImpl userService;
+ @Mock
+ private UserRepository userRepository;
+ @Mock
+ private IssueTokenService issueTokenService;
+
+ @DisplayName("처음 로그인한 사용자가 회원등록을 하면 성공한다")
+ @Test
+ void should_Succeed_When_AnyoneWhoFirstLoggedInRegister(){
+ // given
+ final Long USER_ID = 1L;
+ final String ACCESS_TOKEN = "accessToken";
+ final String REFRESH_TOKEN = "refreshToken";
+
+ RegisterUserRequest request = getRegisterUserRequest();
+
+ when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
+ when(userRepository.findByDeviceId(request.deviceId())).thenReturn(Optional.empty());
+ when(userRepository.save(any())).thenReturn(User.builder()
+ .id(USER_ID)
+ .name(request.nickname())
+ .build());
+
+ when(issueTokenService.issueAccessToken(any(), any())).thenReturn(ACCESS_TOKEN);
+ when(issueTokenService.issueRefreshToken(any())).thenReturn(REFRESH_TOKEN);
+
+ // when
+ RegisterUserResponse response = userService.registerUser(request);
+
+ // then
+ assertThat(response.userId()).isEqualTo(USER_ID);
+ assertThat(response.nickname()).isEqualTo(request.nickname());
+ assertThat(response.accessToken()).isEqualTo(ACCESS_TOKEN);
+ assertThat(response.refreshToken()).isEqualTo(REFRESH_TOKEN);
+ }
+
+ private static RegisterUserRequest getRegisterUserRequest() {
+ RegisterUserRequest request = RegisterUserRequest.builder()
+ .nickname("유저1")
+ .kakaoId(1234L)
+ .deviceId("1234")
+ .build();
+ return request;
+ }
+
+ @DisplayName("비회원이 회원등록을 하면 성공한다")
+ @Test
+ void should_Succeed_When_GuestRegister(){
+ // given
+ final Long USER_ID = 1L;
+ final String ACCESS_TOKEN = "accessToken";
+ final String REFRESH_TOKEN = "refreshToken";
+
+ RegisterUserRequest request = getRegisterUserRequest();
+
+ User user = mock(User.class);
+
+ when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.empty());
+ when(userRepository.findByDeviceId(request.deviceId())).thenReturn(Optional.of(user));
+ when(userRepository.save(any())).thenReturn(User.builder()
+ .id(USER_ID)
+ .name(request.nickname())
+ .build());
+
+ when(issueTokenService.issueAccessToken(any(), any())).thenReturn(ACCESS_TOKEN);
+ when(issueTokenService.issueRefreshToken(any())).thenReturn(REFRESH_TOKEN);
+
+ // when
+ RegisterUserResponse response = userService.registerUser(request);
+
+ // then
+ verify(user).upgradeToMember(eq(request.kakaoId()), eq(request.nickname()));
+
+ assertThat(response.userId()).isEqualTo(USER_ID);
+ assertThat(response.nickname()).isEqualTo(request.nickname());
+ assertThat(response.accessToken()).isEqualTo(ACCESS_TOKEN);
+ assertThat(response.refreshToken()).isEqualTo(REFRESH_TOKEN);
+ }
+
+ @DisplayName("이미 가입한 회원이 회원등록을 하면 예외를 발생시킨다")
+ @Test
+ void should_ThrowException_When_ExistingUserRegister(){
+ // given
+ final Long USER_ID = 1L;
+
+ RegisterUserRequest request = getRegisterUserRequest();
+
+ User user = User.builder()
+ .id(USER_ID)
+ .build();
+
+ when(userRepository.findByKakaoId(request.kakaoId())).thenReturn(Optional.of(user));
+
+ // when
+ // then
+ assertThatThrownBy(() -> userService.registerUser(request))
+ .isInstanceOf(CustomException.class)
+ .hasMessageContaining(ALREADY_REGISTERED_USER.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java b/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java
index 59f26dff..705e9eea 100644
--- a/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java
+++ b/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java
@@ -404,23 +404,6 @@ public void createTestCities() {
sigunguRepository.save(Sigungu.builder().name("해운대구").sido(busan).build());
}
- public User createUserWithDefaultProfileImage(DefaultProfileImage img) {
- User user = createTestUser();
- user.changeProfileImage(img.getName());
- return userRepository.save(user);
- }
-
- public User createUserWithUploadedProfileImage(String imageUrl) {
- User user = User.builder()
- .name("홍길동")
- .role(Role.USER)
- .deviceId("device-uploaded")
- .profileImageUrl(imageUrl)
- .build();
-
- return userRepository.save(user);
- }
-
public User createTestGuest() {
User user = User.builder()
.name("게스트")
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 1f62a029..b0ef0a06 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -28,9 +28,11 @@ spring:
findyou:
jwt:
- access:
- expire-ms: 6000000
+ expiration-ms:
+ access-token : 6000000
+ refresh-token : 6000000
secret-key : secretkey1224secretkey1224secretkey1224secretkey1224
+ refresh-token-redis-key-prefix: "refresh-token-redis-key-prefix:"
home-stats:
parsing-timeout-sec: 20