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 + + + + + + + + +
+
+ [Server] 장익환

+
+ [Server] 김상균

+
+ [Server] 정주연

+ +
+ +## ⚒️ 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