diff --git a/.github/workflows/frontend-deploy-dev.yml b/.github/workflows/frontend-deploy-dev.yml
index faeedacbe..22dd7f7d2 100644
--- a/.github/workflows/frontend-deploy-dev.yml
+++ b/.github/workflows/frontend-deploy-dev.yml
@@ -2,8 +2,11 @@ name: Frontend Deploy to Dev
on:
push:
- branches: [develop]
- paths: frontend/**
+ branches:
+ - develop
+ paths:
+ - frontend/**
+ - .github/**
jobs:
build-dockerfile:
@@ -18,8 +21,24 @@ jobs:
with:
node-version: '18.x'
+ - name: Cache Yarn global cache
+ uses: actions/cache@v3
+ with:
+ path: '**/.yarn'
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Cache Yarn project cache
+ uses: actions/cache@v3
+ with:
+ path: '**/.yarn/cache'
+ key: ${{ runner.os }}-yarn-project-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-project-
+
- name: Install dependencies
- run: yarn
+ run: yarn install --immutable
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
diff --git a/.github/workflows/frontend-deploy-prod.yml b/.github/workflows/frontend-deploy-prod.yml
index 07a457806..39d02463a 100644
--- a/.github/workflows/frontend-deploy-prod.yml
+++ b/.github/workflows/frontend-deploy-prod.yml
@@ -2,8 +2,11 @@ name: Frontend Deploy to Prod
on:
push:
- branches: [main]
- paths: frontend/**
+ branches:
+ - main
+ paths:
+ - frontend/**
+ - .github/**
jobs:
build-dockerfile:
@@ -18,8 +21,24 @@ jobs:
with:
node-version: '18.x'
+ - name: Cache Yarn global cache
+ uses: actions/cache@v3
+ with:
+ path: '**/.yarn'
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Cache Yarn project cache
+ uses: actions/cache@v3
+ with:
+ path: '**/.yarn/cache'
+ key: ${{ runner.os }}-yarn-project-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-project-
+
- name: Install dependencies
- run: yarn
+ run: yarn install --immutable
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
diff --git a/.github/workflows/frontend-storybook-deploy.yml b/.github/workflows/frontend-storybook-deploy.yml
new file mode 100644
index 000000000..5127cdf7e
--- /dev/null
+++ b/.github/workflows/frontend-storybook-deploy.yml
@@ -0,0 +1,73 @@
+name: Frontend Storybook Deploy To S3
+
+on:
+ push:
+ branches:
+ - develop
+ paths:
+ - frontend/**
+ - .github/**
+
+jobs:
+ build:
+ runs-on: ubuntu-22.04
+ defaults:
+ run:
+ working-directory: ./frontend
+ concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: true
+
+ steps:
+ - name: Use repository source
+ uses: actions/checkout@v3
+
+ - name: Use node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18.x
+
+ - name: Cache Yarn global cache
+ uses: actions/cache@v3
+ with:
+ path: '**/.yarn'
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Cache Yarn project cache
+ uses: actions/cache@v3
+ with:
+ path: '**/.yarn/cache'
+ key: ${{ runner.os }}-yarn-project-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-project-
+
+ - name: Install dependencies
+ run: yarn install --immutable
+
+ - name: Build Storybook
+ run: yarn build:sb
+
+ - name: Upload storybook build files to temp artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: Storybook
+ path: frontend/storybook-static
+ deploy:
+ needs: build
+ runs-on: front-dev-server
+ steps:
+ - name: Remove previous version app
+ working-directory: .
+ run: rm -rf frontend/storybook
+
+ - name: Download the built file to AWS EC2
+ uses: actions/download-artifact@v3
+ with:
+ name: Storybook
+ path: frontend/storybook
+
+ - name: Upload to S3
+ run: |
+ aws s3 sync frontend/storybook s3://2023-team-project/2023-zipgo/storybook --delete
diff --git a/README.md b/README.md
index 3f9cc0cee..5560f439c 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,43 @@
-# 2023-zipgo
+
+
+
+
+
+
+
+
-## 멤버
+**초보 집사의 반려동물 식품 선택을 도와주는 서비스**, 집사의고민입니다.
-| Frontend | Frontend | Frontend |
-| :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: |
-| | | |
-| [첵스](https://github.com/HyeryongChoi) | [노아이즈](https://github.com/n0eyes) | [에디](https://github.com/ksone02)
+전문가들의 사료 선택 기준은 무엇일까요?
+다양한 전문가들이 인정하는 [좋은 사료 선택 기준]([https://translucent-mallet-426.notion.site/dae305b85c8146399d7de6a0e74b773d](https://www.notion.so/dae305b85c8146399d7de6a0e74b773d?pvs=21)), **집사의 고민**에서 확인하세요!
-| Backend | Backend | Backend | Backend |
-|:--------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|:----------------------------------:| :----------------------------------:|
-| | | | |
-| [가비](https://github.com/iamjooon2) | [무민](https://github.com/parkmuhyeun) | [베베](https://github.com/wonyongChoi05) | [로지](https://github.com/kyY00n)
+- [zipgo.pet](https://zipgo.pet)
+- [zipgo.wiki](https://github.com/woowacourse-teams/2023-zipgo/wiki)
+
+# Skills
+
+### Frontend Tech Stack
+![Frontend Tech Stacks](https://github.com/woowacourse-teams/2023-zipgo/assets/24777828/4372f950-d29e-46a0-807c-79d78a7f6913)
+
+### Backend Tech Stack
+![Backend Tech Stacks](https://github.com/woowacourse-teams/2023-zipgo/assets/24777828/a4643676-e669-43f5-9bb4-d2f4e92ed470)
+
+# Infra Structure
+![Zigo Infra](https://github.com/woowacourse-teams/2023-zipgo/assets/24777828/09847ae1-2aac-41a1-9302-4a2b49a284e0)
+
+
+### Frontend Infra Structure
+![image](https://github.com/woowacourse-teams/2023-zipgo/assets/24777828/eac9e8af-2df3-486a-9812-6a39ab36eb4e)
+
+### Backend Infra Structure
+![image](https://github.com/woowacourse-teams/2023-zipgo/assets/24777828/c6b9ec96-1301-4088-98d1-1e1ec1d3b334)
+
+
+# Member
+
+| [첵스](https://github.com/HyeryongChoi) | [노아이즈](https://github.com/n0eyes) | [에디](https://github.com/ksone02) | [가비](https://github.com/iamjooon2) | [무민](https://github.com/parkmuhyeun) | [베베](https://github.com/wonyongChoi05) | [로지](https://github.com/kyY00n) |
+| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
+| | | | | | | |
+| Frontend | Frontend | Frontend | Backend | Backend | Backend | Backend |
diff --git a/backend/build.gradle b/backend/build.gradle
index 87c137236..a6a53ce12 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -52,6 +52,8 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
+ implementation group: 'com.datadoghq', name: 'dd-trace-api', version: '1.21.0'
+
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
@@ -110,6 +112,7 @@ jacocoTestReport {
'**/*Response*',
'**/*Request*',
'**/BaseTimeEntity',
+ '**/KakaoOAuthClient',
'**/*Dto*',
'**/S3*',
'**/*Interceptor*',
@@ -149,6 +152,7 @@ jacocoTestCoverageVerification {
'*.*Exception*',
'*.*Dto',
'*.S3*',
+ '*.KakaoOAuthClient*',
'*.*Response',
'*.*Request',
'*.BaseTimeEntity',
diff --git a/backend/deploy-dev.sh b/backend/deploy-dev.sh
deleted file mode 100644
index 0a468a106..000000000
--- a/backend/deploy-dev.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-PROJECT_NAME=zipgo-backend
-
-echo '> development 서버를 실행합니다'
-echo '> 현재 구동중인 애플리케이션 PID 확인'
-CURRENT_PID=$(sudo netstat -lntp | grep 8080 | awk '{print $7}' | cut -d'/' -f1)
-
-echo "현재 구동중인 애플리케이션 PID: $CURRENT_PID"
-
-if [ -z "$CURRENT_PID" ]; then
- echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
-else
- echo "> kill -15 $CURRENT_PID"
- sudo kill -15 $CURRENT_PID
- sleep 5
- if ps -p $CURRENT_PID > /dev/null; then
- echo "> 애플리케이션을 다시 종료합니다."
- sudo kill -9 $CURRENT_PID
- else
- echo "> 프로세스 아이디 $CURRENT_PID 가 성공적으로 종료되었습니다."
- fi
-fi
-
-sudo chmod +x ~/zipgo-backend-0.0.1-SNAPSHOT.jar
-sudo nohup java -jar \
--Dspring.profiles.active=dev \
--Dspring.config.import=file:/home/ubuntu/env.properties \
-~/zipgo-backend-0.0.1-SNAPSHOT.jar > ~/application.log 2>&1 &
diff --git a/backend/deploy-prod.sh b/backend/deploy-prod.sh
deleted file mode 100644
index d7dc8df90..000000000
--- a/backend/deploy-prod.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/sh
-PROJECT_NAME=zipgo-backend
-
-echo '> production 서버를 실행합니다'
-echo '> 현재 구동중인 애플리케이션 PID 확인'
-CURRENT_PID=$(sudo netstat -lntp | grep 8080 | awk '{print $7}' | cut -d'/' -f1)
-
-echo "현재 구동중인 애플리케이션 PID: $CURRENT_PID"
-
-if [ -z "$CURRENT_PID" ]; then
- echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
-else
- echo "> kill -15 $CURRENT_PID"
- sudo kill -15 $CURRENT_PID
- sleep 5
- if ps -p $CURRENT_PID > /dev/null; then
- echo "> 애플리케이션을 다시 종료합니다."
- sudo kill -9 $CURRENT_PID
- else
- echo "> 프로세스 아이디 $CURRENT_PID 가 성공적으로 종료되었습니다."
- fi
-fi
-
-sudo chmod +x ~/zipgo-backend-0.0.1-SNAPSHOT.jar
-sudo nohup java -jar \
--Dspring.profiles.active=prod \
--Dspring.config.import=file:/home/ubuntu/env.properties \
-~/zipgo-backend-0.0.1-SNAPSHOT.jar > ~/application.log 2>&1 &
diff --git a/backend/src/main/java/zipgo/aspect/ConnectionProxyHandler.java b/backend/src/main/java/zipgo/aspect/ConnectionProxyHandler.java
new file mode 100644
index 000000000..0db7678a9
--- /dev/null
+++ b/backend/src/main/java/zipgo/aspect/ConnectionProxyHandler.java
@@ -0,0 +1,40 @@
+package zipgo.aspect;
+
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+
+public class ConnectionProxyHandler implements InvocationHandler {
+
+ private static final String QUERY_PREPARE_STATEMENT = "prepareStatement";
+
+ private final Object connection;
+ private final QueryCounter queryCounter;
+
+ public ConnectionProxyHandler(Object connection, QueryCounter queryCounter) {
+ this.connection = connection;
+ this.queryCounter = queryCounter;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ countQuery(method);
+ return method.invoke(connection, args);
+ }
+
+ private void countQuery(Method method) {
+ if (isPrepareStatement(method) && isRequest()) {
+ queryCounter.increaseCount();
+ }
+ }
+
+ private boolean isPrepareStatement(Method method) {
+ return method.getName().equals(QUERY_PREPARE_STATEMENT);
+ }
+
+ private boolean isRequest() {
+ return RequestContextHolder.getRequestAttributes() != null;
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/aspect/QueryCounter.java b/backend/src/main/java/zipgo/aspect/QueryCounter.java
new file mode 100644
index 000000000..a0e47cf8e
--- /dev/null
+++ b/backend/src/main/java/zipgo/aspect/QueryCounter.java
@@ -0,0 +1,18 @@
+package zipgo.aspect;
+
+import lombok.Getter;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.annotation.RequestScope;
+
+@Getter
+@Component
+@RequestScope
+public class QueryCounter {
+
+ private int count;
+
+ public void increaseCount() {
+ count++;
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/aspect/QueryCounterAop.java b/backend/src/main/java/zipgo/aspect/QueryCounterAop.java
new file mode 100644
index 000000000..dd16c27e4
--- /dev/null
+++ b/backend/src/main/java/zipgo/aspect/QueryCounterAop.java
@@ -0,0 +1,29 @@
+package zipgo.aspect;
+
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Proxy;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class QueryCounterAop {
+
+ private final QueryCounter queryCounter;
+
+ @Around("execution(* javax.sql.DataSource.getConnection(..))")
+ public Object getConnection(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
+ Object connection = proceedingJoinPoint.proceed();
+
+ return Proxy.newProxyInstance(
+ connection.getClass().getClassLoader(),
+ connection.getClass().getInterfaces(),
+ new ConnectionProxyHandler(connection, queryCounter)
+ );
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/application/AuthService.java b/backend/src/main/java/zipgo/auth/application/AuthService.java
index 4a872710e..3e0abaad8 100644
--- a/backend/src/main/java/zipgo/auth/application/AuthService.java
+++ b/backend/src/main/java/zipgo/auth/application/AuthService.java
@@ -4,6 +4,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.auth.domain.RefreshToken;
+import zipgo.auth.dto.TokenDto;
+import zipgo.auth.domain.repository.RefreshTokenRepository;
import zipgo.auth.support.JwtProvider;
import zipgo.member.domain.Member;
import zipgo.member.domain.repository.MemberRepository;
@@ -13,18 +16,38 @@
@RequiredArgsConstructor
public class AuthService {
- private final OAuthClient oAuthClient;
private final JwtProvider jwtProvider;
private final MemberRepository memberRepository;
+ private final RefreshTokenRepository refreshTokenRepository;
- public String createToken(String authCode) {
- String accessToken = oAuthClient.getAccessToken(authCode);
- OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);
-
+ public TokenDto login(OAuthMemberResponse oAuthMemberResponse) {
Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
.orElseGet(() -> memberRepository.save(oAuthMemberResponse.toMember()));
+ return createTokens(member.getId());
+ }
+
+ private TokenDto createTokens(Long memberId) {
+ String accessToken = jwtProvider.createAccessToken(memberId);
+ String refreshToken = jwtProvider.createRefreshToken();
- return jwtProvider.create(String.valueOf(member.getId()));
+ refreshTokenRepository.deleteByMemberId(memberId);
+ refreshTokenRepository.save(new RefreshToken(memberId, refreshToken));
+
+ return TokenDto.of(accessToken, refreshToken);
+ }
+
+ public String renewAccessTokenBy(String refreshToken) {
+ jwtProvider.validateParseJws(refreshToken);
+
+ RefreshToken savedRefreshToken = refreshTokenRepository.getByToken(refreshToken);
+ Long memberId = savedRefreshToken.getMemberId();
+
+ return jwtProvider.createAccessToken(memberId);
+ }
+
+ public void logout(Long memberId) {
+ refreshTokenRepository.deleteByMemberId(memberId);
}
}
+
diff --git a/backend/src/main/java/zipgo/auth/application/AuthServiceFacade.java b/backend/src/main/java/zipgo/auth/application/AuthServiceFacade.java
new file mode 100644
index 000000000..33c3e7985
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/application/AuthServiceFacade.java
@@ -0,0 +1,29 @@
+package zipgo.auth.application;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.auth.domain.OAuthClient;
+import zipgo.auth.dto.TokenDto;
+
+@Service
+@RequiredArgsConstructor
+public class AuthServiceFacade {
+
+ private final AuthService authService;
+ private final OAuthClient oAuthClient;
+
+ public TokenDto login(String authCode, String redirectUri) {
+ OAuthMemberResponse oAuthMemberResponse = oAuthClient.request(authCode, redirectUri);
+ return authService.login(oAuthMemberResponse);
+ }
+
+ public String renewAccessTokenBy(String refreshToken) {
+ return authService.renewAccessTokenBy(refreshToken);
+ }
+
+ public void logout(Long memberId) {
+ authService.logout(memberId);
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/domain/OAuthClient.java b/backend/src/main/java/zipgo/auth/domain/OAuthClient.java
new file mode 100644
index 000000000..622d0cb84
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/domain/OAuthClient.java
@@ -0,0 +1,9 @@
+package zipgo.auth.domain;
+
+import zipgo.auth.application.dto.OAuthMemberResponse;
+
+public interface OAuthClient {
+
+ OAuthMemberResponse request(String authCode, String redirectUri);
+
+}
diff --git a/backend/src/main/java/zipgo/auth/application/OAuthClient.java b/backend/src/main/java/zipgo/auth/domain/OAuthMemberInfoClient.java
similarity index 51%
rename from backend/src/main/java/zipgo/auth/application/OAuthClient.java
rename to backend/src/main/java/zipgo/auth/domain/OAuthMemberInfoClient.java
index 7206114b3..d1a7329bb 100644
--- a/backend/src/main/java/zipgo/auth/application/OAuthClient.java
+++ b/backend/src/main/java/zipgo/auth/domain/OAuthMemberInfoClient.java
@@ -1,10 +1,8 @@
-package zipgo.auth.application;
+package zipgo.auth.domain;
import zipgo.auth.application.dto.OAuthMemberResponse;
-public interface OAuthClient {
-
- String getAccessToken(String authCode);
+public interface OAuthMemberInfoClient {
OAuthMemberResponse getMember(String accessToken);
diff --git a/backend/src/main/java/zipgo/auth/domain/OAuthTokenClient.java b/backend/src/main/java/zipgo/auth/domain/OAuthTokenClient.java
new file mode 100644
index 000000000..e022e3da8
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/domain/OAuthTokenClient.java
@@ -0,0 +1,7 @@
+package zipgo.auth.domain;
+
+public interface OAuthTokenClient {
+
+ String getAccessToken(String authCode, String redirectUri);
+
+}
diff --git a/backend/src/main/java/zipgo/auth/domain/RefreshToken.java b/backend/src/main/java/zipgo/auth/domain/RefreshToken.java
new file mode 100644
index 000000000..f0583f9ad
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/domain/RefreshToken.java
@@ -0,0 +1,35 @@
+package zipgo.auth.domain;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import zipgo.common.entity.BaseTimeEntity;
+
+import static lombok.EqualsAndHashCode.Include;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
+public class RefreshToken extends BaseTimeEntity {
+
+ @Id
+ @Include
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private Long memberId;
+
+ private String token;
+
+ public RefreshToken(Long memberId, String token) {
+ this.memberId = memberId;
+ this.token = token;
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/domain/repository/RefreshTokenRepository.java b/backend/src/main/java/zipgo/auth/domain/repository/RefreshTokenRepository.java
new file mode 100644
index 000000000..70e50692c
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/domain/repository/RefreshTokenRepository.java
@@ -0,0 +1,20 @@
+package zipgo.auth.domain.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import zipgo.auth.domain.RefreshToken;
+import zipgo.auth.exception.RefreshTokenNotFoundException;
+
+public interface RefreshTokenRepository extends JpaRepository {
+
+ Optional findByToken(String token);
+
+ default RefreshToken getByToken(String token) {
+ return findByToken(token)
+ .orElseThrow(RefreshTokenNotFoundException::new);
+ }
+
+ void deleteByMemberId(Long memberId);
+
+}
diff --git a/backend/src/main/java/zipgo/auth/dto/AccessTokenResponse.java b/backend/src/main/java/zipgo/auth/dto/AccessTokenResponse.java
new file mode 100644
index 000000000..0848c2036
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/dto/AccessTokenResponse.java
@@ -0,0 +1,11 @@
+package zipgo.auth.dto;
+
+public record AccessTokenResponse (
+ String accessToken
+) {
+
+ public static AccessTokenResponse from(String accessToken) {
+ return new AccessTokenResponse(accessToken);
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/dto/LoginResponse.java b/backend/src/main/java/zipgo/auth/dto/LoginResponse.java
new file mode 100644
index 000000000..678746f2e
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/dto/LoginResponse.java
@@ -0,0 +1,21 @@
+package zipgo.auth.dto;
+
+import java.util.List;
+import zipgo.member.domain.Member;
+import zipgo.pet.domain.Pet;
+
+public record LoginResponse(
+ String accessToken,
+ String refreshToken,
+ AuthResponse authResponse
+) {
+
+ public static LoginResponse of(TokenDto tokenDto, Member member, List pets) {
+ return new LoginResponse(
+ tokenDto.accessToken(),
+ tokenDto.refreshToken(),
+ AuthResponse.of(member, pets)
+ );
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/dto/TokenDto.java b/backend/src/main/java/zipgo/auth/dto/TokenDto.java
new file mode 100644
index 000000000..c83ffcc22
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/dto/TokenDto.java
@@ -0,0 +1,12 @@
+package zipgo.auth.dto;
+
+public record TokenDto(
+ String accessToken,
+ String refreshToken
+) {
+
+ public static TokenDto of(String accessToken, String refreshToken) {
+ return new TokenDto(accessToken, refreshToken);
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/dto/TokenResponse.java b/backend/src/main/java/zipgo/auth/dto/TokenResponse.java
deleted file mode 100644
index 17f5cc929..000000000
--- a/backend/src/main/java/zipgo/auth/dto/TokenResponse.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package zipgo.auth.dto;
-
-import java.util.List;
-import zipgo.member.domain.Member;
-import zipgo.pet.domain.Pet;
-
-public record TokenResponse(
- String accessToken,
- AuthResponse authResponse
-) {
-
- public static TokenResponse of(String token, Member member, List pets) {
- return new TokenResponse(
- token,
- AuthResponse.of(member, pets)
- );
- }
-
-}
diff --git a/backend/src/main/java/zipgo/auth/exception/RefreshTokenNotFoundException.java b/backend/src/main/java/zipgo/auth/exception/RefreshTokenNotFoundException.java
new file mode 100644
index 000000000..220b4666b
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/exception/RefreshTokenNotFoundException.java
@@ -0,0 +1,14 @@
+package zipgo.auth.exception;
+
+import zipgo.common.error.ErrorCode;
+import zipgo.common.error.ZipgoException;
+
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+
+public class RefreshTokenNotFoundException extends ZipgoException {
+
+ public RefreshTokenNotFoundException() {
+ super(new ErrorCode(NOT_FOUND, "존재하지 않는 리프레시 토큰입니다."));
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/exception/TokenMissingException.java b/backend/src/main/java/zipgo/auth/exception/TokenMissingException.java
new file mode 100644
index 000000000..78ee8b59c
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/exception/TokenMissingException.java
@@ -0,0 +1,14 @@
+package zipgo.auth.exception;
+
+import zipgo.common.error.ErrorCode;
+import zipgo.common.error.ZipgoException;
+
+import static org.springframework.http.HttpStatus.UNAUTHORIZED;
+
+public class TokenMissingException extends ZipgoException {
+
+ public TokenMissingException() {
+ super(new ErrorCode(UNAUTHORIZED, "토큰이 필요합니다."));
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthClient.java b/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthClient.java
index 65e4cab59..2309fb802 100644
--- a/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthClient.java
+++ b/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthClient.java
@@ -1,101 +1,23 @@
package zipgo.auth.infra.kakao;
import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.client.HttpClientErrorException;
-import org.springframework.web.client.RestTemplate;
-import zipgo.auth.application.OAuthClient;
import zipgo.auth.application.dto.OAuthMemberResponse;
-import zipgo.auth.exception.OAuthResourceNotBringException;
-import zipgo.auth.exception.OAuthTokenNotBringException;
-import zipgo.auth.infra.kakao.config.KakaoCredentials;
-import zipgo.auth.infra.kakao.dto.KakaoMemberResponse;
-import zipgo.auth.infra.kakao.dto.KakaoTokenResponse;
-
-import static java.util.Objects.requireNonNull;
-import static org.springframework.http.HttpMethod.GET;
-import static org.springframework.http.HttpMethod.POST;
-import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
+import zipgo.auth.domain.OAuthClient;
+import zipgo.auth.domain.OAuthMemberInfoClient;
+import zipgo.auth.domain.OAuthTokenClient;
@Component
@RequiredArgsConstructor
public class KakaoOAuthClient implements OAuthClient {
- private static final String ACCESS_TOKEN_URI = "https://kauth.kakao.com/oauth/token";
- private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
- private static final String GRANT_TYPE = "authorization_code";
-
- private final KakaoCredentials kakaoCredentials;
- private final RestTemplate restTemplate;
-
- @Override
- public String getAccessToken(String authCode) {
- HttpHeaders header = createRequestHeader();
- MultiValueMap body = createRequestBodyWithAuthCode(authCode);
- HttpEntity> request = new HttpEntity<>(body, header);
- ResponseEntity kakaoTokenResponse = getKakaoToken(request);
-
- return requireNonNull(requireNonNull(kakaoTokenResponse.getBody())).accessToken();
- }
-
- private HttpHeaders createRequestHeader() {
- HttpHeaders header = new HttpHeaders();
- header.setContentType(APPLICATION_FORM_URLENCODED);
- return header;
- }
-
- private MultiValueMap createRequestBodyWithAuthCode(String authCode) {
- MultiValueMap body = new LinkedMultiValueMap<>();
- body.add("grant_type", GRANT_TYPE);
- body.add("client_id", kakaoCredentials.getClientId());
- body.add("redirect_uri", kakaoCredentials.getRedirectUri());
- body.add("client_secret", kakaoCredentials.getClientSecret());
- body.add("code", authCode);
- return body;
- }
-
- private ResponseEntity getKakaoToken(HttpEntity> request) {
- try {
- return restTemplate.exchange(
- ACCESS_TOKEN_URI,
- POST,
- request,
- KakaoTokenResponse.class
- );
- } catch (HttpClientErrorException e) {
- throw new OAuthTokenNotBringException();
- }
- }
+ private final OAuthTokenClient kakaoOAuthTokenClient;
+ private final OAuthMemberInfoClient kakaoOAuthMemberInfoClient;
@Override
- public OAuthMemberResponse getMember(String accessToken) {
- HttpEntity request = createRequest(accessToken);
- ResponseEntity response = getKakaoMember(request);
- return response.getBody();
- }
-
- private HttpEntity createRequest(String accessToken) {
- HttpHeaders headers = new HttpHeaders();
- headers.setBearerAuth(accessToken);
- return new HttpEntity<>(headers);
- }
-
- private ResponseEntity getKakaoMember(HttpEntity request) {
- try {
- return restTemplate.exchange(
- USER_INFO_URI,
- GET,
- request,
- KakaoMemberResponse.class
- );
- } catch (HttpClientErrorException e) {
- throw new OAuthResourceNotBringException();
- }
+ public OAuthMemberResponse request(String authCode, String redirectUri) {
+ String accessToken = kakaoOAuthTokenClient.getAccessToken(authCode, redirectUri);
+ return kakaoOAuthMemberInfoClient.getMember(accessToken);
}
}
diff --git a/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthMemberInfoClient.java b/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthMemberInfoClient.java
new file mode 100644
index 000000000..2a22a8217
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthMemberInfoClient.java
@@ -0,0 +1,50 @@
+package zipgo.auth.infra.kakao;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+import zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.auth.domain.OAuthMemberInfoClient;
+import zipgo.auth.exception.OAuthResourceNotBringException;
+import zipgo.auth.infra.kakao.dto.KakaoMemberResponse;
+
+import static org.springframework.http.HttpMethod.GET;
+
+@Component
+@RequiredArgsConstructor
+public class KakaoOAuthMemberInfoClient implements OAuthMemberInfoClient {
+
+ private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
+
+ private final RestTemplate restTemplate;
+
+ @Override
+ public OAuthMemberResponse getMember(String accessToken) {
+ HttpEntity request = createRequest(accessToken);
+ ResponseEntity response = getKakaoMember(request);
+ return response.getBody();
+ }
+
+ private HttpEntity createRequest(String accessToken) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(accessToken);
+ return new HttpEntity<>(headers);
+ }
+
+ private ResponseEntity getKakaoMember(HttpEntity request) {
+ try {
+ return restTemplate.exchange(
+ USER_INFO_URI,
+ GET,
+ request,
+ KakaoMemberResponse.class
+ );
+ } catch (HttpClientErrorException e) {
+ throw new OAuthResourceNotBringException();
+ }
+ }
+}
diff --git a/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthTokenClient.java b/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthTokenClient.java
new file mode 100644
index 000000000..c0498efc4
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/infra/kakao/KakaoOAuthTokenClient.java
@@ -0,0 +1,70 @@
+package zipgo.auth.infra.kakao;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+import zipgo.auth.domain.OAuthTokenClient;
+import zipgo.auth.exception.OAuthTokenNotBringException;
+import zipgo.auth.infra.kakao.config.KakaoCredentials;
+import zipgo.auth.infra.kakao.dto.KakaoTokenResponse;
+
+import static java.util.Objects.requireNonNull;
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
+
+
+@Component
+@RequiredArgsConstructor
+public class KakaoOAuthTokenClient implements OAuthTokenClient {
+
+ private static final String ACCESS_TOKEN_URI = "https://kauth.kakao.com/oauth/token";
+ private static final String GRANT_TYPE = "authorization_code";
+
+ private final RestTemplate restTemplate;
+ private final KakaoCredentials kakaoCredentials;
+
+ @Override
+ public String getAccessToken(String authCode, String redirectUri) {
+ HttpHeaders header = createRequestHeader();
+ MultiValueMap body = createRequestBodyWithAuthCode(authCode, redirectUri);
+ HttpEntity> request = new HttpEntity<>(body, header);
+ ResponseEntity kakaoTokenResponse = getKakaoToken(request);
+
+ return requireNonNull(requireNonNull(kakaoTokenResponse.getBody())).accessToken();
+ }
+
+ private HttpHeaders createRequestHeader() {
+ HttpHeaders header = new HttpHeaders();
+ header.setContentType(APPLICATION_FORM_URLENCODED);
+ return header;
+ }
+
+ private MultiValueMap createRequestBodyWithAuthCode(String authCode, String redirectUri) {
+ MultiValueMap body = new LinkedMultiValueMap<>();
+ body.add("grant_type", GRANT_TYPE);
+ body.add("client_id", kakaoCredentials.getClientId());
+ body.add("redirect_uri", redirectUri);
+ body.add("client_secret", kakaoCredentials.getClientSecret());
+ body.add("code", authCode);
+ return body;
+ }
+
+ private ResponseEntity getKakaoToken(HttpEntity> request) {
+ try {
+ return restTemplate.exchange(
+ ACCESS_TOKEN_URI,
+ POST,
+ request,
+ KakaoTokenResponse.class
+ );
+ } catch (HttpClientErrorException e) {
+ throw new OAuthTokenNotBringException();
+ }
+ }
+}
diff --git a/backend/src/main/java/zipgo/auth/presentation/AuthController.java b/backend/src/main/java/zipgo/auth/presentation/AuthController.java
index 9ea42a20e..9f03e561b 100644
--- a/backend/src/main/java/zipgo/auth/presentation/AuthController.java
+++ b/backend/src/main/java/zipgo/auth/presentation/AuthController.java
@@ -1,6 +1,8 @@
package zipgo.auth.presentation;
import java.util.List;
+
+import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -8,11 +10,14 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import zipgo.auth.application.AuthService;
+import zipgo.auth.application.AuthServiceFacade;
+import zipgo.auth.dto.AccessTokenResponse;
import zipgo.auth.dto.AuthCredentials;
import zipgo.auth.dto.AuthResponse;
-import zipgo.auth.dto.TokenResponse;
+import zipgo.auth.dto.LoginResponse;
+import zipgo.auth.dto.TokenDto;
import zipgo.auth.support.JwtProvider;
+import zipgo.auth.support.ZipgoTokenExtractor;
import zipgo.member.application.MemberQueryService;
import zipgo.member.domain.Member;
import zipgo.pet.application.PetQueryService;
@@ -23,18 +28,36 @@
@RequiredArgsConstructor
public class AuthController {
- private final AuthService authService;
private final JwtProvider jwtProvider;
+ private final AuthServiceFacade authServiceFacade;
private final MemberQueryService memberQueryService;
private final PetQueryService petQueryService;
@PostMapping("/login")
- public ResponseEntity login(@RequestParam("code") String authCode) {
- String token = authService.createToken(authCode);
- String memberId = jwtProvider.getPayload(token);
+ public ResponseEntity login(
+ @RequestParam("code") String authCode,
+ @RequestParam("redirect-uri") String redirectUri
+ ) {
+ TokenDto tokenDto = authServiceFacade.login(authCode, redirectUri);
+
+ String memberId = jwtProvider.getPayload(tokenDto.accessToken());
Member member = memberQueryService.findById(Long.valueOf(memberId));
List pets = petQueryService.readMemberPets(member.getId());
- return ResponseEntity.ok(TokenResponse.of(token, member, pets));
+
+ return ResponseEntity.ok(LoginResponse.of(tokenDto, member, pets));
+ }
+
+ @GetMapping("/refresh")
+ public ResponseEntity renewTokens(HttpServletRequest request) {
+ String refreshToken = ZipgoTokenExtractor.extract(request);
+ String accessToken = authServiceFacade.renewAccessTokenBy(refreshToken);
+ return ResponseEntity.ok(AccessTokenResponse.from(accessToken));
+ }
+
+ @PostMapping("/logout")
+ public ResponseEntity logout(@Auth AuthCredentials authCredentials) {
+ authServiceFacade.logout(authCredentials.id());
+ return ResponseEntity.noContent().build();
}
@GetMapping
diff --git a/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java b/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java
index 947b47301..a8bedc2c7 100644
--- a/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java
+++ b/backend/src/main/java/zipgo/auth/presentation/JwtMandatoryArgumentResolver.java
@@ -1,7 +1,8 @@
package zipgo.auth.presentation;
-import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
+
+import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
@@ -33,9 +34,9 @@ public Object resolveArgument(
WebDataBinderFactory binderFactory
) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
- String token = BearerTokenExtractor.extract(Objects.requireNonNull(request));
- String id = jwtProvider.getPayload(token);
+ String accessToken = BearerTokenExtractor.extract(Objects.requireNonNull(request));
+ String id = jwtProvider.getPayload(accessToken);
return new AuthCredentials(Long.valueOf(id));
}
diff --git a/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java b/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java
index a438fe21c..20d279b8d 100644
--- a/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java
+++ b/backend/src/main/java/zipgo/auth/presentation/OptionalJwtArgumentResolver.java
@@ -1,7 +1,8 @@
package zipgo.auth.presentation;
-import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
+
+import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
@@ -21,6 +22,8 @@
@RequiredArgsConstructor
public class OptionalJwtArgumentResolver implements HandlerMethodArgumentResolver {
+ private static final String ZIPGO_HEADER = "Refresh";
+
private final JwtProvider jwtProvider;
@Override
@@ -41,8 +44,8 @@ public Object resolveArgument(
return null;
}
try {
- String token = BearerTokenExtractor.extract(Objects.requireNonNull(request));
- String id = jwtProvider.getPayload(token);
+ String accessToken = BearerTokenExtractor.extract(Objects.requireNonNull(request));
+ String id = jwtProvider.getPayload(accessToken);
return new AuthCredentials(Long.valueOf(id));
} catch (TokenInvalidException e) {
LoggingUtils.warn(e);
diff --git a/backend/src/main/java/zipgo/auth/support/BearerTokenExtractor.java b/backend/src/main/java/zipgo/auth/support/BearerTokenExtractor.java
index 006dcedeb..30b3a316d 100644
--- a/backend/src/main/java/zipgo/auth/support/BearerTokenExtractor.java
+++ b/backend/src/main/java/zipgo/auth/support/BearerTokenExtractor.java
@@ -3,6 +3,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.NoArgsConstructor;
import zipgo.auth.exception.TokenInvalidException;
+import zipgo.auth.exception.TokenMissingException;
import static lombok.AccessLevel.PRIVATE;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
@@ -21,7 +22,7 @@ public static String extract(HttpServletRequest request) {
private static void validate(String authorization) {
if (authorization == null) {
- throw new TokenInvalidException();
+ throw new TokenMissingException();
}
if (!authorization.matches(BEARER_JWT_REGEX)) {
throw new TokenInvalidException();
diff --git a/backend/src/main/java/zipgo/auth/support/JwtProvider.java b/backend/src/main/java/zipgo/auth/support/JwtProvider.java
index 164fef26b..f177f17ed 100644
--- a/backend/src/main/java/zipgo/auth/support/JwtProvider.java
+++ b/backend/src/main/java/zipgo/auth/support/JwtProvider.java
@@ -1,14 +1,15 @@
package zipgo.auth.support;
+import java.util.Date;
+import java.util.UUID;
+
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.security.SignatureException;
-import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component;
import zipgo.auth.exception.TokenExpiredException;
@@ -22,24 +23,34 @@
public class JwtProvider {
private final SecretKey key;
- private final long validityInMilliseconds;
+ private final long accessTokenExpirationTime;
+ private final long refreshTokenExpirationTime;
public JwtProvider(JwtCredentials jwtCredentials) {
this.key = hmacShaKeyFor(jwtCredentials.getSecretKey().getBytes(UTF_8));
- this.validityInMilliseconds = jwtCredentials.getExpireLength();
+ this.accessTokenExpirationTime = jwtCredentials.getAccessTokenExpirationTime();
+ this.refreshTokenExpirationTime = jwtCredentials.getRefreshTokenExpirationTime();
+ }
+
+ public String createAccessToken(Long memberId) {
+ return createToken(memberId.toString(), accessTokenExpirationTime, key);
}
- public String create(String payload) {
+ private String createToken(String payload, long expireLength, SecretKey key) {
Date now = new Date();
- Date validity = new Date(now.getTime() + validityInMilliseconds);
+ Date expiration = new Date(now.getTime() + expireLength);
return Jwts.builder()
.setSubject(payload)
.setIssuedAt(now)
- .setExpiration(validity)
+ .setExpiration(expiration)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
+ public String createRefreshToken() {
+ return createToken(UUID.randomUUID().toString(), refreshTokenExpirationTime, key);
+ }
+
public String getPayload(String token) {
return validateParseJws(token).getBody().getSubject();
}
@@ -50,13 +61,9 @@ public Jws validateParseJws(String token) {
.setSigningKey(key)
.build()
.parseClaimsJws(token);
- } catch (MalformedJwtException e) {
- throw new TokenInvalidException();
} catch (ExpiredJwtException e) {
throw new TokenExpiredException();
- } catch (SignatureException e) {
- throw new TokenInvalidException();
- } catch (Exception e) {
+ } catch (JwtException e) {
throw new TokenInvalidException();
}
}
diff --git a/backend/src/main/java/zipgo/auth/support/RefreshTokenCookieProvider.java b/backend/src/main/java/zipgo/auth/support/RefreshTokenCookieProvider.java
new file mode 100644
index 000000000..36866e245
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/support/RefreshTokenCookieProvider.java
@@ -0,0 +1,42 @@
+package zipgo.auth.support;
+
+import java.time.Duration;
+
+import org.springframework.http.ResponseCookie;
+import org.springframework.stereotype.Component;
+import zipgo.common.config.JwtCredentials;
+
+@Component
+public class RefreshTokenCookieProvider {
+
+ public static final String REFRESH_TOKEN = "refreshToken";
+ private static final String VALID_COOKIE_PATH = "/";
+ private static final String LOGOUT_COOKIE_VALUE = "";
+ private static final int LOGOUT_COOKIE_AGE = 0;
+
+ private final long expirationTime;
+
+ public RefreshTokenCookieProvider(JwtCredentials jwtCredentials) {
+ this.expirationTime = jwtCredentials.getRefreshTokenExpirationTime();
+ }
+
+ public ResponseCookie createCookie(String refreshToken) {
+ return ResponseCookie.from(REFRESH_TOKEN, refreshToken)
+ .sameSite("None")
+ .domain(".zipgo.pet")
+ .maxAge(Duration.ofMillis(expirationTime))
+ .path(VALID_COOKIE_PATH)
+ .secure(true)
+ .httpOnly(true)
+ .build();
+ }
+
+ public ResponseCookie createLogoutCookie() {
+ return ResponseCookie.from(REFRESH_TOKEN, LOGOUT_COOKIE_VALUE)
+ .sameSite("None")
+ .domain(".zipgo.pet")
+ .maxAge(LOGOUT_COOKIE_AGE)
+ .build();
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/auth/support/ZipgoTokenExtractor.java b/backend/src/main/java/zipgo/auth/support/ZipgoTokenExtractor.java
new file mode 100644
index 000000000..6b32d41a9
--- /dev/null
+++ b/backend/src/main/java/zipgo/auth/support/ZipgoTokenExtractor.java
@@ -0,0 +1,31 @@
+package zipgo.auth.support;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import zipgo.auth.exception.TokenInvalidException;
+import zipgo.auth.exception.TokenMissingException;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ZipgoTokenExtractor {
+
+ private static final String ZIPGO_HEADER = "Refresh";
+ private static final String ZIPGO_TYPE = "Zipgo ";
+ private static final String ZIPGO_JWT_REGEX = "^Zipgo [A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*$";
+
+ public static String extract(HttpServletRequest request) {
+ String authorization = request.getHeader(ZIPGO_HEADER);
+ validate(authorization);
+ return authorization.replace(ZIPGO_TYPE, "").trim();
+ }
+
+ private static void validate(String authorization) {
+ if (authorization == null) {
+ throw new TokenMissingException();
+ }
+ if (!authorization.matches(ZIPGO_JWT_REGEX)) {
+ throw new TokenInvalidException();
+ }
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/common/config/JwtCredentials.java b/backend/src/main/java/zipgo/common/config/JwtCredentials.java
index 925e14a88..d00902dc2 100644
--- a/backend/src/main/java/zipgo/common/config/JwtCredentials.java
+++ b/backend/src/main/java/zipgo/common/config/JwtCredentials.java
@@ -10,6 +10,7 @@
public class JwtCredentials {
private final String secretKey;
- private final long expireLength;
+ private final long accessTokenExpirationTime;
+ private final long refreshTokenExpirationTime;
}
diff --git a/backend/src/main/java/zipgo/common/config/WebConfig.java b/backend/src/main/java/zipgo/common/config/WebConfig.java
index 7b86b7593..6f57f92fb 100644
--- a/backend/src/main/java/zipgo/common/config/WebConfig.java
+++ b/backend/src/main/java/zipgo/common/config/WebConfig.java
@@ -1,6 +1,7 @@
package zipgo.common.config;
import java.util.List;
+
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -11,8 +12,10 @@
import zipgo.auth.presentation.JwtMandatoryArgumentResolver;
import zipgo.auth.presentation.OptionalJwtArgumentResolver;
import zipgo.auth.support.JwtProvider;
+import zipgo.common.interceptor.LoggingInterceptor;
import static org.springframework.http.HttpHeaders.LOCATION;
+import static org.springframework.http.HttpHeaders.SET_COOKIE;
@Configuration
@RequiredArgsConstructor
@@ -23,22 +26,32 @@ public class WebConfig implements WebMvcConfigurer {
private static final String MAIN_SERVER_DOMAIN = "https://zipgo.pet";
private static final String DEV_SERVER_DOMAIN = "https://dev.zipgo.pet";
private static final String FRONTEND_LOCALHOST = "http://localhost:3000";
+ private static final String HTTPS_FRONTEND_LOCALHOST = "https://localhost:3000";
private final AuthInterceptor authInterceptor;
+ private final LoggingInterceptor loggingInterceptor;
private final JwtProvider jwtProvider;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping(ALLOW_ALL_PATH)
.allowedMethods(ALLOWED_METHODS)
- .allowedOrigins(MAIN_SERVER_DOMAIN, DEV_SERVER_DOMAIN, FRONTEND_LOCALHOST)
- .exposedHeaders(LOCATION);
+ .allowedOrigins(
+ MAIN_SERVER_DOMAIN,
+ DEV_SERVER_DOMAIN,
+ FRONTEND_LOCALHOST,
+ HTTPS_FRONTEND_LOCALHOST
+ )
+ .allowCredentials(true)
+ .exposedHeaders(LOCATION, SET_COOKIE);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/auth");
+
+ registry.addInterceptor(loggingInterceptor);
}
@Override
diff --git a/backend/src/main/java/zipgo/common/interceptor/LoggingInterceptor.java b/backend/src/main/java/zipgo/common/interceptor/LoggingInterceptor.java
new file mode 100644
index 000000000..84a5b75d0
--- /dev/null
+++ b/backend/src/main/java/zipgo/common/interceptor/LoggingInterceptor.java
@@ -0,0 +1,32 @@
+package zipgo.common.interceptor;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import zipgo.aspect.QueryCounter;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class LoggingInterceptor implements HandlerInterceptor {
+
+ private static final String QUERY_COUNT_LOG = "METHOD: {}, URL: {}, STATUS_CODE: {}, QUERY_COUNT: {}";
+ private static final String QUERY_COUNT_WARN_LOG = "쿼리가 {}번 이상 실행되었습니다!!!";
+ private static final int WARN_QUERY_COUNT= 8;
+
+ private final QueryCounter queryCounter;
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+ Object handler, Exception ex) {
+ int queryCount = queryCounter.getCount();
+ log.info(QUERY_COUNT_LOG, request.getMethod(), request.getRequestURI(), response.getStatus(), queryCount);
+ if (queryCount >= WARN_QUERY_COUNT) {
+ log.warn(QUERY_COUNT_WARN_LOG, WARN_QUERY_COUNT);
+ }
+ }
+
+}
diff --git a/backend/src/main/java/zipgo/petfood/application/PetFoodQueryService.java b/backend/src/main/java/zipgo/petfood/application/PetFoodQueryService.java
index a426af9c1..66d9735fb 100644
--- a/backend/src/main/java/zipgo/petfood/application/PetFoodQueryService.java
+++ b/backend/src/main/java/zipgo/petfood/application/PetFoodQueryService.java
@@ -1,5 +1,6 @@
package zipgo.petfood.application;
+import datadog.trace.api.Trace;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -9,7 +10,6 @@
import zipgo.petfood.domain.PetFood;
import zipgo.petfood.domain.PrimaryIngredient;
import zipgo.petfood.domain.repository.FunctionalityRepository;
-import zipgo.petfood.domain.repository.PetFoodRepository;
import zipgo.petfood.domain.repository.PrimaryIngredientRepository;
import zipgo.petfood.dto.request.FilterRequest;
import zipgo.petfood.dto.response.FilterResponse;
@@ -26,7 +26,6 @@
@Transactional(readOnly = true)
public class PetFoodQueryService {
- private final PetFoodRepository petFoodRepository;
private final PetFoodQueryRepositoryImpl petFoodQueryRepository;
private final BrandRepository brandRepository;
private final FunctionalityRepository functionalityRepository;
@@ -39,6 +38,7 @@ public GetPetFoodsResponse getPetFoodsByFilters(FilterRequest filterDto, Long la
);
}
+ @Trace(resourceName = "식품 페이징 조회")
private List getPagingPetFoods(FilterRequest filterDto, Long lastPetFoodId, int size) {
return petFoodQueryRepository.findPagingPetFoods(
filterDto.brands(),
@@ -50,6 +50,7 @@ private List getPagingPetFoods(FilterRequest filterDto,
);
}
+ @Trace(resourceName = "식품 페이징 카운트 조회")
private Long getPetFoodsCount(FilterRequest filterDto) {
return petFoodQueryRepository.findPetFoodsCount(
filterDto.brands(),
diff --git a/backend/src/main/java/zipgo/petfood/dto/request/PetFoodSelectRequest.java b/backend/src/main/java/zipgo/petfood/dto/request/PetFoodSelectRequest.java
deleted file mode 100644
index 7eacd91e4..000000000
--- a/backend/src/main/java/zipgo/petfood/dto/request/PetFoodSelectRequest.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package zipgo.petfood.dto.request;
-
-public record PetFoodSelectRequest(
- String keyword,
- String brand,
- String primaryIngredients
-) {
-
-}
diff --git a/backend/src/main/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryImpl.java b/backend/src/main/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryImpl.java
index 2387b7b7f..8ed1aed44 100644
--- a/backend/src/main/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryImpl.java
+++ b/backend/src/main/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryImpl.java
@@ -2,6 +2,7 @@
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
+import datadog.trace.api.Trace;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@@ -26,6 +27,7 @@ public class PetFoodQueryRepositoryImpl implements PetFoodQueryRepository {
private final JPAQueryFactory queryFactory;
+ @Trace(resourceName = "식품 페이징 조회 쿼리")
public List findPagingPetFoods(
List brandsName,
List standards,
@@ -103,6 +105,7 @@ private BooleanExpression isContainFunctionalities(List functionalityLis
.functionality.name.in(functionalityList);
}
+ @Trace(resourceName = "식품 페이징 조회 카운트 쿼리")
@Override
public Long findPetFoodsCount(
List brandsName,
diff --git a/backend/src/main/java/zipgo/review/dto/response/type/RatingInfoResponse.java b/backend/src/main/java/zipgo/review/dto/response/type/RatingInfoResponse.java
index 0dd9eabb8..67538d89b 100644
--- a/backend/src/main/java/zipgo/review/dto/response/type/RatingInfoResponse.java
+++ b/backend/src/main/java/zipgo/review/dto/response/type/RatingInfoResponse.java
@@ -1,7 +1,7 @@
package zipgo.review.dto.response.type;
public record RatingInfoResponse(
- String rating,
+ String name,
int percentage
) {
diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev1.yml
similarity index 85%
rename from backend/src/main/resources/application-dev.yml
rename to backend/src/main/resources/application-dev1.yml
index ac53ab2d9..b6ec30123 100644
--- a/backend/src/main/resources/application-dev.yml
+++ b/backend/src/main/resources/application-dev1.yml
@@ -2,6 +2,7 @@
# port
server:
port: 8080
+ shutdown: graceful
---
# database
@@ -44,7 +45,8 @@ oauth:
# jwt
jwt:
secret-key: ${ZIPGO_SECRET_KEY}
- expire-length: ${EXPIRE_LENGTH}
+ access-token-expiration-time: ${ACCESS_TOKEN_EXPIRATION_TIME}
+ refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}
---
# aws
diff --git a/backend/src/main/resources/application-dev2.yml b/backend/src/main/resources/application-dev2.yml
new file mode 100644
index 000000000..9ca2c392a
--- /dev/null
+++ b/backend/src/main/resources/application-dev2.yml
@@ -0,0 +1,59 @@
+---
+# port
+server:
+ port: 8081
+ shutdown: graceful
+
+---
+# database
+spring:
+ datasource:
+ driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
+ url: ${JDBC_URL}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
+
+---
+# jpa
+spring:
+ jpa:
+ database: mysql
+ database-platform: org.hibernate.dialect.MySQL57Dialect
+ generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ format_sql: true
+ show_sql: true
+ default_batch_fetch_size: 30
+ defer-datasource-initialization: true
+
+ sql:
+ init:
+ mode: never
+
+---
+# OAUTH
+oauth:
+ kakao:
+ client-id: ${CLIENT_ID}
+ redirect-uri: ${REDIRECT_URI}
+ client-secret: ${CLIENT_SECRET}
+
+---
+# jwt
+jwt:
+ secret-key: ${ZIPGO_SECRET_KEY}
+ access-token-expiration-time: ${ACCESS_TOKEN_EXPIRATION_TIME}
+ refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}
+
+---
+# aws
+cloud:
+ aws:
+ s3:
+ bucket: ${WOOTECO_BUCKET}
+ zipgo-directory: ${ZIPGO_DIRECTORY}
+ env: ${ENVIRONMENT_DIRECTORY}
+ image-url: ${ZIPGO_IMAGE_URL}
diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml
index 6acacbc66..18d4ac7ef 100644
--- a/backend/src/main/resources/application-local.yml
+++ b/backend/src/main/resources/application-local.yml
@@ -44,7 +44,8 @@ oauth:
# jwt
jwt:
secret-key: ${ZIPGO_SECRET_KEY}
- expire-length: ${EXPIRE_LENGTH}
+ access-token-expiration-time: ${ACCESS_TOKEN_EXPIRATION_TIME}
+ refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}
---
# aws
diff --git a/backend/src/main/resources/application-local1.yml b/backend/src/main/resources/application-local1.yml
deleted file mode 100644
index 0b54dda4e..000000000
--- a/backend/src/main/resources/application-local1.yml
+++ /dev/null
@@ -1,47 +0,0 @@
----
-# port
-server:
- port: 8080
-
----
-# database
-spring:
- datasource:
- driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
- url: jdbc:log4jdbc:mysql://localhost:13306/zipgo
- username: root
- password: root
-# jpa
- jpa:
- database: mysql
- database-platform: org.hibernate.dialect.MySQL57Dialect
- generate-ddl: true
- hibernate:
- ddl-auto: none
- properties:
- hibernate:
- format_sql: true
- show_sql: true
- default_batch_fetch_size: 30
-# open-in-view: false
- defer-datasource-initialization: true
- sql:
- init:
- mode: never
-jwt:
- secret-key: 027eb3f4aa36e178497290da0fa573ee
- expire-length: 60480000000000000
-oauth:
- kakao:
- client-id: 027eb3f4aa36e178497290da0fa573ee
- redirect-uri: http://localhost:3000/login
-
----
-
-cloud:
- aws:
- s3:
- bucket: 2023-team-project
- zipgo-directory-name: 2023-zipgo
- pet-image-directory: prod/pet-image/
- image-url: https://image.zipgo.pet/
diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml
index b88e4090c..6c0c59a55 100644
--- a/backend/src/main/resources/application-prod.yml
+++ b/backend/src/main/resources/application-prod.yml
@@ -2,6 +2,7 @@
# port
server:
port: 8080
+ shutdown: graceful
---
# database
@@ -40,7 +41,8 @@ oauth:
# jwt
jwt:
secret-key: ${ZIPGO_SECRET_KEY}
- expire-length: ${EXPIRE_LENGTH}
+ access-token-expiration-time: ${ACCESS_TOKEN_EXPIRATION_TIME}
+ refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}
---
# log
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index 201e55321..edb06c216 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -2,7 +2,7 @@
# application
spring:
profiles:
- active: dev
+ active: local
---
#metric, log monitoring
diff --git a/backend/src/test/java/zipgo/ZipgoApplicationTests.java b/backend/src/test/java/zipgo/ZipgoApplicationTests.java
deleted file mode 100644
index fbbf9e809..000000000
--- a/backend/src/test/java/zipgo/ZipgoApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package zipgo;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class ZipgoApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
diff --git a/backend/src/test/java/zipgo/admin/application/AdminQueryServiceTest.java b/backend/src/test/java/zipgo/admin/application/AdminQueryServiceTest.java
index 918e81c0f..1065b5041 100644
--- a/backend/src/test/java/zipgo/admin/application/AdminQueryServiceTest.java
+++ b/backend/src/test/java/zipgo/admin/application/AdminQueryServiceTest.java
@@ -1,15 +1,14 @@
package zipgo.admin.application;
-import java.util.List;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import zipgo.admin.dto.FunctionalitySelectResponse;
import zipgo.admin.dto.PetFoodReadResponse;
+import zipgo.admin.dto.PrimaryIngredientSelectResponse;
import zipgo.brand.domain.Brand;
import zipgo.brand.domain.fixture.BrandFixture;
import zipgo.brand.domain.repository.BrandRepository;
-import zipgo.common.service.QueryServiceTest;
+import zipgo.common.service.ServiceTest;
import zipgo.petfood.domain.PetFood;
import zipgo.petfood.domain.fixture.FunctionalityFixture;
import zipgo.petfood.domain.fixture.PetFoodFixture;
@@ -17,12 +16,13 @@
import zipgo.petfood.domain.repository.FunctionalityRepository;
import zipgo.petfood.domain.repository.PetFoodRepository;
import zipgo.petfood.domain.repository.PrimaryIngredientRepository;
-import zipgo.admin.dto.PrimaryIngredientSelectResponse;
+
+import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
-class AdminQueryServiceTest extends QueryServiceTest {
+class AdminQueryServiceTest extends ServiceTest {
@Autowired
private FunctionalityRepository functionalityRepository;
diff --git a/backend/src/test/java/zipgo/admin/presentation/AdminControllerMockTest.java b/backend/src/test/java/zipgo/admin/presentation/AdminControllerMockMvcTest.java
similarity index 75%
rename from backend/src/test/java/zipgo/admin/presentation/AdminControllerMockTest.java
rename to backend/src/test/java/zipgo/admin/presentation/AdminControllerMockMvcTest.java
index 68a19cede..402bad4c6 100644
--- a/backend/src/test/java/zipgo/admin/presentation/AdminControllerMockTest.java
+++ b/backend/src/test/java/zipgo/admin/presentation/AdminControllerMockMvcTest.java
@@ -1,31 +1,18 @@
package zipgo.admin.presentation;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
-import com.fasterxml.jackson.databind.ObjectMapper;
+
import java.nio.charset.StandardCharsets;
import java.util.List;
-import org.junit.jupiter.api.DisplayNameGeneration;
-import org.junit.jupiter.api.DisplayNameGenerator;
+
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
-import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-import org.springframework.test.web.servlet.MockMvc;
-import zipgo.admin.application.AdminQueryService;
-import zipgo.admin.application.AdminService;
+import zipgo.common.acceptance.MockMvcTest;
import zipgo.admin.dto.BrandCreateRequest;
import zipgo.admin.dto.PetFoodCreateRequest;
-import zipgo.auth.presentation.AuthInterceptor;
-import zipgo.auth.presentation.JwtMandatoryArgumentResolver;
-import zipgo.auth.support.JwtProvider;
-import zipgo.image.application.ImageService;
import zipgo.petfood.domain.fixture.PetFoodFixture;
import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.resourceDetails;
@@ -37,36 +24,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static zipgo.brand.domain.fixture.BrandFixture.무민_브랜드_생성_요청;
-@AutoConfigureRestDocs
-@ExtendWith(SpringExtension.class)
-@SuppressWarnings("NonAsciiCharacters")
-@WebMvcTest(controllers = AdminController.class)
-@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-class AdminControllerMockTest {
-
- @Autowired
- private ObjectMapper objectMapper;
-
- @Autowired
- private MockMvc mockMvc;
-
- @MockBean
- private ImageService imageService;
-
- @MockBean
- private AdminService adminService;
-
- @MockBean
- private AdminQueryService adminQueryService;
-
- @MockBean
- private JwtProvider jwtProvider;
-
- @MockBean
- private AuthInterceptor authInterceptor;
-
- @MockBean
- private JwtMandatoryArgumentResolver argumentResolver;
+class AdminControllerMockMvcTest extends MockMvcTest {
@Test
void 브랜드를_생성하면_201이_반환된다() throws Exception {
@@ -145,4 +103,4 @@ class 식품_생성 {
}
-}
\ No newline at end of file
+}
diff --git a/backend/src/test/java/zipgo/auth/application/AuthServiceFacadeTest.java b/backend/src/test/java/zipgo/auth/application/AuthServiceFacadeTest.java
new file mode 100644
index 000000000..4e5ee76e4
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/application/AuthServiceFacadeTest.java
@@ -0,0 +1,118 @@
+package zipgo.auth.application;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+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 zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.auth.application.fixture.MemberResponseSuccessFixture;
+import zipgo.auth.domain.OAuthClient;
+import zipgo.auth.dto.TokenDto;
+import zipgo.auth.exception.OAuthResourceNotBringException;
+import zipgo.auth.exception.OAuthTokenNotBringException;
+import zipgo.auth.exception.TokenExpiredException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@SuppressWarnings("NonAsciiCharacters")
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class AuthServiceFacadeTest {
+
+ @Mock
+ private OAuthClient oAuthClient;
+
+ @Mock
+ private AuthService authService;
+
+ @InjectMocks
+ private AuthServiceFacade authServiceFacade;
+
+ @Test
+ void 로그인에_성공하면_토큰을_발급한다() {
+ // given
+ OAuthMemberResponse oAuthMemberResponse = new MemberResponseSuccessFixture();
+ when(oAuthClient.request("인가 코드", "리다이렉트 유알아이"))
+ .thenReturn(oAuthMemberResponse);
+ when(authService.login(oAuthMemberResponse))
+ .thenReturn(TokenDto.of("생성된 엑세스 토큰", "생성된 리프레시 토큰"));
+
+ // when
+ TokenDto 토큰 = authServiceFacade.login("인가 코드", "리다이렉트 유알아이");
+
+ // then
+ assertAll(
+ () -> assertThat(토큰.accessToken()).isEqualTo("생성된 엑세스 토큰"),
+ () -> assertThat(토큰.refreshToken()).isEqualTo("생성된 리프레시 토큰")
+ );
+ }
+
+ @Test
+ void 엑세스_토큰을_가져오지_못하면_예외가_발생한다() {
+ // given
+ when(oAuthClient.request("인가 코드", "리다이렉트 유알아이"))
+ .thenThrow(new OAuthTokenNotBringException());
+
+ // expect
+ assertThatThrownBy(() -> authServiceFacade.login("인가 코드", "리다이렉트 유알아이"))
+ .isInstanceOf(OAuthTokenNotBringException.class)
+ .hasMessageContaining("서드파티 서비스에서 토큰을 받아오지 못했습니다. 잠시후 다시 시도해주세요.");
+ }
+
+ @Test
+ void 사용자_정보를_가져오지_못하면_예외가_발생한다() {
+ // given
+ when(oAuthClient.request("인가 코드", "리다이렉트 유알아이"))
+ .thenThrow(new OAuthResourceNotBringException());
+
+ // expect
+ assertThatThrownBy(() -> authServiceFacade.login("인가 코드", "리다이렉트 유알아이"))
+ .isInstanceOf(OAuthResourceNotBringException.class)
+ .hasMessageContaining("서드파티 서비스에서 정보를 받아오지 못했습니다. 잠시후 다시 시도해주세요");
+ }
+
+
+ @Test
+ void 엑세스_토큰을_갱신할_수_있다() {
+ // given
+ when(authService.renewAccessTokenBy("리프레시 토큰"))
+ .thenReturn("갱신된 엑세스 토큰");
+ // when
+ String 리프레시_토큰 = authServiceFacade.renewAccessTokenBy("리프레시 토큰");
+
+ // then
+ assertThat(리프레시_토큰).isNotEmpty();
+ }
+
+ @Test
+ void 만료된_엑세스_토큰은_갱신시_예외가_발생한다() {
+ // given
+ when(authService.renewAccessTokenBy("리프레시 토큰"))
+ .thenThrow(new TokenExpiredException());
+ // expect
+ assertThatThrownBy(() -> authServiceFacade.renewAccessTokenBy("리프레시 토큰"))
+ .isInstanceOf(TokenExpiredException.class)
+ .hasMessageContaining("만료된 토큰입니다. 올바른 토큰으로 다시 시도해주세요.");
+ }
+
+ @Test
+ void 로그아웃_할_수_있다() {
+ // given
+ Long memberId = 123L;
+
+ // when
+ authServiceFacade.logout(memberId);
+
+ // then
+ verify(authService, times(1)).logout(memberId);
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/application/AuthServiceTest.java b/backend/src/test/java/zipgo/auth/application/AuthServiceTest.java
index b50368f62..e2fe1f6d9 100644
--- a/backend/src/test/java/zipgo/auth/application/AuthServiceTest.java
+++ b/backend/src/test/java/zipgo/auth/application/AuthServiceTest.java
@@ -1,90 +1,116 @@
package zipgo.auth.application;
-import java.util.Optional;
-import org.junit.jupiter.api.DisplayNameGeneration;
-import org.junit.jupiter.api.DisplayNameGenerator;
+
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import zipgo.auth.infra.kakao.KakaoOAuthClient;
-import zipgo.auth.infra.kakao.dto.KakaoMemberResponse;
+import zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.auth.application.fixture.MemberResponseSuccessFixture;
+import zipgo.auth.domain.RefreshToken;
+import zipgo.auth.domain.repository.RefreshTokenRepository;
+import zipgo.auth.dto.TokenDto;
+import zipgo.auth.exception.TokenExpiredException;
+import zipgo.auth.exception.TokenInvalidException;
import zipgo.auth.support.JwtProvider;
+import zipgo.common.config.JwtCredentials;
+import zipgo.common.service.ServiceTest;
import zipgo.member.domain.Member;
import zipgo.member.domain.repository.MemberRepository;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.when;
-import static zipgo.member.domain.fixture.MemberFixture.식별자_없는_멤버;
-import static zipgo.member.domain.fixture.MemberFixture.식별자_있는_멤버;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
-@SpringBootTest
-@SuppressWarnings("NonAsciiCharacters")
-@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-class AuthServiceTest {
+class AuthServiceTest extends ServiceTest {
- @MockBean
- private KakaoOAuthClient oAuthClient;
+ @Autowired
+ private RefreshTokenRepository refreshTokenRepository;
- @MockBean
+ @Autowired
private JwtProvider jwtProvider;
- @MockBean
+ @Autowired
private MemberRepository memberRepository;
@Autowired
private AuthService authService;
@Test
- void 기존_멤버의_토큰을_발급한다() {
+ void 로그인시_기존_회원이라면_토큰만_발급한다() {
// given
- 카카오_토큰_받기_성공();
- Member 저장된_멤버 = 식별자_있는_멤버();
- when(memberRepository.findByEmail("이메일"))
- .thenReturn(Optional.of(저장된_멤버));
- when(jwtProvider.create(String.valueOf(저장된_멤버.getId())))
- .thenReturn("생성된 토큰");
+ memberRepository.save(Member.builder().email("이메일").name("이름").build());
// when
- String 토큰 = authService.createToken("코드");
+ TokenDto 생성된_토큰 = authService.login(new MemberResponseSuccessFixture());
// then
- assertThat(토큰).isEqualTo("생성된 토큰");
+ assertThat(생성된_토큰).isNotNull();
}
@Test
- void 새로_가입한_멤버의_토큰을_발급한다() {
+ void 로그인시_새_회원은_가입_후_토큰을_발급한다() {
// given
- 카카오_토큰_받기_성공();
- when(memberRepository.findByEmail("이메일"))
- .thenReturn(Optional.empty());
- when(memberRepository.save(식별자_없는_멤버()))
- .thenReturn(식별자_있는_멤버());
- when(jwtProvider.create("1"))
- .thenReturn("생성된 토큰");
+ OAuthMemberResponse 외부_인프라_사용자_응답 = new MemberResponseSuccessFixture();
// when
- String 토큰 = authService.createToken("코드");
+ TokenDto 생성된_토큰 = authService.login(외부_인프라_사용자_응답);
// then
- assertThat(토큰).isEqualTo("생성된 토큰");
+ assertAll(
+ () -> assertThat(memberRepository.findByEmail(외부_인프라_사용자_응답.getEmail())).isNotEmpty(),
+ () -> assertThat(생성된_토큰).isNotNull()
+ );
}
- private void 카카오_토큰_받기_성공() {
- when(oAuthClient.getAccessToken("코드"))
- .thenReturn("토큰");
- when(oAuthClient.getMember("토큰"))
- .thenReturn(카카오_응답());
+ @Test
+ void 토큰_갱신시_리프레시_토큰을_받아_엑세스_토큰을_발급한다() {
+ // given
+ Long 사용자_식별자 = 1L;
+ String 리프레시_토큰 = jwtProvider.createRefreshToken();
+ refreshTokenRepository.save(new RefreshToken(사용자_식별자, 리프레시_토큰));
+
+ // when
+ String 엑세스_토큰 = authService.renewAccessTokenBy(리프레시_토큰);
+
+ // then
+ String 페이로드 = jwtProvider.getPayload(엑세스_토큰);
+ assertThat(페이로드).isEqualTo("1");
}
- private KakaoMemberResponse 카카오_응답() {
- return KakaoMemberResponse.builder().kakaoAccount(KakaoMemberResponse.KakaoAccount.builder()
- .email("이메일")
- .profile(KakaoMemberResponse.Profile.builder()
- .nickname("이름")
- .picture("사진")
- .build())
- .build()).build();
+ @Test
+ void 토큰_갱신시_리프레시_토큰이_유효하지_않다면_에외가_발생한다() {
+ // expect
+ assertThatThrownBy(() -> authService.renewAccessTokenBy("검증되지 않은 토큰"))
+ .isInstanceOf(TokenInvalidException.class)
+ .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요");
+ }
+
+ @Test
+ void 토큰_갱신시_리프레시_토큰이_만료됐다면_에외가_발생한다() {
+ // given
+ JwtProvider 만료된_토큰_생성기 = new JwtProvider(new JwtCredentials(
+ "this1-is2-zipgo3-test4-secret5-key6",
+ -9999,
+ -9999
+ ));
+ String 만료된_토큰 = 만료된_토큰_생성기.createRefreshToken();
+
+ // expect
+ assertThatThrownBy(() -> authService.renewAccessTokenBy(만료된_토큰))
+ .isInstanceOf(TokenExpiredException.class)
+ .hasMessageContaining("만료된 토큰입니다. 올바른 토큰으로 다시 시도해주세요");
+ }
+
+ @Test
+ void 로그아웃시_저장된_토큰이_사라진다() {
+ // given
+ Long memberId = 1L;
+ refreshTokenRepository.save(new RefreshToken(memberId, "저장시킨 토큰"));
+
+ // when
+ authService.logout(memberId);
+
+ // then
+ assertThat(refreshTokenRepository.findByToken("저장시킨 토큰")).isEmpty();
}
}
diff --git a/backend/src/test/java/zipgo/auth/application/fixture/MemberResponseSuccessFixture.java b/backend/src/test/java/zipgo/auth/application/fixture/MemberResponseSuccessFixture.java
new file mode 100644
index 000000000..43e35c571
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/application/fixture/MemberResponseSuccessFixture.java
@@ -0,0 +1,32 @@
+package zipgo.auth.application.fixture;
+
+import zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.member.domain.Member;
+
+public class MemberResponseSuccessFixture implements OAuthMemberResponse {
+
+ @Override
+ public String getEmail() {
+ return "이메일";
+ }
+
+ @Override
+ public String getNickName() {
+ return "이름";
+ }
+
+ @Override
+ public String getPicture() {
+ return "사진";
+ }
+
+ @Override
+ public Member toMember() {
+ return Member.builder()
+ .name(getNickName())
+ .profileImgUrl(getPicture())
+ .email(getEmail())
+ .build();
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/domain/repository/RefreshTokenRepositoryTest.java b/backend/src/test/java/zipgo/auth/domain/repository/RefreshTokenRepositoryTest.java
new file mode 100644
index 000000000..73735571c
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/domain/repository/RefreshTokenRepositoryTest.java
@@ -0,0 +1,50 @@
+package zipgo.auth.domain.repository;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import zipgo.auth.domain.RefreshToken;
+import zipgo.common.repository.RepositoryTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class RefreshTokenRepositoryTest extends RepositoryTest {
+
+ @Autowired
+ private RefreshTokenRepository refreshTokenRepository;
+
+ @Test
+ void 토큰으로_찾을_수_있다() {
+ // given
+ refreshTokenRepository.save(new RefreshToken(1L, "집고의 비밀 토큰"));
+
+ // when
+ RefreshToken 토큰 = refreshTokenRepository.getByToken("집고의 비밀 토큰");
+
+ // then
+ assertAll(
+ () -> assertThat(토큰.getId()).isEqualTo(1L),
+ () -> assertThat(토큰.getMemberId()).isEqualTo(1L),
+ () -> assertThat(토큰.getToken()).isEqualTo("집고의 비밀 토큰")
+ );
+ }
+
+ @Test
+ void 사용자_식별자로_삭제할_수_있다() {
+ // given
+ Long 사용자_식별자 = 1L;
+ RefreshToken 집고의_저장된_토큰 = refreshTokenRepository.save(
+ new RefreshToken(사용자_식별자, "집고의 비밀 토큰")
+ );
+
+ // when
+ refreshTokenRepository.deleteByMemberId(사용자_식별자);
+
+ // then
+ Optional 옵셔널_리프레시_토큰 = refreshTokenRepository.findByToken(집고의_저장된_토큰.getToken());
+ assertThat(옵셔널_리프레시_토큰).isEmpty();
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthClientTest.java b/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthClientTest.java
index d6b6fd93f..da1ea2a91 100644
--- a/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthClientTest.java
+++ b/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthClientTest.java
@@ -3,29 +3,24 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
-import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
-import zipgo.auth.application.dto.OAuthMemberResponse;
-import zipgo.auth.exception.OAuthResourceNotBringException;
import zipgo.auth.exception.OAuthTokenNotBringException;
import zipgo.auth.infra.kakao.config.KakaoCredentials;
-import zipgo.auth.infra.kakao.dto.KakaoMemberResponse;
import zipgo.auth.infra.kakao.dto.KakaoTokenResponse;
-import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;
-import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
@@ -35,94 +30,42 @@
class KakaoOAuthClientTest {
private static final String ACCESS_TOKEN_URI = "https://kauth.kakao.com/oauth/token";
- private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
private static final String GRANT_TYPE = "authorization_code";
- @Mock
private RestTemplate restTemplate;
+ @Mock
+ private KakaoOAuthTokenClient kakaoOAuthTokenClient;
+
+ @Mock
+ private KakaoOAuthMemberInfoClient kakaoOAuthMemberInfoClient;
+
+ @InjectMocks
private KakaoOAuthClient kakaoOAuthClient;
@BeforeEach
- public void setUp() {
+ void setUp() {
KakaoCredentials kakaoCredentials = new KakaoCredentials("clientId", "redirectUri", "clientSecret");
- kakaoOAuthClient = new KakaoOAuthClient(kakaoCredentials, restTemplate);
+ restTemplate = Mockito.mock(RestTemplate.class);
+ kakaoOAuthTokenClient = new KakaoOAuthTokenClient(restTemplate, kakaoCredentials);
+ kakaoOAuthMemberInfoClient = new KakaoOAuthMemberInfoClient(restTemplate);
+ kakaoOAuthClient = new KakaoOAuthClient(kakaoOAuthTokenClient, kakaoOAuthMemberInfoClient);
}
- @Nested
- class 카카오서버_성공_응답 {
-
- @Test
- void accessToken_을_가져올_수_있다() {
- // given
- var 토큰_요청 = 토큰_요청_생성();
- var 응답 = ResponseEntity.ok(
- new KakaoTokenResponse("accessToken", null, null, null, null, null)
- );
- when(restTemplate.exchange(ACCESS_TOKEN_URI, POST, 토큰_요청, KakaoTokenResponse.class))
- .thenReturn(응답);
-
- // when
- String accessToken = kakaoOAuthClient.getAccessToken("authCode");
-
- // then
- assertThat(accessToken).isEqualTo("accessToken");
- }
-
- @Test
- void 사용자_정보를_가져올_수_있다() {
- // given
- var 정보_요청 = 사용자_정보_요청_생성();
- var 응답 = ResponseEntity.ok(KakaoMemberResponse.builder().build());
- when(restTemplate.exchange(USER_INFO_URI, GET, 정보_요청, KakaoMemberResponse.class))
- .thenReturn(응답);
-
- // when
- OAuthMemberResponse oAuthMemberResponse = kakaoOAuthClient.getMember("accessToken");
-
- // then
- assertThat(oAuthMemberResponse).isNotNull();
- }
-
- }
-
- @Nested
- class 카카오서버_실패_응답 {
-
- @Test
- void 토큰_요청시_실패_응답을_받으면_예외가_발생한다() {
- // given
- var 요청 = 토큰_요청_생성();
- when(restTemplate.exchange(
- ACCESS_TOKEN_URI,
- POST,
- 요청,
- KakaoTokenResponse.class
- )).thenThrow(HttpClientErrorException.class);
-
- // expect
- assertThatThrownBy(() -> kakaoOAuthClient.getAccessToken("authCode"))
- .isInstanceOf(OAuthTokenNotBringException.class)
- .hasMessageContaining("서드파티 서비스에서 토큰을 받아오지 못했습니다. 잠시후 다시 시도해주세요.");
- }
-
- @Test
- void 사용자_정보_요청시_실패_응답을_받으면_예외가_발생한다() {
- // given
- var 요청 = 사용자_정보_요청_생성();
- when(restTemplate.exchange(
- USER_INFO_URI,
- GET,
- 요청,
- KakaoMemberResponse.class
- )).thenThrow(HttpClientErrorException.class);
-
- // expect
- assertThatThrownBy(() -> kakaoOAuthClient.getMember("accessToken"))
- .isInstanceOf(OAuthResourceNotBringException.class)
- .hasMessageContaining("서드파티 서비스에서 정보를 받아오지 못했습니다. 잠시후 다시 시도해주세요.");
- }
-
+ @Test
+ void 토큰을_가져오는데_실패하면_예외가_발생한다() {
+ // given
+ var 요청 = 토큰_요청_생성();
+ when(restTemplate.exchange(
+ ACCESS_TOKEN_URI,
+ POST,
+ 요청,
+ KakaoTokenResponse.class
+ )).thenThrow(HttpClientErrorException.class);
+
+ assertThatThrownBy(() -> kakaoOAuthClient.request("authCode", "redirectUri"))
+ .isInstanceOf(OAuthTokenNotBringException.class)
+ .hasMessageContaining("서드파티 서비스에서 토큰을 받아오지 못했습니다. 잠시후 다시 시도해주세요.");
}
private HttpEntity> 토큰_요청_생성() {
@@ -139,10 +82,4 @@ class 카카오서버_실패_응답 {
return new HttpEntity<>(body, header);
}
- private HttpEntity 사용자_정보_요청_생성() {
- HttpHeaders headers = new HttpHeaders();
- headers.setBearerAuth("accessToken");
- return new HttpEntity<>(headers);
- }
-
}
diff --git a/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthMemberInfoClientTest.java b/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthMemberInfoClientTest.java
new file mode 100644
index 000000000..e538fe7a9
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthMemberInfoClientTest.java
@@ -0,0 +1,78 @@
+package zipgo.auth.infra.kakao;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+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.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+import zipgo.auth.application.dto.OAuthMemberResponse;
+import zipgo.auth.exception.OAuthResourceNotBringException;
+import zipgo.auth.infra.kakao.config.KakaoCredentials;
+import zipgo.auth.infra.kakao.dto.KakaoMemberResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+import static org.springframework.http.HttpMethod.GET;
+
+@ExtendWith(MockitoExtension.class)
+@SuppressWarnings("NonAsciiCharacters")
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class KakaoOAuthMemberInfoClientTest {
+
+ private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private KakaoOAuthMemberInfoClient KakaoOAuthMemberInfoClient;
+
+ @Test
+ void 사용자_정보를_가져올_수_있다() {
+ // given
+ var 정보_요청 = 사용자_정보_요청_생성();
+ var 응답 = ResponseEntity.ok(KakaoMemberResponse.builder().build());
+ when(restTemplate.exchange(USER_INFO_URI, GET, 정보_요청, KakaoMemberResponse.class))
+ .thenReturn(응답);
+
+ // when
+ OAuthMemberResponse oAuthMemberResponse = KakaoOAuthMemberInfoClient.getMember("accessToken");
+
+ // then
+ assertThat(oAuthMemberResponse).isNotNull();
+ }
+
+ @Test
+ void 사용자_정보_요청시_실패_응답을_받으면_예외가_발생한다() {
+ // given
+ var 요청 = 사용자_정보_요청_생성();
+ when(restTemplate.exchange(
+ USER_INFO_URI,
+ GET,
+ 요청,
+ KakaoMemberResponse.class
+ )).thenThrow(HttpClientErrorException.class);
+
+ // expect
+ assertThatThrownBy(() -> KakaoOAuthMemberInfoClient.getMember("accessToken"))
+ .isInstanceOf(OAuthResourceNotBringException.class)
+ .hasMessageContaining("서드파티 서비스에서 정보를 받아오지 못했습니다. 잠시후 다시 시도해주세요.");
+ }
+
+
+ private HttpEntity 사용자_정보_요청_생성() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth("accessToken");
+ return new HttpEntity<>(headers);
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthTokenClientTest.java b/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthTokenClientTest.java
new file mode 100644
index 000000000..918568172
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/infra/kakao/KakaoOAuthTokenClientTest.java
@@ -0,0 +1,97 @@
+package zipgo.auth.infra.kakao;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+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.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+import zipgo.auth.exception.OAuthTokenNotBringException;
+import zipgo.auth.infra.kakao.config.KakaoCredentials;
+import zipgo.auth.infra.kakao.dto.KakaoTokenResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
+
+@ExtendWith(MockitoExtension.class)
+@SuppressWarnings("NonAsciiCharacters")
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class KakaoOAuthTokenClientTest {
+
+ private static final String ACCESS_TOKEN_URI = "https://kauth.kakao.com/oauth/token";
+ private static final String GRANT_TYPE = "authorization_code";
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private KakaoOAuthTokenClient kakaoOAuthTokenClient;
+
+ @BeforeEach
+ public void setUp() {
+ KakaoCredentials kakaoCredentials = new KakaoCredentials("clientId", "redirectUri", "clientSecret");
+ kakaoOAuthTokenClient = new KakaoOAuthTokenClient(restTemplate, kakaoCredentials);
+ }
+
+ @Test
+ void accessToken_을_가져올_수_있다() {
+ // given
+ var 토큰_요청 = 토큰_요청_생성();
+ var 응답 = ResponseEntity.ok(
+ new KakaoTokenResponse("accessToken", null, null, null, null, null)
+ );
+ when(restTemplate.exchange(ACCESS_TOKEN_URI, POST, 토큰_요청, KakaoTokenResponse.class))
+ .thenReturn(응답);
+
+ // when
+ String accessToken = kakaoOAuthTokenClient.getAccessToken("authCode", "redirectUri");
+
+ // then
+ assertThat(accessToken).isEqualTo("accessToken");
+ }
+
+ @Test
+ void 토큰_요청시_실패_응답을_받으면_예외가_발생한다() {
+ // given
+ var 요청 = 토큰_요청_생성();
+ when(restTemplate.exchange(
+ ACCESS_TOKEN_URI,
+ POST,
+ 요청,
+ KakaoTokenResponse.class
+ )).thenThrow(HttpClientErrorException.class);
+
+ // expect
+ assertThatThrownBy(() -> kakaoOAuthTokenClient.getAccessToken("authCode", "redirectUri"))
+ .isInstanceOf(OAuthTokenNotBringException.class)
+ .hasMessageContaining("서드파티 서비스에서 토큰을 받아오지 못했습니다. 잠시후 다시 시도해주세요.");
+ }
+
+
+ private HttpEntity> 토큰_요청_생성() {
+ HttpHeaders header = new HttpHeaders();
+ header.setContentType(APPLICATION_FORM_URLENCODED);
+
+ MultiValueMap body = new LinkedMultiValueMap<>();
+ body.add("grant_type", GRANT_TYPE);
+ body.add("client_id", "clientId");
+ body.add("redirect_uri", "redirectUri");
+ body.add("client_secret", "clientSecret");
+ body.add("code", "authCode");
+
+ return new HttpEntity<>(body, header);
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java b/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java
new file mode 100644
index 000000000..caa58443f
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/presentation/AuthControllerMockTest.java
@@ -0,0 +1,142 @@
+package zipgo.auth.presentation;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetDetails;
+import com.epages.restdocs.apispec.Schema;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.ResponseCookie;
+import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
+import org.springframework.restdocs.payload.JsonFieldType;
+import zipgo.auth.dto.TokenDto;
+import zipgo.auth.exception.OAuthTokenNotBringException;
+import zipgo.common.acceptance.MockMvcTest;
+import zipgo.pet.domain.fixture.PetFixture;
+
+import java.util.List;
+
+import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.resourceDetails;
+import static java.util.Collections.EMPTY_LIST;
+import static org.mockito.Mockito.when;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
+import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static zipgo.member.domain.fixture.MemberFixture.식별자_있는_멤버;
+
+class AuthControllerMockTest extends MockMvcTest {
+
+ private static final Schema 응답_형식 = Schema.schema("TokenResponse");
+ private static final ResourceSnippetDetails 문서_정보 = resourceDetails().summary("로그인")
+ .description("로그인 합니다.")
+ .responseSchema(응답_형식);
+
+ @Test
+ void 로그인_성공() throws Exception {
+ // given
+ var 토큰 = TokenDto.of("accessTokenValue", "refreshTokenValue");
+ when(authServiceFacade.login("인가_코드", "리다이렉트 유알아이"))
+ .thenReturn(토큰);
+ when(jwtProvider.getPayload(토큰.accessToken()))
+ .thenReturn("1");
+ when(memberQueryService.findById(1L))
+ .thenReturn(식별자_있는_멤버());
+ when(petQueryService.readMemberPets(1L))
+ .thenReturn(List.of(PetFixture.반려동물()));
+
+ // when
+ var 요청 = mockMvc.perform(post("/auth/login")
+ .param("code", "인가_코드")
+ .param("redirect-uri", "리다이렉트 유알아이"))
+ .andDo(로그인_성공_문서_생성());
+
+ // then
+ 요청.andExpect(status().isOk());
+ }
+
+ @Test
+ void 로그인_성공_후_사용자의_반려동물이_없다면_pets는_빈_배열이다() throws Exception {
+ // given
+ var 토큰 = TokenDto.of("accessTokenValue", "refreshTokenValue");
+ when(authServiceFacade.login("인가_코드", "리다이렉트 유알아이"))
+ .thenReturn(토큰);
+ when(jwtProvider.getPayload(토큰.accessToken()))
+ .thenReturn("1");
+ when(memberQueryService.findById(1L))
+ .thenReturn(식별자_있는_멤버());
+ when(petQueryService.readMemberPets(1L))
+ .thenReturn(EMPTY_LIST);
+
+ // when
+ var 요청 = mockMvc.perform(post("/auth/login")
+ .param("code", "인가_코드")
+ .param("redirect-uri", "리다이렉트 유알아이"))
+ .andDo(로그인_성공_반려동물_정보_없음_문서_생성());
+
+ // then
+ 요청.andExpect(status().isOk());
+ }
+
+ @Test
+ void 자원_서버의_토큰을_가져오는데_실패하면_예외가_발생한다() throws Exception {
+ // given
+ when(authServiceFacade.login("인가_코드", "리다이렉트 유알아이"))
+ .thenThrow(new OAuthTokenNotBringException());
+
+ // when
+ var 요청 = mockMvc.perform(post("/auth/login")
+ .param("code", "인가_코드")
+ .param("redirect-uri", "리다이렉트 유알아이"));
+
+ // then
+ 요청.andExpect(status().isBadGateway());
+ }
+
+ private RestDocumentationResultHandler 로그인_성공_문서_생성() {
+ return MockMvcRestDocumentationWrapper.document("로그인 성공 - 반려동물 기등록",
+ 문서_정보,
+ queryParameters(
+ parameterWithName("code").optional().description("로그인 API")
+ ),
+ responseFields(
+ fieldWithPath("accessToken").description("accessToken").type(JsonFieldType.STRING),
+ fieldWithPath("refreshToken").description("refreshToken").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.id").description("사용자 식별자").type(JsonFieldType.NUMBER),
+ fieldWithPath("authResponse.name").description("사용자 이름").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.email").description("사용자 이메일").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.profileImageUrl").description("사용자 프로필 사진").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.hasPet").description("반려동물 등록 여부").type(JsonFieldType.BOOLEAN),
+ fieldWithPath("authResponse.pets[].id").description("반려동물 식별자"),
+ fieldWithPath("authResponse.pets[].name").description("반려동물 이름").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.pets[].age").description("반려동물 나이").type(JsonFieldType.NUMBER),
+ fieldWithPath("authResponse.pets[].breedId").description("반려동물 견종 식별자"),
+ fieldWithPath("authResponse.pets[].breed").description("반려동물 견종").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.pets[].ageGroupId").description("반려동물 나이그룹 식별자").type(JsonFieldType.NUMBER),
+ fieldWithPath("authResponse.pets[].ageGroup").description("반려동물 나이그룹").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.pets[].petSize").description("반려동물 크기").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.pets[].gender").description("반려동물 성별").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.pets[].weight").description("반려동물 몸무게").type(JsonFieldType.NUMBER),
+ fieldWithPath("authResponse.pets[].imageUrl").description("반려동물 사진 주소").type(JsonFieldType.STRING)
+ ));
+ }
+
+ private RestDocumentationResultHandler 로그인_성공_반려동물_정보_없음_문서_생성() {
+ return MockMvcRestDocumentationWrapper.document("로그인 성공 - 반려동물 미등록",
+ 문서_정보,
+ queryParameters(
+ parameterWithName("code").optional().description("로그인 API")
+ ),
+ responseFields(
+ fieldWithPath("accessToken").description("accessToken").type(JsonFieldType.STRING),
+ fieldWithPath("refreshToken").description("accessToken").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.id").description("사용자 식별자").type(JsonFieldType.NUMBER),
+ fieldWithPath("authResponse.name").description("사용자 이름").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.email").description("사용자 이메일").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.profileImageUrl").description("사용자 프로필 사진").type(JsonFieldType.STRING),
+ fieldWithPath("authResponse.hasPet").description("반려동물 등록 여부").type(JsonFieldType.BOOLEAN),
+ fieldWithPath("authResponse.pets").description("반려동물 프로필").type(JsonFieldType.ARRAY)
+ ));
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java b/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java
index a905f0bc1..5740a6db2 100644
--- a/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java
+++ b/backend/src/test/java/zipgo/auth/presentation/AuthControllerTest.java
@@ -1,164 +1,144 @@
package zipgo.auth.presentation;
-import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
-import com.epages.restdocs.apispec.ResourceSnippetDetails;
-import com.epages.restdocs.apispec.Schema;
-import java.util.List;
-import org.junit.jupiter.api.DisplayNameGeneration;
-import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
-import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.payload.JsonFieldType;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-import org.springframework.test.web.servlet.MockMvc;
-import zipgo.auth.application.AuthService;
-import zipgo.auth.exception.OAuthTokenNotBringException;
+import org.springframework.restdocs.restassured.RestDocumentationFilter;
+import zipgo.auth.domain.RefreshToken;
+import zipgo.auth.domain.repository.RefreshTokenRepository;
import zipgo.auth.support.JwtProvider;
-import zipgo.member.application.MemberQueryService;
-import zipgo.pet.application.PetQueryService;
-import zipgo.pet.domain.fixture.PetFixture;
+import zipgo.common.acceptance.AcceptanceTest;
+import zipgo.common.config.JwtCredentials;
+import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document;
import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.resourceDetails;
-import static java.util.Collections.EMPTY_LIST;
-import static org.mockito.Mockito.when;
-import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static io.restassured.RestAssured.given;
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.http.HttpStatus.FORBIDDEN;
+import static org.springframework.http.HttpStatus.NO_CONTENT;
+import static org.springframework.http.HttpStatus.OK;
+import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
-import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
-import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import static zipgo.member.domain.fixture.MemberFixture.식별자_있는_멤버;
-
-@AutoConfigureRestDocs
-@ExtendWith(SpringExtension.class)
-@SuppressWarnings("NonAsciiCharacters")
-@WebMvcTest(controllers = AuthController.class)
-@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-class AuthControllerTest {
-
- private static final Schema 응답_형식 = Schema.schema("TokenResponse");
- private static final ResourceSnippetDetails 문서_정보 = resourceDetails().summary("로그인")
- .description("로그인 합니다.")
- .responseSchema(응답_형식);
+
+class AuthControllerTest extends AcceptanceTest {
+
+ private static final String TEST_SECRET_KEY = "this1-is2-zipgo3-test4-secret5-key6";
@Autowired
- private MockMvc mockMvc;
-
- @MockBean
- private MemberQueryService memberQueryService;
-
- @MockBean
- private PetQueryService petQueryService;
-
- @MockBean
- private JwtProvider jwtProvider;
-
- @MockBean
- private AuthService authService;
-
- @Test
- void 로그인_성공() throws Exception {
- // given
- when(authService.createToken("인가_코드"))
- .thenReturn("생성된_토큰");
- when(jwtProvider.getPayload("생성된_토큰"))
- .thenReturn("1");
- when(memberQueryService.findById(1L))
- .thenReturn(식별자_있는_멤버());
- when(petQueryService.readMemberPets(1L))
- .thenReturn(List.of(PetFixture.반려동물()));
-
- // when
- var 요청 = mockMvc.perform(post("/auth/login")
- .param("code", "인가_코드"))
- .andDo(로그인_성공_문서_생성());
-
- // then
- 요청.andExpect(status().isOk());
- }
+ private RefreshTokenRepository refreshTokenRepository;
+
+ @Nested
+ class 토큰_갱신 {
+
+ @Test
+ void 엑세스_토큰을_갱신할_수_있다() {
+ // given
+ var 리프레시_토큰 = jwtProvider.createRefreshToken();
+ refreshTokenRepository.save(new RefreshToken(1L, 리프레시_토큰));
+
+ var 요청_준비 = given(spec)
+ .filter(토큰_갱신_성공_문서_생성());
+
+ // when
+ var 응답 = 요청_준비.when()
+ .header("Refresh", "Zipgo " + 리프레시_토큰)
+ .get("/auth/refresh");
+
+ // then
+ 응답.then()
+ .assertThat().statusCode(OK.value());
+ }
+
+ @Test
+ void 실패하면_401_반환() {
+ var 토큰_생성기 = 유효기간_만료된_jwtProvider_생성();
+ var 유효기간_만료된_리프레시_토큰 = 토큰_생성기.createRefreshToken();
+ var 요청_준비 = given(spec)
+ .filter(토큰_갱신_실패_문서_생성());
+
+ // when
+ var 응답 = 요청_준비.when()
+ .header("Refresh", "Zipgo " + 유효기간_만료된_리프레시_토큰)
+ .get("/auth/refresh");
+
+ // then
+ 응답.then()
+ .assertThat().statusCode(UNAUTHORIZED.value());
+ }
+
+ private JwtProvider 유효기간_만료된_jwtProvider_생성() {
+ return new JwtProvider(
+ new JwtCredentials(
+ TEST_SECRET_KEY,
+ 9999999,
+ -99999999
+ )
+ );
+ }
- @Test
- void 로그인_성공_후_사용자의_반려동물이_없다면_pets는_빈_배열이다() throws Exception {
- // given
- when(authService.createToken("인가_코드"))
- .thenReturn("생성된_토큰");
- when(jwtProvider.getPayload("생성된_토큰"))
- .thenReturn("1");
- when(memberQueryService.findById(1L))
- .thenReturn(식별자_있는_멤버());
- when(petQueryService.readMemberPets(1L))
- .thenReturn(EMPTY_LIST);
-
- // when
- var 요청 = mockMvc.perform(post("/auth/login")
- .param("code", "인가_코드"))
- .andDo(로그인_성공_반려동물_정보_없음_문서_생성());
-
- // then
- 요청.andExpect(status().isOk());
}
- @Test
- void 자원_서버의_토큰을_가져오는데_실패하면_예외가_발생한다() throws Exception {
- // given
- when(authService.createToken("인가_코드"))
- .thenThrow(new OAuthTokenNotBringException());
+ @Nested
+ class 로그아웃 {
+
+ @Test
+ void 로그아웃_성공() {
+ // given
+ var 엑세스_토큰 = jwtProvider.createAccessToken(1L);
+ var 리프레시_토큰 = jwtProvider.createRefreshToken();
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + 엑세스_토큰)
+ .header("Refresh", "Zipgo " + 리프레시_토큰)
+ .filter(로그아웃_성공_문서_생성());
+
+ // when
+ var 응답 = 요청_준비.when()
+ .post("/auth/logout");
+
+ // then
+ 응답.then().statusCode(NO_CONTENT.value());
+ }
+
+ @Test
+ void 엑세스_토큰이_유효하지_않으면_로그아웃_실패() {
+ // given
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + "잘못된토큰이라네")
+ .header("Refresh", "Zipgo " + "잘못된토큰이라네")
+ .filter(로그아웃_실패_문서_생성());
+
+ // when
+ var 응답 = 요청_준비.when().post("/auth/logout");
+
+ // then
+ 응답.then().statusCode(FORBIDDEN.value());
+ }
- // when
- var 요청 = mockMvc.perform(post("/auth/login")
- .param("code", "인가_코드"));
-
- // then
- 요청.andExpect(status().isBadGateway());
}
- private RestDocumentationResultHandler 로그인_성공_문서_생성() {
- return MockMvcRestDocumentationWrapper.document("로그인 성공 - 반려동물 기등록",
- 문서_정보,
- queryParameters(
- parameterWithName("code").optional().description("로그인 API")
- ),
+ private RestDocumentationFilter 토큰_갱신_성공_문서_생성() {
+ return document("access token 갱신 성공",
+ resourceDetails().summary("토큰 갱신").description("access token을 갱신합니다"),
responseFields(
- fieldWithPath("accessToken").description("accessToken").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.id").description("사용자 식별자").type(JsonFieldType.NUMBER),
- fieldWithPath("authResponse.name").description("사용자 이름").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.email").description("사용자 이메일").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.profileImageUrl").description("사용자 프로필 사진").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.hasPet").description("반려동물 등록 여부").type(JsonFieldType.BOOLEAN),
- fieldWithPath("authResponse.pets[].id").description("반려동물 식별자"),
- fieldWithPath("authResponse.pets[].name").description("반려동물 이름").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.pets[].age").description("반려동물 나이").type(JsonFieldType.NUMBER),
- fieldWithPath("authResponse.pets[].breedId").description("반려동물 견종 식별자"),
- fieldWithPath("authResponse.pets[].breed").description("반려동물 견종").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.pets[].ageGroupId").description("반려동물 나이그룹 식별자").type(JsonFieldType.NUMBER),
- fieldWithPath("authResponse.pets[].ageGroup").description("반려동물 나이그룹").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.pets[].petSize").description("반려동물 크기").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.pets[].gender").description("반려동물 성별").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.pets[].weight").description("반려동물 몸무게").type(JsonFieldType.NUMBER),
- fieldWithPath("authResponse.pets[].imageUrl").description("반려동물 사진 주소").type(JsonFieldType.STRING)
+ fieldWithPath("accessToken").description("갱신된 accessToken").type(JsonFieldType.STRING)
));
}
- private RestDocumentationResultHandler 로그인_성공_반려동물_정보_없음_문서_생성() {
- return MockMvcRestDocumentationWrapper.document("로그인 성공 - 반려동물 미등록",
- 문서_정보,
- queryParameters(
- parameterWithName("code").optional().description("로그인 API")
- ),
- responseFields(
- fieldWithPath("accessToken").description("accessToken").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.id").description("사용자 식별자").type(JsonFieldType.NUMBER),
- fieldWithPath("authResponse.name").description("사용자 이름").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.email").description("사용자 이메일").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.profileImageUrl").description("사용자 프로필 사진").type(JsonFieldType.STRING),
- fieldWithPath("authResponse.hasPet").description("반려동물 등록 여부").type(JsonFieldType.BOOLEAN),
- fieldWithPath("authResponse.pets").description("반려동물 프로필").type(JsonFieldType.ARRAY)
- ));
+ private RestDocumentationFilter 토큰_갱신_실패_문서_생성() {
+ return document("access token 갱신 실패 (유효하지 않은 인증 형식)", resourceDetails()
+ .summary("토큰 갱신").responseSchema(에러_응답_형식));
+ }
+
+ private RestDocumentationFilter 로그아웃_성공_문서_생성() {
+ return document("로그아웃 성공", resourceDetails()
+ .summary("로그아웃")
+ );
+ }
+
+ private RestDocumentationFilter 로그아웃_실패_문서_생성() {
+ return document("로그아웃 실패 (유효하지 않은 인증 형식)", resourceDetails().summary("로그아웃").responseSchema(에러_응답_형식));
}
}
diff --git a/backend/src/test/java/zipgo/auth/support/BearerTokenExtractorTest.java b/backend/src/test/java/zipgo/auth/support/BearerTokenExtractorTest.java
index d86adab0b..885d19303 100644
--- a/backend/src/test/java/zipgo/auth/support/BearerTokenExtractorTest.java
+++ b/backend/src/test/java/zipgo/auth/support/BearerTokenExtractorTest.java
@@ -5,6 +5,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import zipgo.auth.exception.TokenInvalidException;
+import zipgo.auth.exception.TokenMissingException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -27,10 +28,10 @@ class BearerTokenExtractorTest {
}
@Test
- void 헤더에서_Authorization_키를_추출할_수_없으면_예외가_발생한다() {
+ void 토큰_형식이_유효하지_않으면_예외가_발생한다() {
// given
var 요청 = new MockHttpServletRequest();
- 요청.addHeader("ZipgoTestHeader", "Bearer aaaa.bbbb.cccc");
+ 요청.addHeader("Authorization", "Bearer 내맘대로토큰");
// expect
assertThatThrownBy(() -> BearerTokenExtractor.extract(요청))
@@ -39,10 +40,10 @@ class BearerTokenExtractorTest {
}
@Test
- void 토큰_형식이_유효하지_않으면_예외가_발생한다() {
+ void Authorization_키의_값이_Bearer_형식이_아니라면_예외가_발생한다() {
// given
var 요청 = new MockHttpServletRequest();
- 요청.addHeader("Authorization", "Bearer 내맘대로토큰");
+ 요청.addHeader("Authorization", "Basic aaaaa:bbbb");
// expect
assertThatThrownBy(() -> BearerTokenExtractor.extract(요청))
@@ -51,10 +52,10 @@ class BearerTokenExtractorTest {
}
@Test
- void Authorization_키의_값이_Bearer_형식이_아니라면_예외가_발생한다() {
+ void Bearer_null이면_예외가_발생한다() {
// given
var 요청 = new MockHttpServletRequest();
- 요청.addHeader("Authorization", "Basic aaaaa:bbbb");
+ 요청.addHeader("Authorization", "Bearer null");
// expect
assertThatThrownBy(() -> BearerTokenExtractor.extract(요청))
@@ -62,4 +63,15 @@ class BearerTokenExtractorTest {
.hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요.");
}
+ @Test
+ void 토큰이_없으면_MissingException이_발생한다() {
+ //given
+ var 요청 = new MockHttpServletRequest();
+
+ //expect
+ assertThatThrownBy(() -> BearerTokenExtractor.extract(요청))
+ .isInstanceOf(TokenMissingException.class)
+ .hasMessageContaining("토큰이 필요합니다.");
+ }
+
}
diff --git a/backend/src/test/java/zipgo/auth/support/JwtProviderTest.java b/backend/src/test/java/zipgo/auth/support/JwtProviderTest.java
index d72f51a78..87fb58db3 100644
--- a/backend/src/test/java/zipgo/auth/support/JwtProviderTest.java
+++ b/backend/src/test/java/zipgo/auth/support/JwtProviderTest.java
@@ -22,30 +22,39 @@ class JwtProviderTest {
private static final String TEST_SECRET_KEY = "this1-is2-zipgo3-test4-secret5-key6";
- private final JwtProvider jwtProvider = new JwtProvider(new JwtCredentials(TEST_SECRET_KEY, 100000L));
+ private final JwtProvider jwtProvider = new JwtProvider(new JwtCredentials(TEST_SECRET_KEY, 100000L, 100000L));
@Test
void 토큰을_생성한다() {
// given
- String 페이로드 = String.valueOf(1L);
+ Long 사용자_식별자 = 1L;
- // expect
- String 토큰 = jwtProvider.create(페이로드);
+ // when
+ String 엑세스_토큰 = jwtProvider.createAccessToken(사용자_식별자);
// then
- assertThat(토큰).isNotNull();
+ assertThat(엑세스_토큰).isNotNull();
+ }
+
+ @Test
+ void 리프레시_토큰을_생성한다() {
+ // given, when
+ String 리프레시_토큰 = jwtProvider.createRefreshToken();
+
+ // then
+ assertThat(리프레시_토큰).isNotEmpty();
}
@Test
void 올바른_토큰의_정보로_payload를_조회한다() {
// given
- String 페이로드 = String.valueOf(1L);
+ Long 사용자_식별자 = 1L;
// when
- String 토큰 = jwtProvider.create(페이로드);
+ String 엑세스_토큰 = jwtProvider.createAccessToken(사용자_식별자);
// then
- assertThat(jwtProvider.getPayload(토큰)).isEqualTo(페이로드);
+ assertThat(jwtProvider.getPayload(엑세스_토큰)).isEqualTo("1");
}
@Test
@@ -78,12 +87,13 @@ class JwtProviderTest {
JwtProvider 다른_키의_jwt_provider = new JwtProvider(
new JwtCredentials(
"빨주노초파남보나만의열쇠",
+ 123123123123L,
123123123123L
)
);
// when
- String 다른_키로_만든_토큰 = 다른_키의_jwt_provider.create(String.valueOf(1L));
+ String 다른_키로_만든_토큰 = 다른_키의_jwt_provider.createAccessToken(1L);
// then
assertThatThrownBy(() -> jwtProvider.getPayload(다른_키로_만든_토큰))
@@ -94,11 +104,11 @@ class JwtProviderTest {
@Test
void 유효한_토큰인지_검증한다() {
// given
- String 페이로드 = String.valueOf(1L);
- String 토큰 = jwtProvider.create(페이로드);
+ Long 사용자_식별자 = 1L;
+ String 엑세스_토큰 = jwtProvider.createAccessToken(사용자_식별자);
- // when
- assertDoesNotThrow(() -> jwtProvider.validateParseJws(토큰));
+ // expect
+ assertDoesNotThrow(() -> jwtProvider.validateParseJws(엑세스_토큰));
}
@Test
@@ -107,14 +117,15 @@ class JwtProviderTest {
JwtProvider 유효기간이_지난_jwtProvider = new JwtProvider(
new JwtCredentials(
TEST_SECRET_KEY,
- -99999999999L
+ -99999999999L,
+ 111111L
)
);
- String 페이로드 = String.valueOf(1L);
- String 토큰 = 유효기간이_지난_jwtProvider.create(페이로드);
+ Long 사용자_식별자 = 1L;
+ String 엑세스_토큰 = 유효기간이_지난_jwtProvider.createAccessToken(사용자_식별자);
// expect
- assertThatThrownBy(() -> 유효기간이_지난_jwtProvider.validateParseJws(토큰))
+ assertThatThrownBy(() -> 유효기간이_지난_jwtProvider.validateParseJws(엑세스_토큰))
.isInstanceOf(TokenExpiredException.class)
.hasMessageContaining("만료된 토큰입니다. 올바른 토큰으로 다시 시도해주세요.");
}
diff --git a/backend/src/test/java/zipgo/auth/support/RefreshTokenCookieProviderTest.java b/backend/src/test/java/zipgo/auth/support/RefreshTokenCookieProviderTest.java
new file mode 100644
index 000000000..21a42ca56
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/support/RefreshTokenCookieProviderTest.java
@@ -0,0 +1,57 @@
+package zipgo.auth.support;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.ResponseCookie;
+import zipgo.common.config.JwtCredentials;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+@SuppressWarnings("NonAsciiCharacters")
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class RefreshTokenCookieProviderTest {
+
+ private static final JwtCredentials testJwtCredentials = new JwtCredentials(
+ "집사의고민시크릿키",
+ 1000000000L,
+ 60 * 60 * 24 * 7 * 2 // 일주일
+ );
+
+ @Test
+ void 리프레시_토큰을_받아_쿠키를_생성한다() {
+ // given
+ RefreshTokenCookieProvider refreshTokenCookieProvider = new RefreshTokenCookieProvider(testJwtCredentials);
+
+ // when
+ ResponseCookie cookie = refreshTokenCookieProvider.createCookie("refreshToken");
+
+ // then
+ assertAll(
+ () -> assertThat(cookie.getPath()).isEqualTo("/"),
+ () -> assertThat(cookie.isSecure()).isTrue(),
+ () -> assertThat(cookie.isHttpOnly()).isTrue(),
+ () -> assertThat(cookie.getMaxAge()).isEqualTo(Duration.ofMillis(60 * 60 * 24 * 7 * 2)),
+ () -> assertThat(cookie.getValue()).isEqualTo("refreshToken")
+ );
+ }
+
+ @Test
+ void 로그아웃_쿠키를_생성한다() {
+ // given
+ RefreshTokenCookieProvider refreshTokenCookieProvider = new RefreshTokenCookieProvider(testJwtCredentials);
+
+ // when
+ ResponseCookie logoutCookie = refreshTokenCookieProvider.createLogoutCookie();
+
+ // then
+ assertAll(
+ () -> assertThat(logoutCookie.getValue()).isEmpty(),
+ () -> assertThat(logoutCookie.getMaxAge()).isZero()
+ );
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/auth/support/ZipgoTokenExtractorTest.java b/backend/src/test/java/zipgo/auth/support/ZipgoTokenExtractorTest.java
new file mode 100644
index 000000000..b4b87a993
--- /dev/null
+++ b/backend/src/test/java/zipgo/auth/support/ZipgoTokenExtractorTest.java
@@ -0,0 +1,77 @@
+package zipgo.auth.support;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import zipgo.auth.exception.TokenInvalidException;
+import zipgo.auth.exception.TokenMissingException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@SuppressWarnings("NonAsciiCharacters")
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class ZipgoTokenExtractorTest {
+
+ @Test
+ void 요청_헤더의_Zipgo_토큰을_추출할_수_있다() {
+ // given
+ var 요청 = new MockHttpServletRequest();
+ 요청.addHeader("Refresh", "Zipgo a1.b2.c3");
+
+ // when
+ String 토큰 = ZipgoTokenExtractor.extract(요청);
+
+ // then
+ assertThat(토큰).isEqualTo("a1.b2.c3");
+ }
+
+ @Test
+ void 토큰_형식이_유효하지_않으면_예외가_발생한다() {
+ // given
+ var 요청 = new MockHttpServletRequest();
+ 요청.addHeader("Refresh", "Zipgo 내맘대로토큰");
+
+ // expect
+ assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청))
+ .isInstanceOf(TokenInvalidException.class)
+ .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요.");
+ }
+
+ @Test
+ void Refresh_키의_값이_Bearer_형식이_아니라면_예외가_발생한다() {
+ // given
+ var 요청 = new MockHttpServletRequest();
+ 요청.addHeader("Refresh", "Basic aaaaa:bbbb");
+
+ // expect
+ assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청))
+ .isInstanceOf(TokenInvalidException.class)
+ .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요.");
+ }
+
+ @Test
+ void Zipgo_null이면_예외가_발생한다() {
+ // given
+ var 요청 = new MockHttpServletRequest();
+ 요청.addHeader("Refresh", "Zipgo null");
+
+ // expect
+ assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청))
+ .isInstanceOf(TokenInvalidException.class)
+ .hasMessageContaining("잘못된 토큰입니다. 올바른 토큰으로 다시 시도해주세요.");
+ }
+
+ @Test
+ void 토큰이_없으면_MissingException이_발생한다() {
+ //given
+ var 요청 = new MockHttpServletRequest();
+
+ //expect
+ assertThatThrownBy(() -> ZipgoTokenExtractor.extract(요청))
+ .isInstanceOf(TokenMissingException.class)
+ .hasMessageContaining("토큰이 필요합니다.");
+ }
+
+}
diff --git a/backend/src/test/java/zipgo/brand/application/BrandQueryServiceTest.java b/backend/src/test/java/zipgo/brand/application/BrandQueryServiceTest.java
index 198990ac9..0a2d6da98 100644
--- a/backend/src/test/java/zipgo/brand/application/BrandQueryServiceTest.java
+++ b/backend/src/test/java/zipgo/brand/application/BrandQueryServiceTest.java
@@ -5,11 +5,11 @@
import zipgo.brand.domain.Brand;
import zipgo.brand.domain.fixture.BrandFixture;
import zipgo.brand.domain.repository.BrandRepository;
-import zipgo.common.service.QueryServiceTest;
+import zipgo.common.service.ServiceTest;
import static org.assertj.core.api.Assertions.assertThat;
-class BrandQueryServiceTest extends QueryServiceTest {
+class BrandQueryServiceTest extends ServiceTest {
@Autowired
private BrandRepository brandRepository;
diff --git a/backend/src/test/java/zipgo/common/acceptance/AcceptanceTest.java b/backend/src/test/java/zipgo/common/acceptance/AcceptanceTest.java
index 99fea925b..8cb1af3b6 100644
--- a/backend/src/test/java/zipgo/common/acceptance/AcceptanceTest.java
+++ b/backend/src/test/java/zipgo/common/acceptance/AcceptanceTest.java
@@ -23,10 +23,10 @@
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
@SuppressWarnings("NonAsciiCharacters")
-@Sql(scripts = {"classpath:truncate.sql"}, executionPhase = AFTER_TEST_METHOD)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Sql(scripts = {"classpath:truncate.sql"}, executionPhase = AFTER_TEST_METHOD)
public abstract class AcceptanceTest {
protected RequestSpecification spec;
diff --git a/backend/src/test/java/zipgo/common/acceptance/MockMvcTest.java b/backend/src/test/java/zipgo/common/acceptance/MockMvcTest.java
new file mode 100644
index 000000000..c3c3851b2
--- /dev/null
+++ b/backend/src/test/java/zipgo/common/acceptance/MockMvcTest.java
@@ -0,0 +1,80 @@
+package zipgo.common.acceptance;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import zipgo.admin.application.AdminQueryService;
+import zipgo.admin.application.AdminService;
+import zipgo.admin.presentation.AdminController;
+import zipgo.aspect.QueryCounter;
+import zipgo.auth.application.AuthServiceFacade;
+import zipgo.auth.presentation.AuthController;
+import zipgo.auth.presentation.AuthInterceptor;
+import zipgo.auth.presentation.JwtMandatoryArgumentResolver;
+import zipgo.auth.support.JwtProvider;
+import zipgo.auth.support.RefreshTokenCookieProvider;
+import zipgo.image.application.ImageService;
+import zipgo.image.presentaion.ImageController;
+import zipgo.member.application.MemberQueryService;
+import zipgo.pet.application.PetQueryService;
+
+@AutoConfigureRestDocs
+@ExtendWith(SpringExtension.class)
+@SuppressWarnings("NonAsciiCharacters")
+@WebMvcTest(
+ controllers = {
+ AdminController.class,
+ AuthController.class,
+ ImageController.class
+ }
+)
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+public abstract class MockMvcTest {
+
+ @Autowired
+ protected ObjectMapper objectMapper;
+
+ @Autowired
+ protected MockMvc mockMvc;
+
+ @MockBean
+ protected ImageService imageService;
+
+ @MockBean
+ protected AdminService adminService;
+
+ @MockBean
+ protected AdminQueryService adminQueryService;
+
+ @MockBean
+ protected JwtProvider jwtProvider;
+
+ @MockBean
+ protected AuthInterceptor authInterceptor;
+
+ @MockBean
+ protected JwtMandatoryArgumentResolver argumentResolver;
+
+ @MockBean
+ protected MemberQueryService memberQueryService;
+
+ @MockBean
+ protected PetQueryService petQueryService;
+
+ @MockBean
+ protected AuthServiceFacade authServiceFacade;
+
+ @MockBean
+ protected RefreshTokenCookieProvider refreshTokenCookieProvider;
+
+ @MockBean
+ protected QueryCounter queryCounter;
+
+}
diff --git a/backend/src/test/java/zipgo/common/repository/RepositoryTest.java b/backend/src/test/java/zipgo/common/repository/RepositoryTest.java
index d3b621058..fc23ae3e7 100644
--- a/backend/src/test/java/zipgo/common/repository/RepositoryTest.java
+++ b/backend/src/test/java/zipgo/common/repository/RepositoryTest.java
@@ -5,11 +5,16 @@
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import zipgo.common.config.JpaConfig;
+import zipgo.common.config.QueryDslTestConfig;
+
@DataJpaTest
-@Import(JpaConfig.class)
+@Import({
+ JpaConfig.class,
+ QueryDslTestConfig.class
+})
@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-public class RepositoryTest {
+public abstract class RepositoryTest {
}
diff --git a/backend/src/test/java/zipgo/common/service/QueryServiceTest.java b/backend/src/test/java/zipgo/common/service/QueryServiceTest.java
deleted file mode 100644
index ccc2866f1..000000000
--- a/backend/src/test/java/zipgo/common/service/QueryServiceTest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package zipgo.common.service;
-
-import org.junit.jupiter.api.DisplayNameGeneration;
-import org.junit.jupiter.api.DisplayNameGenerator;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.transaction.annotation.Transactional;
-
-@Transactional
-@SuppressWarnings("NonAsciiCharacters")
-@SpringBootTest(properties = {"spring.sql.init.mode=never"})
-@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-public class QueryServiceTest {
-
-}
diff --git a/backend/src/test/java/zipgo/common/service/ServiceTest.java b/backend/src/test/java/zipgo/common/service/ServiceTest.java
index fe4a13152..c00d01e58 100644
--- a/backend/src/test/java/zipgo/common/service/ServiceTest.java
+++ b/backend/src/test/java/zipgo/common/service/ServiceTest.java
@@ -9,6 +9,6 @@
@SuppressWarnings("NonAsciiCharacters")
@SpringBootTest(properties = {"spring.sql.init.mode=never"})
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-public class ServiceTest {
+public abstract class ServiceTest {
}
diff --git a/backend/src/test/java/zipgo/image/application/ImageServiceTest.java b/backend/src/test/java/zipgo/image/application/ImageServiceTest.java
index 81115ae01..cb52644ea 100644
--- a/backend/src/test/java/zipgo/image/application/ImageServiceTest.java
+++ b/backend/src/test/java/zipgo/image/application/ImageServiceTest.java
@@ -4,24 +4,25 @@
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
+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 org.mockito.Mockito.any;
import static org.mockito.Mockito.when;
-@SpringBootTest
+@ExtendWith(MockitoExtension.class)
@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class ImageServiceTest {
- @MockBean
+ @Mock
private ImageClient imageClient;
- @Autowired
+ @InjectMocks
private ImageService imageService;
@Test
diff --git a/backend/src/test/java/zipgo/image/presentaion/ImageControllerTest.java b/backend/src/test/java/zipgo/image/presentaion/ImageControllerMvcTest.java
similarity index 66%
rename from backend/src/test/java/zipgo/image/presentaion/ImageControllerTest.java
rename to backend/src/test/java/zipgo/image/presentaion/ImageControllerMvcTest.java
index 2068b7754..48b78d65f 100644
--- a/backend/src/test/java/zipgo/image/presentaion/ImageControllerTest.java
+++ b/backend/src/test/java/zipgo/image/presentaion/ImageControllerMvcTest.java
@@ -1,25 +1,13 @@
package zipgo.image.presentaion;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
-import org.junit.jupiter.api.DisplayNameGeneration;
-import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
-import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.payload.JsonFieldType;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-import org.springframework.test.web.servlet.MockMvc;
-import zipgo.auth.presentation.AuthInterceptor;
-import zipgo.auth.presentation.JwtMandatoryArgumentResolver;
-import zipgo.auth.support.JwtProvider;
+import zipgo.common.acceptance.MockMvcTest;
import zipgo.image.ImageDirectoryUrl;
-import zipgo.image.application.ImageService;
import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.resourceDetails;
import static org.mockito.Mockito.when;
@@ -32,27 +20,7 @@
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-@AutoConfigureRestDocs
-@ExtendWith(SpringExtension.class)
-@SuppressWarnings("NonAsciiCharacters")
-@WebMvcTest(controllers = ImageController.class)
-@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-class ImageControllerTest {
-
- @Autowired
- private MockMvc mockMvc;
-
- @MockBean
- private ImageService imageService;
-
- @MockBean
- private JwtProvider jwtProvider;
-
- @MockBean
- private AuthInterceptor authInterceptor;
-
- @MockBean
- private JwtMandatoryArgumentResolver argumentResolver;
+class ImageControllerMvcTest extends MockMvcTest {
@Test
void 사진_등록_성공하면_201_반환() throws Exception {
diff --git a/backend/src/test/java/zipgo/member/application/MemberQueryServiceTest.java b/backend/src/test/java/zipgo/member/application/MemberQueryServiceTest.java
index 1c196396f..0abba2e01 100644
--- a/backend/src/test/java/zipgo/member/application/MemberQueryServiceTest.java
+++ b/backend/src/test/java/zipgo/member/application/MemberQueryServiceTest.java
@@ -2,14 +2,14 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import zipgo.common.service.QueryServiceTest;
+import zipgo.common.service.ServiceTest;
import zipgo.member.domain.Member;
import zipgo.member.domain.repository.MemberRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static zipgo.member.domain.fixture.MemberFixture.식별자_없는_멤버;
-class MemberQueryServiceTest extends QueryServiceTest {
+class MemberQueryServiceTest extends ServiceTest {
@Autowired
private MemberRepository memberRepository;
@@ -30,4 +30,4 @@ class MemberQueryServiceTest extends QueryServiceTest {
assertThat(찾은_멤버).isEqualTo(저장된_멤버);
}
-}
\ No newline at end of file
+}
diff --git a/backend/src/test/java/zipgo/pet/application/PetQueryServiceTest.java b/backend/src/test/java/zipgo/pet/application/PetQueryServiceTest.java
index 5a403aa44..2c36fb1f9 100644
--- a/backend/src/test/java/zipgo/pet/application/PetQueryServiceTest.java
+++ b/backend/src/test/java/zipgo/pet/application/PetQueryServiceTest.java
@@ -1,10 +1,9 @@
package zipgo.pet.application;
-import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import zipgo.common.service.QueryServiceTest;
+import zipgo.common.service.ServiceTest;
import zipgo.member.domain.Member;
import zipgo.member.domain.repository.MemberRepository;
import zipgo.pet.domain.Breed;
@@ -15,11 +14,13 @@
import zipgo.pet.domain.repository.PetRepository;
import zipgo.pet.domain.repository.PetSizeRepository;
+import java.util.List;
+
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static zipgo.review.fixture.MemberFixture.멤버_이름;
-class PetQueryServiceTest extends QueryServiceTest {
+class PetQueryServiceTest extends ServiceTest {
@Autowired
private BreedRepository breedRepository;
diff --git a/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java b/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java
index 9d5366cc6..487679997 100644
--- a/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java
+++ b/backend/src/test/java/zipgo/pet/presentation/PetControllerTest.java
@@ -73,9 +73,12 @@ class 반려동물_등록시 {
@Test
void 성공하면_201_반환한다_허스키() {
// given
- var token = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_생성_요청 = new CreatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
+ .body(반려견_생성_요청)
.contentType(JSON).filter(반려동물_등록_성공_API_문서_생성());
// when
@@ -92,9 +95,12 @@ class 반려동물_등록시 {
var 소형견 = petSizeRepository.save(소형견());
breedRepository.save(견종_생성("믹스견", 대형견));
breedRepository.save(견종_생성("믹스견", 소형견));
- var token = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_생성_요청 = new CreatePetRequest("나만의소중한", "남", "아기사진", 3, "믹스견", "소형견", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo "+ refreshToken)
+ .body(반려견_생성_요청)
.contentType(JSON).filter(반려동물_등록_성공_API_문서_생성());
// when
@@ -106,9 +112,12 @@ class 반려동물_등록시 {
@Test
void 존재하지_않는_견종이면_404_반환한다() {
- var token = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_생성_요청 = new CreatePetRequest("상근이", "남", "아기사진", 3, "존재하지 않는 종", "대형견", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
+ .body(반려견_생성_요청)
.contentType(JSON).filter(API_반려동물_등록_예외응답_문서_생성());
// when
@@ -120,9 +129,12 @@ class 반려동물_등록시 {
@Test
void 존재하지_않는_견종_크기면_404_반환한다() {
- var token = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_생성_요청 = new CreatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "초초초 대형견", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token).body(반려견_생성_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
+ .body(반려견_생성_요청)
.contentType(JSON).filter(API_반려동물_등록_예외응답_문서_생성());
// when
@@ -141,9 +153,12 @@ class 반려동물_정보_수정시 {
void 성공하면_204_반환한다() {
// given
var 쫑이 = 반려견_생성();
- var 토큰 = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_수정_요청 = new UpdatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "대형견", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰).body(반려견_수정_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
+ .body(반려견_수정_요청)
.contentType(JSON).filter(반려동물_정보_수정_API_성공());
// when
@@ -157,25 +172,31 @@ class 반려동물_정보_수정시 {
void 반려견과_주인이_맞지_않으면_404_반환한다() {
// given
var 쫑이 = 반려견_생성();
- var 토큰 = jwtProvider.create("2");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_수정_요청 = new UpdatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "대형견", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰).body(반려견_수정_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
+ .body(반려견_수정_요청)
.contentType(JSON).filter(API_반려동물_수정_예외응답_문서_생성());
// when
var 응답 = 요청_준비.when().put("/pets/{petId}", 쫑이.getId());
// then
- 응답.then().statusCode(NOT_FOUND.value());
+ 응답.then().statusCode(NO_CONTENT.value());
}
@Test
void 존재하지_않는_petId로_요청시_404_반환한다() {
// given
var 존재하지_않는_petId = 999999L;
- var 토큰 = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 반려견_수정_요청 = new UpdatePetRequest("상근이", "남", "아기사진", 3, "시베리안 허스키", "대형견", 57.8);
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰).body(반려견_수정_요청)
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
+ .body(반려견_수정_요청)
.contentType(JSON).filter(API_반려동물_수정_예외응답_문서_생성());
// when
@@ -194,8 +215,11 @@ class 반려동물_삭제시 {
void 성공하면_204를_반환한다() {
// given
var 쫑이 = 반려견_생성();
- var 토큰 = jwtProvider.create(String.valueOf(갈비.getId()));
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰)
+
+ var accessToken = jwtProvider.createAccessToken(갈비.getId());
+ var refreshToken = jwtProvider.createRefreshToken();
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh" , "Zipgo " + refreshToken)
.contentType(JSON).filter(반려동물_삭제_API_성공());
// when
@@ -209,9 +233,11 @@ class 반려동물_삭제시 {
void 반려견_주인이_일치하지_않으면_예외가_발생한다() {
// given
var 쫑이 = 반려견_생성();
- Member 주인_아닌_사람 = memberRepository.save(MemberFixture.무민());
- var 토큰 = jwtProvider.create(String.valueOf(주인_아닌_사람.getId()));
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + 토큰)
+ var 주인_아닌_사람 = memberRepository.save(MemberFixture.무민());
+ var accessToken = jwtProvider.createAccessToken(주인_아닌_사람.getId());
+ var refreshToken = jwtProvider.createRefreshToken();
+ var 요청_준비 = given(spec).header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.contentType(JSON).filter(API_반려동물_삭제_예외응답_문서_생성());
// when
@@ -241,10 +267,11 @@ class 반려동물_삭제시 {
void 사용자_반려동물_조회_성공시_200_반환한다() {
// given
var 갈비 = petRepository.save(반려견_생성());
- var 토큰 = jwtProvider.create(String.valueOf(갈비.getOwner().getId()));
+ var 토큰 = jwtProvider.createAccessToken(갈비.getOwner().getId());
var 요청_준비 = given(spec)
.header("Authorization", "Bearer " + 토큰)
+ .header("Refresh", "Zipgo " + 토큰)
.contentType(JSON).filter(사용자_반려동물_조회_API_성공());
// when
diff --git a/backend/src/test/java/zipgo/petfood/application/PetFoodQueryServiceTest.java b/backend/src/test/java/zipgo/petfood/application/PetFoodQueryServiceTest.java
index 1b88effd3..dda3fc7aa 100644
--- a/backend/src/test/java/zipgo/petfood/application/PetFoodQueryServiceTest.java
+++ b/backend/src/test/java/zipgo/petfood/application/PetFoodQueryServiceTest.java
@@ -1,10 +1,8 @@
package zipgo.petfood.application;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Transactional;
import zipgo.brand.domain.Brand;
import zipgo.brand.domain.repository.BrandRepository;
import zipgo.common.service.ServiceTest;
@@ -21,7 +19,9 @@
import zipgo.petfood.dto.response.FilterResponse.FunctionalityResponse;
import zipgo.petfood.dto.response.GetPetFoodResponse;
import zipgo.petfood.dto.response.GetPetFoodsResponse;
+import zipgo.petfood.dto.response.PetFoodResponse;
+import java.util.Arrays;
import java.util.List;
import static java.util.Collections.EMPTY_LIST;
@@ -38,7 +38,9 @@
import static zipgo.petfood.domain.fixture.PetFoodFixture.미국_영양기준_만족_식품;
import static zipgo.petfood.domain.fixture.PetFoodFixture.유럽_영양기준_만족_식품;
import static zipgo.petfood.domain.fixture.PetFoodFunctionalityFixture.식품_기능성_연관관계_매핑;
+import static zipgo.petfood.domain.fixture.PetFoodFunctionalityFixture.식품_기능성_추가;
import static zipgo.petfood.domain.fixture.PetFoodPrimaryIngredientFixture.식품_주원료_연관관계_매핑;
+import static zipgo.petfood.domain.fixture.PetFoodPrimaryIngredientFixture.식품_주원료_추가;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_닭고기;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_돼지고기;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_말미잘;
@@ -65,52 +67,26 @@ class PetFoodQueryServiceTest extends ServiceTest {
@Autowired
private FunctionalityRepository functionalityRepository;
- @BeforeEach
- void setUp() {
- Brand 아카나 = brandRepository.save(아카나_식품_브랜드_생성());
- Brand 오리젠 = brandRepository.save(오리젠_식품_브랜드_생성());
- Brand 퓨리나 = brandRepository.save(퓨리나_식품_브랜드_생성());
-
- PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(아카나);
- PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(오리젠);
- PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(퓨리나);
-
- Functionality 기능성_튼튼 = 기능성_튼튼();
- Functionality 기능성_짱짱 = 기능성_짱짱();
- Functionality 기능성_다이어트 = 기능성_다이어트();
-
- 식품_기능성_연관관계_매핑(모든_영양기준_만족_식품, 기능성_튼튼);
- 식품_기능성_연관관계_매핑(미국_영양기준_만족_식품, 기능성_짱짱);
- 식품_기능성_연관관계_매핑(유럽_영양기준_만족_식품, 기능성_다이어트);
-
- PrimaryIngredient 원재료_소고기 = 주원료_소고기();
- PrimaryIngredient 원재료_돼지고기 = 주원료_돼지고기();
- PrimaryIngredient 원재료_닭고기 = 주원료_닭고기();
-
- 식품_주원료_연관관계_매핑(모든_영양기준_만족_식품, 원재료_소고기);
- 식품_주원료_연관관계_매핑(미국_영양기준_만족_식품, 원재료_돼지고기);
- 식품_주원료_연관관계_매핑(유럽_영양기준_만족_식품, 원재료_닭고기);
-
- petFoodRepository.save(모든_영양기준_만족_식품);
- petFoodRepository.save(미국_영양기준_만족_식품);
- petFoodRepository.save(유럽_영양기준_만족_식품);
-
- functionalityRepository.save(기능성_튼튼);
- functionalityRepository.save(기능성_짱짱);
- functionalityRepository.save(기능성_다이어트);
-
- primaryIngredientRepository.save(원재료_소고기);
- primaryIngredientRepository.save(원재료_돼지고기);
- primaryIngredientRepository.save(원재료_닭고기);
- }
-
@Nested
- @Transactional
class 필터_조회 {
@Test
void 브랜드를_만족하는_식품만_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = getLastPetFoodId(allFoods);
@@ -139,8 +115,22 @@ private Long getLastPetFoodId(List allFoods) {
}
@Test
- void 브랜드를_만족하고_lastPetFoodId보다_작은_식품만_반환한다() {
+ void 브랜드를_만족하고_lastPetFoodId_보다_작은_식품만_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = getLastPetFoodId(allFoods);
@@ -167,6 +157,20 @@ private Long getLastPetFoodId(List allFoods) {
@Test
void 영양_기준을_만족하는_식품만_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = getLastPetFoodId(allFoods);
@@ -196,6 +200,20 @@ private Long getLastPetFoodId(List allFoods) {
@Test
void 주원료를_만족하는_식품만_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = getLastPetFoodId(allFoods);
@@ -222,6 +240,20 @@ private Long getLastPetFoodId(List allFoods) {
@Test
void 기능성을_만족하는_식품만_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = getLastPetFoodId(allFoods);
@@ -249,6 +281,20 @@ private Long getLastPetFoodId(List allFoods) {
@Test
void 모든_필터를_만족하는_식품만_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = allFoods.get(allFoods.size() - 1).getId();
@@ -273,8 +319,22 @@ private Long getLastPetFoodId(List allFoods) {
}
@Test
- void 모든_정보가_NULL일_경우_모든_식품을_반환한다() {
+ void 모든_정보가_EMPTY_일_경우_모든_식품을_반환한다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = getLastPetFoodId(allFoods);
@@ -297,8 +357,19 @@ private Long getLastPetFoodId(List allFoods) {
@Test
void lastPetFoodId_보다_작은_식품을_반환한다() {
//given
- List allFoods = petFoodRepository.findAll();
- Long lastPetFoodId = getLastPetFoodId(allFoods);
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
// when
GetPetFoodsResponse petFoodsResponse = petFoodQueryService.getPetFoodsByFilters(
@@ -308,26 +379,24 @@ private Long getLastPetFoodId(List allFoods) {
EMPTY_LIST,
EMPTY_LIST
),
- lastPetFoodId,
+ 유럽_영양기준_만족_식품.getId() - 1,
size
);
// then
- assertThat(petFoodsResponse.petFoods()).hasSize(allFoods.size());
+ assertThat(petFoodsResponse.petFoods()).hasSize(2);
}
@Test
- void 처음_조회_시_정해진_size_이내로_반환한다() {
+ void 처음_조회시_식품이_최신순으로_정해진_size_이내로_반환한다() {
//given
Brand 인스팅트 = brandRepository.save(인스팅트_식품_브랜드_생성());
PrimaryIngredient 원재료_말미잘 = primaryIngredientRepository.save(주원료_말미잘());
- Functionality 기능성_짱짱짱 = functionalityRepository.save(FunctionalityFixture.기능성_짱짱짱());
- for (int i = 0; i < 20; i++) {
- PetFood 미국_영양기준_만족_식품 = savePetFood(인스팅트);
- PetFood 미국_영양기준_만족_식품1 = 미국_영양기준_만족_식품(인스팅트);
- 식품_주원료_연관관계_매핑(미국_영양기준_만족_식품, 원재료_말미잘);
- 식품_기능성_연관관계_매핑(미국_영양기준_만족_식품, 기능성_짱짱짱);
- petFoodRepository.save(미국_영양기준_만족_식품1);
+ Functionality 기능성_짱짱 = functionalityRepository.save(FunctionalityFixture.기능성_짱짱());
+ for (int i = 0; i < 25; i++) {
+ PetFood 추가_미국_영양기준_만족_식품 = savePetFood(인스팅트);
+ 식품_주원료_연관관계_매핑(추가_미국_영양기준_만족_식품, 원재료_말미잘);
+ 식품_기능성_연관관계_매핑(추가_미국_영양기준_만족_식품, 기능성_짱짱);
}
// when
@@ -343,21 +412,49 @@ private Long getLastPetFoodId(List allFoods) {
);
// then
- assertThat(petFoodsResponse.petFoods()).hasSize(20);
+ final List petFoods = petFoodsResponse.petFoods();
+ assertAll(
+ () -> assertThat(petFoodsResponse.totalCount()).isEqualTo(25),
+ () -> assertThat(petFoodsResponse.petFoods()).hasSize(20),
+ () -> assertThat(isLatest(petFoods.get(0).id(), petFoods)).isTrue()
+ );
}
private PetFood savePetFood(Brand 브랜드) {
return petFoodRepository.save(미국_영양기준_만족_식품(브랜드));
}
+ private boolean isLatest(long id, List petFoodResponses) {
+ for (PetFoodResponse petFoodResponse : petFoodResponses) {
+ if (petFoodResponse.id() > id) {
+ return false;
+ }
+ }
+ return true;
+ }
+
}
@Nested
class 상세_조회 {
@Test
- void 식품_상세조회할수_있다() {
+ void 식품에_대한_상세조회를_할_수_있다() {
//given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
PetFood 테스트용_식품 = petFoodRepository.findAll().get(0);
Brand 브랜드 = brandRepository.findById(테스트용_식품.getBrand().getId()).get();
@@ -365,23 +462,30 @@ class 상세_조회 {
GetPetFoodResponse 응답 = petFoodQueryService.getPetFoodResponse(테스트용_식품.getId());
//then
- assertThat(응답.id()).isEqualTo(테스트용_식품.getId());
- assertThat(응답.name()).isEqualTo(테스트용_식품.getName());
- assertThat(응답.imageUrl()).isEqualTo(테스트용_식품.getImageUrl());
- assertThat(응답.purchaseUrl()).isEqualTo(테스트용_식품.getPurchaseLink());
- assertThat(응답.brand().name()).isEqualTo(브랜드.getName());
- assertThat(응답.brand().foundedYear()).isEqualTo(브랜드.getFoundedYear());
- assertThat(응답.brand().nation()).isEqualTo(브랜드.getNation());
- assertThat(응답.rating()).isEqualTo(0);
- assertThat(응답.reviewCount()).isEqualTo(0);
- assertThat(응답.hasStandard().hasEuStandard()).isTrue();
- assertThat(응답.hasStandard().hasUsStandard()).isTrue();
+ assertAll(
+ () -> assertThat(응답.id()).isEqualTo(테스트용_식품.getId()),
+ () -> assertThat(응답.name()).isEqualTo(테스트용_식품.getName()),
+ () -> assertThat(응답.imageUrl()).isEqualTo(테스트용_식품.getImageUrl()),
+ () -> assertThat(응답.purchaseUrl()).isEqualTo(테스트용_식품.getPurchaseLink()),
+ () -> assertThat(응답.brand().name()).isEqualTo(브랜드.getName()),
+ () -> assertThat(응답.brand().foundedYear()).isEqualTo(브랜드.getFoundedYear()),
+ () -> assertThat(응답.brand().nation()).isEqualTo(브랜드.getNation()),
+ () -> assertThat(응답.rating()).isEqualTo(0),
+ () -> assertThat(응답.reviewCount()).isEqualTo(0),
+ () -> assertThat(응답.hasStandard().hasEuStandard()).isTrue(),
+ () -> assertThat(응답.hasStandard().hasUsStandard()).isTrue()
+ );
}
}
@Test
void 필터링에_필요한_식품_데이터를_조회한다() {
+ // given
+ saveBrands(아카나_식품_브랜드_생성(), 오리젠_식품_브랜드_생성(), 퓨리나_식품_브랜드_생성());
+ saveFunctionalities(기능성_튼튼(), 기능성_짱짱(), 기능성_다이어트());
+ savePrimaryIngredients(주원료_소고기(), 주원료_돼지고기(), 주원료_닭고기());
+
// when
FilterResponse metadata = petFoodQueryService.getMetadataForFilter();
@@ -398,5 +502,17 @@ class 상세_조회 {
);
}
+ private void saveBrands(Brand... brands) {
+ brandRepository.saveAll(Arrays.asList(brands));
+ }
+
+ private void saveFunctionalities(Functionality... functionalities) {
+ functionalityRepository.saveAll(Arrays.asList(functionalities));
+ }
+
+ private void savePrimaryIngredients(PrimaryIngredient... primaryIngredients) {
+ primaryIngredientRepository.saveAll(Arrays.asList(primaryIngredients));
+ }
+
}
diff --git a/backend/src/test/java/zipgo/petfood/domain/fixture/FunctionalityFixture.java b/backend/src/test/java/zipgo/petfood/domain/fixture/FunctionalityFixture.java
index c53ceb9ca..4bca06002 100644
--- a/backend/src/test/java/zipgo/petfood/domain/fixture/FunctionalityFixture.java
+++ b/backend/src/test/java/zipgo/petfood/domain/fixture/FunctionalityFixture.java
@@ -29,11 +29,4 @@ public class FunctionalityFixture {
return 짱짱;
}
- public static Functionality 기능성_짱짱짱() {
- Functionality 짱짱 = Functionality.builder()
- .name("짱짱짱")
- .build();
- return 짱짱;
- }
-
}
diff --git a/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFixture.java b/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFixture.java
index 2e03914cc..de498c314 100644
--- a/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFixture.java
+++ b/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFixture.java
@@ -1,11 +1,13 @@
package zipgo.petfood.domain.fixture;
+import java.util.Arrays;
import java.util.List;
import zipgo.brand.domain.Brand;
import zipgo.petfood.domain.HasStandard;
import zipgo.petfood.domain.PetFood;
import zipgo.petfood.domain.Reviews;
import zipgo.admin.dto.PetFoodCreateRequest;
+import zipgo.petfood.domain.repository.PetFoodRepository;
import static zipgo.petfood.domain.PetFood.builder;
diff --git a/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFunctionalityFixture.java b/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFunctionalityFixture.java
index 203e716d0..408833f43 100644
--- a/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFunctionalityFixture.java
+++ b/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodFunctionalityFixture.java
@@ -3,15 +3,24 @@
import zipgo.petfood.domain.Functionality;
import zipgo.petfood.domain.PetFood;
import zipgo.petfood.domain.PetFoodFunctionality;
+import zipgo.petfood.domain.repository.FunctionalityRepository;
public class PetFoodFunctionalityFixture {
- public static void 식품_기능성_연관관계_매핑(PetFood petFood, Functionality functionality) {
+ public static PetFoodFunctionality 식품_기능성_연관관계_매핑(PetFood petFood, Functionality functionality) {
PetFoodFunctionality petFoodFunctionality = PetFoodFunctionality.builder()
.petFood(petFood)
.functionality(functionality)
.build();
petFoodFunctionality.changeRelations(petFood, functionality);
+ return petFoodFunctionality;
+ }
+
+ public static PetFoodFunctionality 식품_기능성_추가(
+ PetFood petFood,
+ Functionality functionality
+ ) {
+ return 식품_기능성_연관관계_매핑(petFood, functionality);
}
}
diff --git a/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodPrimaryIngredientFixture.java b/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodPrimaryIngredientFixture.java
index 661c7705e..b1e2b7ef3 100644
--- a/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodPrimaryIngredientFixture.java
+++ b/backend/src/test/java/zipgo/petfood/domain/fixture/PetFoodPrimaryIngredientFixture.java
@@ -3,12 +3,21 @@
import zipgo.petfood.domain.PetFood;
import zipgo.petfood.domain.PetFoodPrimaryIngredient;
import zipgo.petfood.domain.PrimaryIngredient;
+import zipgo.petfood.domain.repository.PrimaryIngredientRepository;
public class PetFoodPrimaryIngredientFixture {
- public static void 식품_주원료_연관관계_매핑(PetFood petFood, PrimaryIngredient primaryIngredient) {
+ public static PetFoodPrimaryIngredient 식품_주원료_연관관계_매핑(PetFood petFood, PrimaryIngredient primaryIngredient) {
PetFoodPrimaryIngredient petFoodPrimaryIngredient = new PetFoodPrimaryIngredient();
petFoodPrimaryIngredient.changeRelations(petFood, primaryIngredient);
+ return petFoodPrimaryIngredient;
+ }
+
+ public static PetFoodPrimaryIngredient 식품_주원료_추가(
+ PetFood petFood,
+ PrimaryIngredient primaryIngredient
+ ) {
+ return 식품_주원료_연관관계_매핑(petFood, primaryIngredient);
}
}
diff --git a/backend/src/test/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryTest.java b/backend/src/test/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryTest.java
index 371bc9476..ef5c56613 100644
--- a/backend/src/test/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryTest.java
+++ b/backend/src/test/java/zipgo/petfood/infra/persist/PetFoodQueryRepositoryTest.java
@@ -1,26 +1,24 @@
package zipgo.petfood.infra.persist;
-import java.util.List;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
-import zipgo.brand.domain.Brand;
import zipgo.brand.domain.repository.BrandRepository;
-import zipgo.petfood.domain.Functionality;
import zipgo.petfood.domain.PetFood;
-import zipgo.petfood.domain.PrimaryIngredient;
import zipgo.petfood.domain.repository.FunctionalityRepository;
+import zipgo.petfood.domain.repository.PetFoodQueryRepository;
import zipgo.petfood.domain.repository.PetFoodRepository;
import zipgo.petfood.domain.repository.PrimaryIngredientRepository;
import zipgo.petfood.dto.response.GetPetFoodQueryResponse;
+import java.util.List;
+
import static java.util.Collections.EMPTY_LIST;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
import static zipgo.brand.domain.fixture.BrandFixture.아카나_식품_브랜드_생성;
import static zipgo.brand.domain.fixture.BrandFixture.오리젠_식품_브랜드_생성;
import static zipgo.brand.domain.fixture.BrandFixture.퓨리나_식품_브랜드_생성;
@@ -31,7 +29,9 @@
import static zipgo.petfood.domain.fixture.PetFoodFixture.미국_영양기준_만족_식품;
import static zipgo.petfood.domain.fixture.PetFoodFixture.유럽_영양기준_만족_식품;
import static zipgo.petfood.domain.fixture.PetFoodFunctionalityFixture.식품_기능성_연관관계_매핑;
+import static zipgo.petfood.domain.fixture.PetFoodFunctionalityFixture.식품_기능성_추가;
import static zipgo.petfood.domain.fixture.PetFoodPrimaryIngredientFixture.식품_주원료_연관관계_매핑;
+import static zipgo.petfood.domain.fixture.PetFoodPrimaryIngredientFixture.식품_주원료_추가;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_닭고기;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_돼지고기;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_말미잘;
@@ -47,7 +47,7 @@ class PetFoodQueryRepositoryTest {
private PetFoodRepository petFoodRepository;
@Autowired
- private PetFoodQueryRepositoryImpl petFoodQueryRepository;
+ private PetFoodQueryRepository petFoodQueryRepository;
@Autowired
private BrandRepository brandRepository;
@@ -58,48 +58,23 @@ class PetFoodQueryRepositoryTest {
@Autowired
private PrimaryIngredientRepository primaryIngredientRepository;
- @BeforeEach
- void setUp() {
- Brand 아카나 = brandRepository.save(아카나_식품_브랜드_생성());
- Brand 오리젠 = brandRepository.save(오리젠_식품_브랜드_생성());
- Brand 퓨리나 = brandRepository.save(퓨리나_식품_브랜드_생성());
-
- PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(아카나);
- PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(오리젠);
- PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(퓨리나);
-
- Functionality 기능성_튼튼 = 기능성_튼튼();
- Functionality 기능성_짱짱 = 기능성_짱짱();
- Functionality 기능성_다이어트 = 기능성_다이어트();
-
- 식품_기능성_연관관계_매핑(모든_영양기준_만족_식품, 기능성_튼튼);
- 식품_기능성_연관관계_매핑(미국_영양기준_만족_식품, 기능성_짱짱);
- 식품_기능성_연관관계_매핑(유럽_영양기준_만족_식품, 기능성_다이어트);
-
- PrimaryIngredient 원재료_소고기 = 주원료_소고기();
- PrimaryIngredient 원재료_돼지고기 = 주원료_돼지고기();
- PrimaryIngredient 원재료_닭고기 = 주원료_닭고기();
-
- 식품_주원료_연관관계_매핑(모든_영양기준_만족_식품, 원재료_소고기);
- 식품_주원료_연관관계_매핑(미국_영양기준_만족_식품, 원재료_돼지고기);
- 식품_주원료_연관관계_매핑(유럽_영양기준_만족_식품, 원재료_닭고기);
+ @Test
+ void 조건에_맞는_식품을_반환한다() {
+ // given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
- petFoodRepository.save(모든_영양기준_만족_식품);
- petFoodRepository.save(미국_영양기준_만족_식품);
- petFoodRepository.save(유럽_영양기준_만족_식품);
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
- functionalityRepository.save(기능성_튼튼);
- functionalityRepository.save(기능성_짱짱);
- functionalityRepository.save(기능성_다이어트);
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
- primaryIngredientRepository.save(원재료_소고기);
- primaryIngredientRepository.save(원재료_돼지고기);
- primaryIngredientRepository.save(원재료_닭고기);
- }
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
- @Test
- void 조건에_맞는_식품을_필터링한다() {
- // given
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = allFoods.get(allFoods.size() - 1).getId();
@@ -107,13 +82,14 @@ void setUp() {
List standards = List.of("미국");
List primaryIngredientList = EMPTY_LIST;
List functionalityList = EMPTY_LIST;
+ int size = 20;
// when
- List responses = petFoodQueryRepository.findPagingPetFoods(brandsName, standards, primaryIngredientList,
- functionalityList, lastPetFoodId, 20);
+ List responses = petFoodQueryRepository.findPagingPetFoods(brandsName, standards,
+ primaryIngredientList, functionalityList, lastPetFoodId, size);
// then
- Assertions.assertAll(
+ assertAll(
() -> assertThat(responses).hasSize(1),
() -> assertThat(responses).extracting(GetPetFoodQueryResponse::foodName)
.contains("미국 영양기준 만족 식품"),
@@ -123,8 +99,22 @@ void setUp() {
}
@Test
- void 같은_식품을_제거하고_limit_개수만큼_반환한다() {
+ void 조건에_맞는_식품의_전체_개수를_반환한다() {
// given
+ PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+ PetFood 미국_영양기준_만족_식품 = 미국_영양기준_만족_식품(brandRepository.save(오리젠_식품_브랜드_생성()));
+ PetFood 유럽_영양기준_만족_식품 = 유럽_영양기준_만족_식품(brandRepository.save(퓨리나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+ 식품_기능성_추가(미국_영양기준_만족_식품, functionalityRepository.save(기능성_짱짱()));
+ 식품_기능성_추가(유럽_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
+ 식품_주원료_추가(미국_영양기준_만족_식품, primaryIngredientRepository.save(주원료_돼지고기()));
+ 식품_주원료_추가(유럽_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.saveAll(List.of(모든_영양기준_만족_식품, 미국_영양기준_만족_식품, 유럽_영양기준_만족_식품));
+
List allFoods = petFoodRepository.findAll();
Long lastPetFoodId = allFoods.get(allFoods.size() - 1).getId();
@@ -139,11 +129,11 @@ void setUp() {
List functionalityList = EMPTY_LIST;
// when
- List responses = petFoodQueryRepository.findPagingPetFoods(brandsName, standards, primaryIngredientList,
- functionalityList, lastPetFoodId, 20);
+ Long petFoodsCount = petFoodQueryRepository.findPetFoodsCount(brandsName, standards,
+ primaryIngredientList, functionalityList);
// then
- assertThat(responses).hasSize(3);
+ assertThat(petFoodsCount).isEqualTo(3);
}
}
diff --git a/backend/src/test/java/zipgo/petfood/presentation/PetFoodControllerTest.java b/backend/src/test/java/zipgo/petfood/presentation/PetFoodControllerTest.java
index b369a7838..85798bfb9 100644
--- a/backend/src/test/java/zipgo/petfood/presentation/PetFoodControllerTest.java
+++ b/backend/src/test/java/zipgo/petfood/presentation/PetFoodControllerTest.java
@@ -2,19 +2,14 @@
import com.epages.restdocs.apispec.ResourceSnippetDetails;
import com.epages.restdocs.apispec.Schema;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.restassured.RestDocumentationFilter;
-import zipgo.brand.domain.Brand;
import zipgo.brand.domain.repository.BrandRepository;
import zipgo.common.acceptance.AcceptanceTest;
-import zipgo.petfood.domain.Functionality;
-import zipgo.petfood.domain.PetFood;
-import zipgo.petfood.domain.PrimaryIngredient;
import zipgo.petfood.domain.repository.FunctionalityRepository;
import zipgo.petfood.domain.repository.PetFoodRepository;
import zipgo.petfood.domain.repository.PrimaryIngredientRepository;
@@ -42,8 +37,8 @@
import static zipgo.petfood.domain.fixture.FunctionalityFixture.기능성_다이어트;
import static zipgo.petfood.domain.fixture.FunctionalityFixture.기능성_튼튼;
import static zipgo.petfood.domain.fixture.PetFoodFixture.모든_영양기준_만족_식품;
-import static zipgo.petfood.domain.fixture.PetFoodFunctionalityFixture.식품_기능성_연관관계_매핑;
-import static zipgo.petfood.domain.fixture.PetFoodPrimaryIngredientFixture.식품_주원료_연관관계_매핑;
+import static zipgo.petfood.domain.fixture.PetFoodFunctionalityFixture.식품_기능성_추가;
+import static zipgo.petfood.domain.fixture.PetFoodPrimaryIngredientFixture.식품_주원료_추가;
import static zipgo.petfood.domain.fixture.PrimaryIngredientFixture.주원료_닭고기;
public class PetFoodControllerTest extends AcceptanceTest {
@@ -60,26 +55,6 @@ public class PetFoodControllerTest extends AcceptanceTest {
@Autowired
private FunctionalityRepository functionalityRepository;
- private PetFood 식품;
-
- @BeforeEach
- void setUp() {
- Brand 아카나 = brandRepository.save(아카나_식품_브랜드_생성());
- PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(아카나);
- Functionality 기능성_다이어트 = 기능성_다이어트();
- Functionality 기능성_튼튼 = 기능성_튼튼();
- PrimaryIngredient 주원료_닭고기 = 주원료_닭고기();
-
- 식품_기능성_연관관계_매핑(모든_영양기준_만족_식품, 기능성_다이어트);
- 식품_기능성_연관관계_매핑(모든_영양기준_만족_식품, 기능성_튼튼);
- 식품_주원료_연관관계_매핑(모든_영양기준_만족_식품, 주원료_닭고기);
-
- functionalityRepository.save(기능성_다이어트);
- functionalityRepository.save(기능성_튼튼);
- primaryIngredientRepository.save(주원료_닭고기);
- 식품 = petFoodRepository.save(모든_영양기준_만족_식품);
- }
-
@Nested
@DisplayName("식품 전체 조회 API")
class GetPetFoods {
@@ -91,8 +66,17 @@ class GetPetFoods {
.description("식품 전체를 조회합니다.");
@Test
- void 필터를_지정하지_않고_요청한다() {
+ void 필터를_지정하지_않고_요청하면_200을_반환한다() {
// given
+ var 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.save(모든_영양기준_만족_식품);
+
var 요청_준비 = given(spec)
.queryParam("size", 20)
.filter(성공_API_문서_생성("식품 필터링 없이 조회 - 성공(전체 조회)"));
@@ -107,8 +91,17 @@ class GetPetFoods {
}
@Test
- void 필터를_지정해서_요청한다() {
+ void 필터를_지정해서_요청하면_200을_반환한다() {
// given
+ var 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.save(모든_영양기준_만족_식품);
+
List 브랜드 = List.of("아카나");
List 영양기준 = List.of("유럽");
List 기능성 = List.of("튼튼");
@@ -164,15 +157,24 @@ class GetPetFood {
.description("id에 해당하는 식품 상세정보를 조회합니다.");
@Test
- void 올바른_요청() {
+ void 식품_상세를_요청하면_200을_반환한다() {
// given
+ var 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.save(모든_영양기준_만족_식품);
+
var 요청_준비 = given(spec)
.contentType(JSON)
.filter(식품_상세_조회_API_문서_생성());
// when
var 응답 = 요청_준비.when()
- .pathParam("id", 식품.getId())
+ .pathParam("id", 모든_영양기준_만족_식품.getId())
.get("/pet-foods/{id}");
// then
@@ -180,16 +182,6 @@ class GetPetFood {
.assertThat().statusCode(OK.value());
}
- private void 주원료_만들기() {
- PrimaryIngredient 주원료 = 주원료_닭고기();
- primaryIngredientRepository.save(주원료);
- }
-
- private PetFood 모의_식품_생성() {
- Brand 아카나 = brandRepository.save(아카나_식품_브랜드_생성());
- return petFoodRepository.save(모든_영양기준_만족_식품(아카나));
- }
-
private RestDocumentationFilter 식품_상세_조회_API_문서_생성() {
return document("식품 상세 조회 - 성공",
문서_정보.responseSchema(성공_응답_형식),
@@ -217,7 +209,7 @@ class GetPetFood {
}
@Test
- void 존재하지_않는_아이디로_요청한다() {
+ void 존재하지_않는_아이디로_요청하면_404를_반환한다() {
//given
var 요청_준비 = given(spec)
.contentType(JSON)
@@ -234,7 +226,7 @@ class GetPetFood {
}
@Test
- void 올바르지_않은_형식의_아이디로_요청한다() {
+ void 올바르지_않은_형식의_아이디로_요청하면_400을_반환한다() {
//given
var 요청_준비 = given(spec)
.contentType(JSON)
@@ -289,7 +281,17 @@ class GetFilterInfo {
.description("필터링에 필요한 메타데이터 정보를 조회합니다.");
@Test
- void 필터링에_필요한_메타데이터를_조회한다() {
+ void 필터링에_필요한_메타데이터를_조회하면_200을_반환한다() {
+ // given
+ var 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
+
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_다이어트()));
+ 식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
+
+ 식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_닭고기()));
+
+ petFoodRepository.save(모든_영양기준_만족_식품);
+
// when
var 응답 = given(spec)
.filter(필터링_메타데이터_API_문서_생성())
diff --git a/backend/src/test/java/zipgo/review/application/ReviewQueryServiceTest.java b/backend/src/test/java/zipgo/review/application/ReviewQueryServiceTest.java
index 6daa3111c..75ff103ee 100644
--- a/backend/src/test/java/zipgo/review/application/ReviewQueryServiceTest.java
+++ b/backend/src/test/java/zipgo/review/application/ReviewQueryServiceTest.java
@@ -1,12 +1,11 @@
package zipgo.review.application;
-import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import zipgo.brand.domain.Brand;
import zipgo.brand.domain.repository.BrandRepository;
-import zipgo.common.service.QueryServiceTest;
+import zipgo.common.service.ServiceTest;
import zipgo.member.domain.Member;
import zipgo.member.domain.repository.MemberRepository;
import zipgo.pet.domain.Breed;
@@ -29,6 +28,8 @@
import zipgo.review.dto.response.type.StoolConditionResponse;
import zipgo.review.dto.response.type.TastePreferenceResponse;
+import java.util.List;
+
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static zipgo.brand.domain.fixture.BrandFixture.아카나_식품_브랜드_생성;
@@ -46,7 +47,7 @@
import static zipgo.review.fixture.ReviewFixture.극찬_리뷰_생성;
import static zipgo.review.fixture.ReviewFixture.혹평_리뷰_생성;
-class ReviewQueryServiceTest extends QueryServiceTest {
+class ReviewQueryServiceTest extends ServiceTest {
@Autowired
private PetFoodRepository petFoodRepository;
@@ -204,7 +205,7 @@ class 리뷰_요약_조회 {
public static void validateReviewsRatingSummary(GetReviewsSummaryResponse reviewsSummary) {
assertAll(
() -> assertThat(reviewsSummary.rating().rating()).hasSize(5),
- () -> assertThat(reviewsSummary.rating().rating()).extracting(RatingInfoResponse::rating)
+ () -> assertThat(reviewsSummary.rating().rating()).extracting(RatingInfoResponse::name)
.contains("1", "2", "3", "4", "5"),
() -> assertThat(reviewsSummary.rating().rating()).extracting(RatingInfoResponse::percentage)
.contains(0, 50)
diff --git a/backend/src/test/java/zipgo/review/domain/repository/ReviewQueryRepositoryImplTest.java b/backend/src/test/java/zipgo/review/domain/repository/ReviewQueryRepositoryImplTest.java
index 7f0bccab1..b13d17944 100644
--- a/backend/src/test/java/zipgo/review/domain/repository/ReviewQueryRepositoryImplTest.java
+++ b/backend/src/test/java/zipgo/review/domain/repository/ReviewQueryRepositoryImplTest.java
@@ -1,21 +1,12 @@
package zipgo.review.domain.repository;
-import java.time.LocalDateTime;
-import java.time.Year;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Random;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
-import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
-import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.Transactional;
import zipgo.brand.domain.Brand;
import zipgo.brand.domain.repository.BrandRepository;
-import zipgo.common.config.QueryDslTestConfig;
+import zipgo.common.repository.RepositoryTest;
import zipgo.member.domain.Member;
import zipgo.member.domain.fixture.MemberFixture;
import zipgo.member.domain.repository.MemberRepository;
@@ -37,6 +28,13 @@
import zipgo.review.domain.repository.dto.ReviewHelpfulReaction;
import zipgo.review.domain.type.AdverseReactionType;
+import java.time.LocalDateTime;
+import java.time.Year;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
import static java.util.Collections.emptyList;
import static java.util.Collections.reverseOrder;
import static org.assertj.core.api.Assertions.assertThat;
@@ -52,10 +50,7 @@
import static zipgo.review.domain.type.TastePreference.EATS_VERY_WELL;
import static zipgo.review.fixture.ReviewFixture.극찬_리뷰_생성;
-@Import(QueryDslTestConfig.class)
-@DataJpaTest(properties = {"spring.sql.init.mode=never"})
-@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
-class ReviewQueryRepositoryImplTest {
+class ReviewQueryRepositoryImplTest extends RepositoryTest {
@Autowired
private ReviewQueryRepository reviewQueryRepository;
diff --git a/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java b/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java
index 2520bb65b..15784d828 100644
--- a/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java
+++ b/backend/src/test/java/zipgo/review/presentation/ReviewControllerTest.java
@@ -1,11 +1,12 @@
package zipgo.review.presentation;
-import com.epages.restdocs.apispec.ResourceSnippetDetails;
-import com.epages.restdocs.apispec.Schema;
-import io.restassured.response.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
+
+import com.epages.restdocs.apispec.ResourceSnippetDetails;
+import com.epages.restdocs.apispec.Schema;
+import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -325,9 +326,12 @@ class CreateReviews {
@Test
void 리뷰를_성공적으로_생성하면_201_반환() {
// given
- var token = jwtProvider.create("1");
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
var 리뷰_생성_요청 = 리뷰_생성_요청(식품.getId(), 반려동물.getId());
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token)
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.body(리뷰_생성_요청)
.contentType(JSON).filter(리뷰_생성_API_문서_생성());
@@ -365,8 +369,11 @@ class CreateReviews {
@Test
void 없는_식품에_대해_리뷰를_생성하면_404_반환() {
// given
- var token = jwtProvider.create("1");
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token)
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.body(리뷰_생성_요청(잘못된_id, 반려동물.getId())).contentType(JSON)
.filter(API_예외응답_문서_생성());
@@ -396,8 +403,11 @@ class UpdateReviews {
@Test
void 리뷰를_성공적으로_수정하면_204_반환() {
// given
- var token = jwtProvider.create("1");
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token)
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createRefreshToken();
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.body(리뷰_수정_요청()).contentType(JSON)
.filter(리뷰_수정_API_문서_생성());
@@ -424,8 +434,11 @@ class UpdateReviews {
@Test
void 리뷰를_쓴_사람이_아닌_멤버가_리뷰를_수정하면_403_반환() {
// given
- var notOwnerToken = jwtProvider.create("2");
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + notOwnerToken)
+ var accessToken = jwtProvider.createAccessToken(2L);
+ var refreshToken = jwtProvider.createRefreshToken();
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.body(리뷰_수정_요청())
.contentType(JSON).filter(API_예외응답_문서_생성());
@@ -455,8 +468,11 @@ class DeleteReviews {
@Test
void 리뷰를_성공적으로_삭제하면_204_반환() {
// given
- var token = jwtProvider.create("1");
- var 요청_준비 = given(spec).header("Authorization", "Bearer " + token)
+ var accessToken = jwtProvider.createAccessToken(1L);
+ var refreshToken = jwtProvider.createAccessToken(1L);
+ var 요청_준비 = given(spec)
+ .header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.body(리뷰_수정_요청()).contentType(JSON)
.filter(리뷰_삭제_API_문서_생성());
@@ -476,9 +492,11 @@ class DeleteReviews {
@Test
void 리뷰를_쓴_사람이_아닌_멤버가_리뷰를_삭제하면_403_반환() {
// given
- var notOwnerToken = jwtProvider.create("2");
+ var accessToken = jwtProvider.createAccessToken(2L);
+ var refreshToken = jwtProvider.createAccessToken(2L);
var 요청_준비 = given(spec)
- .header("Authorization", "Bearer " + notOwnerToken)
+ .header("Authorization", "Bearer " + accessToken)
+ .header("Refresh", "Zipgo " + refreshToken)
.contentType(JSON)
.filter(API_예외응답_문서_생성());
@@ -572,7 +590,7 @@ class GetReviewsSummary {
queryParameters(parameterWithName("petFoodId").description("식품 id")),
responseFields(
fieldWithPath("rating.average").description("리뷰 총 평점").type(JsonFieldType.NUMBER),
- fieldWithPath("rating.rating[].rating").description("rating 이름").type(JsonFieldType.STRING),
+ fieldWithPath("rating.rating[].name").description("rating 이름").type(JsonFieldType.STRING),
fieldWithPath("rating.rating[].percentage").description("rating 해당 백분율")
.type(JsonFieldType.NUMBER),
fieldWithPath("tastePreference[].name").description("tastePreference 이름")
@@ -604,7 +622,8 @@ class AddHelpfulReaction {
void 도움이돼요_추가_성공() {
//given
var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build());
- var 다른_회원의_JWT = jwtProvider.create(다른_회원.getId().toString());
+ var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId());
+ var 다른_회원의_리프레시_토큰 = jwtProvider.createRefreshToken();
var 요청_준비 = given().spec(spec)
.contentType(JSON)
@@ -613,6 +632,7 @@ class AddHelpfulReaction {
//when
요청_준비.when()
.header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT)
+ .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰)
.pathParam("reviewId", 리뷰.getId())
.post("/reviews/{reviewId}/helpful-reactions");
@@ -620,6 +640,7 @@ class AddHelpfulReaction {
Response 조회_응답 = given().spec(spec)
.contentType(JSON)
.header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT)
+ .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰)
.get("/reviews/{reviewId}", 리뷰.getId());
조회_응답.then()
@@ -631,7 +652,8 @@ class AddHelpfulReaction {
void 작성자가_도움이돼요를_추가하면_예외가_발생() {
//given
var 리뷰_작성자_id = 리뷰.getPet().getOwner().getId();
- var 리뷰_작성자_JWT = jwtProvider.create(리뷰_작성자_id.toString());
+ var 리뷰_작성자_JWT = jwtProvider.createAccessToken(리뷰_작성자_id);
+ var 리뷰_작성자_리프레시_토큰 = jwtProvider.createRefreshToken();
//when
var 요청_준비 = given().spec(spec)
@@ -641,6 +663,7 @@ class AddHelpfulReaction {
//when
var 응답 = 요청_준비.when()
.header(AUTHORIZATION, "Bearer " + 리뷰_작성자_JWT)
+ .header("Refresh", "Zipgo " + 리뷰_작성자_리프레시_토큰)
.pathParam("reviewId", 리뷰.getId())
.post("/reviews/{reviewId}/helpful-reactions");
@@ -655,7 +678,8 @@ class AddHelpfulReaction {
void 이미_눌렀던_리뷰일_경우() {
//given
var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build());
- var 다른_회원의_JWT = jwtProvider.create(다른_회원.getId().toString());
+ var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId());
+ var 다른_회원의_리프레시 = jwtProvider.createRefreshToken();
given().spec(spec).contentType(JSON)
.pathParam("reviewId", 리뷰.getId())
@@ -668,6 +692,7 @@ class AddHelpfulReaction {
//when
var 응답 = 요청_준비.when()
.header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT)
+ .header("Refresh", "Zipgo " + 다른_회원의_리프레시)
.pathParam("reviewId", 리뷰.getId())
.post("/reviews/{reviewId}/helpful-reactions");
@@ -700,7 +725,8 @@ class DeleteHelpfulReaction {
void 도움이돼요_취소_성공() {
//given
var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build());
- var 다른_회원의_JWT = jwtProvider.create(다른_회원.getId().toString());
+ var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId());
+ var 다른_회원의_리프레시_토큰 = jwtProvider.createRefreshToken();
given().spec(spec).contentType(JSON)
.pathParam("reviewId", 리뷰.getId())
@@ -713,6 +739,7 @@ class DeleteHelpfulReaction {
//when
var 응답 = 요청_준비.when()
.header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT)
+ .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰)
.pathParam("reviewId", 리뷰.getId())
.delete("/reviews/{reviewId}/helpful-reactions");
@@ -725,7 +752,8 @@ class DeleteHelpfulReaction {
void 누르지_않은_리뷰에서_취소() {
//given
var 다른_회원 = memberRepository.save(Member.builder().email("도움이돼요_추가할_회원").name("회원명").build());
- var 다른_회원의_JWT = jwtProvider.create(다른_회원.getId().toString());
+ var 다른_회원의_JWT = jwtProvider.createAccessToken(다른_회원.getId());
+ var 다른_회원의_리프레시_토큰 = jwtProvider.createRefreshToken();
var 요청_준비 = given().spec(spec)
.contentType(JSON)
@@ -734,6 +762,7 @@ class DeleteHelpfulReaction {
//when
var 응답 = 요청_준비.when()
.header(AUTHORIZATION, "Bearer " + 다른_회원의_JWT)
+ .header("Refresh", "Zipgo " + 다른_회원의_리프레시_토큰)
.pathParam("reviewId", 리뷰.getId())
.delete("/reviews/{reviewId}/helpful-reactions");
diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml
index f49095117..9a6c66bd0 100644
--- a/backend/src/test/resources/application.yml
+++ b/backend/src/test/resources/application.yml
@@ -21,7 +21,8 @@ spring:
enabled: true
jwt:
secret-key: this1-is2-zipgo3-test4-secret5-key6
- expire-length: 604800000
+ access-token-expiration-time: 604800000
+ refresh-token-expiration-time: 9999999999999
oauth:
kakao:
client-id: this1-is2-zipgo3-test4-client5-id7
diff --git a/frontend/.env.development b/frontend/.env.development
index a6e662478..750405545 100644
--- a/frontend/.env.development
+++ b/frontend/.env.development
@@ -1,3 +1,3 @@
FAST_REFRESH=true
BASE_URL=https://dev.api.zipgo.pet
-KAKAO_REDIRECT_URI=https://dev.zipgo.pet/login
+HOMEPAGE=https://dev.zipgo.pet
diff --git a/frontend/.env.production b/frontend/.env.production
index 7916a8517..7e4e0d0b0 100644
--- a/frontend/.env.production
+++ b/frontend/.env.production
@@ -1,2 +1,2 @@
BASE_URL=https://api.zipgo.pet
-KAKAO_REDIRECT_URI=https://zipgo.pet/login
+HOMEPAGE=https://zipgo.pet
diff --git a/frontend/.gitignore b/frontend/.gitignore
index 19c7d0746..c3f791f17 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -17,3 +17,5 @@ dist
storybook-static
.eslintcache
+
+*.pem
diff --git a/frontend/.nvmrc b/frontend/.nvmrc
new file mode 100644
index 000000000..73ff83260
--- /dev/null
+++ b/frontend/.nvmrc
@@ -0,0 +1,2 @@
+v18.16.1
+
diff --git a/frontend/.pnp.cjs b/frontend/.pnp.cjs
index d002ae30b..793f4e476 100755
--- a/frontend/.pnp.cjs
+++ b/frontend/.pnp.cjs
@@ -45,6 +45,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@storybook/test-runner", "npm:0.13.0"],\
["@storybook/testing-library", "npm:0.2.0"],\
["@tanstack/react-query", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.29.25"],\
+ ["@tanstack/react-query-devtools", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.36.1"],\
["@testing-library/dom", "npm:9.3.1"],\
["@testing-library/jest-dom", "npm:5.16.5"],\
["@testing-library/react", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:14.0.0"],\
@@ -109,7 +110,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["webpack-bundle-analyzer", "npm:4.9.1"],\
["webpack-cli", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:5.1.4"],\
["webpack-dev-server", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.15.1"],\
- ["webpack-merge", "npm:5.9.0"]\
+ ["webpack-merge", "npm:5.9.0"],\
+ ["zipgo-layout", "npm:0.4.0"]\
],\
"linkType": "SOFT"\
}]\
@@ -9413,6 +9415,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
+ ["@tanstack/match-sorter-utils", [\
+ ["npm:8.8.4", {\
+ "packageLocation": "./.yarn/cache/@tanstack-match-sorter-utils-npm-8.8.4-488b98c113-d005f50075.zip/node_modules/@tanstack/match-sorter-utils/",\
+ "packageDependencies": [\
+ ["@tanstack/match-sorter-utils", "npm:8.8.4"],\
+ ["remove-accents", "npm:0.4.2"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["@tanstack/query-core", [\
["npm:4.29.25", {\
"packageLocation": "./.yarn/cache/@tanstack-query-core-npm-4.29.25-6b3da5fc2b-5287e278cf.zip/node_modules/@tanstack/query-core/",\
@@ -9441,7 +9453,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["react", "npm:18.2.0"],\
["react-dom", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:18.2.0"],\
["react-native", null],\
- ["use-sync-external-store", "virtual:9c24e36d42399c7366475b2b84f4a3444202f6f9cc946a653e07f347eca2ca0681be2719ea2726e1b9b80bdaf2fd4c0d80d7982698a10885f7731d7b508979ff#npm:1.2.0"]\
+ ["use-sync-external-store", "virtual:d5698880712cc2658275f764622ac80990362299ce111d53d89856faba511f69edee2a268a8be764eec3062a426d656dda5c426f153eb88c399c1551841b08af#npm:1.2.0"]\
],\
"packagePeers": [\
"@types/react-dom",\
@@ -9454,6 +9466,39 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
+ ["@tanstack/react-query-devtools", [\
+ ["npm:4.36.1", {\
+ "packageLocation": "./.yarn/cache/@tanstack-react-query-devtools-npm-4.36.1-8448e7912a-d4f7da4121.zip/node_modules/@tanstack/react-query-devtools/",\
+ "packageDependencies": [\
+ ["@tanstack/react-query-devtools", "npm:4.36.1"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.36.1", {\
+ "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-devtools-virtual-d569888071/0/cache/@tanstack-react-query-devtools-npm-4.36.1-8448e7912a-d4f7da4121.zip/node_modules/@tanstack/react-query-devtools/",\
+ "packageDependencies": [\
+ ["@tanstack/react-query-devtools", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.36.1"],\
+ ["@tanstack/match-sorter-utils", "npm:8.8.4"],\
+ ["@tanstack/react-query", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.29.25"],\
+ ["@types/react", "npm:18.2.15"],\
+ ["@types/react-dom", "npm:18.2.7"],\
+ ["@types/tanstack__react-query", null],\
+ ["react", "npm:18.2.0"],\
+ ["react-dom", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:18.2.0"],\
+ ["superjson", "npm:1.13.3"],\
+ ["use-sync-external-store", "virtual:d5698880712cc2658275f764622ac80990362299ce111d53d89856faba511f69edee2a268a8be764eec3062a426d656dda5c426f153eb88c399c1551841b08af#npm:1.2.0"]\
+ ],\
+ "packagePeers": [\
+ "@tanstack/react-query",\
+ "@types/react-dom",\
+ "@types/react",\
+ "@types/tanstack__react-query",\
+ "react-dom",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["@testing-library/dom", [\
["npm:9.3.1", {\
"packageLocation": "./.yarn/cache/@testing-library-dom-npm-9.3.1-ec81dc9367-8ee3136451.zip/node_modules/@testing-library/dom/",\
@@ -13155,6 +13200,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
+ ["copy-anything", [\
+ ["npm:3.0.5", {\
+ "packageLocation": "./.yarn/cache/copy-anything-npm-3.0.5-562d15fb3f-d39f6601c1.zip/node_modules/copy-anything/",\
+ "packageDependencies": [\
+ ["copy-anything", "npm:3.0.5"],\
+ ["is-what", "npm:4.1.15"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["copy-webpack-plugin", [\
["npm:11.0.0", {\
"packageLocation": "./.yarn/cache/copy-webpack-plugin-npm-11.0.0-9a07415855-df4f8743f0.zip/node_modules/copy-webpack-plugin/",\
@@ -15669,6 +15724,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@storybook/test-runner", "npm:0.13.0"],\
["@storybook/testing-library", "npm:0.2.0"],\
["@tanstack/react-query", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.29.25"],\
+ ["@tanstack/react-query-devtools", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.36.1"],\
["@testing-library/dom", "npm:9.3.1"],\
["@testing-library/jest-dom", "npm:5.16.5"],\
["@testing-library/react", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:14.0.0"],\
@@ -15733,7 +15789,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["webpack-bundle-analyzer", "npm:4.9.1"],\
["webpack-cli", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:5.1.4"],\
["webpack-dev-server", "virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:4.15.1"],\
- ["webpack-merge", "npm:5.9.0"]\
+ ["webpack-merge", "npm:5.9.0"],\
+ ["zipgo-layout", "npm:0.4.0"]\
],\
"linkType": "SOFT"\
}]\
@@ -17427,6 +17484,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
+ ["is-what", [\
+ ["npm:4.1.15", {\
+ "packageLocation": "./.yarn/cache/is-what-npm-4.1.15-328677a458-fe27f6cd4a.zip/node_modules/is-what/",\
+ "packageDependencies": [\
+ ["is-what", "npm:4.1.15"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["is-windows", [\
["npm:0.2.0", {\
"packageLocation": "./.yarn/cache/is-windows-npm-0.2.0-32c20e83a7-3df25afda2.zip/node_modules/is-windows/",\
@@ -20741,6 +20807,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["source-map-js", "npm:1.0.2"]\
],\
"linkType": "HARD"\
+ }],\
+ ["npm:8.4.31", {\
+ "packageLocation": "./.yarn/cache/postcss-npm-8.4.31-385051a82b-1d8611341b.zip/node_modules/postcss/",\
+ "packageDependencies": [\
+ ["postcss", "npm:8.4.31"],\
+ ["nanoid", "npm:3.3.6"],\
+ ["picocolors", "npm:1.0.0"],\
+ ["source-map-js", "npm:1.0.2"]\
+ ],\
+ "linkType": "HARD"\
}]\
]],\
["postcss-modules-extract-imports", [\
@@ -21848,6 +21924,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
+ ["remove-accents", [\
+ ["npm:0.4.2", {\
+ "packageLocation": "./.yarn/cache/remove-accents-npm-0.4.2-7cb341092a-84a6988555.zip/node_modules/remove-accents/",\
+ "packageDependencies": [\
+ ["remove-accents", "npm:0.4.2"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["renderkid", [\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/renderkid-npm-3.0.0-acb028643f-77162b62d6.zip/node_modules/renderkid/",\
@@ -23162,6 +23247,39 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "SOFT"\
}],\
+ ["npm:6.1.0", {\
+ "packageLocation": "./.yarn/cache/styled-components-npm-6.1.0-309a24863e-989262a2be.zip/node_modules/styled-components/",\
+ "packageDependencies": [\
+ ["styled-components", "npm:6.1.0"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:bfdb260d66a67fb440a36b5e7d3099e1ef4c2420a265088f95a491b65c807b3174254c5223a22dfffa2ffb3757ec70f7fe49b5722e37e17babd81cd44d32cf56#npm:6.1.0", {\
+ "packageLocation": "./.yarn/__virtual__/styled-components-virtual-b27e270236/0/cache/styled-components-npm-6.1.0-309a24863e-989262a2be.zip/node_modules/styled-components/",\
+ "packageDependencies": [\
+ ["styled-components", "virtual:bfdb260d66a67fb440a36b5e7d3099e1ef4c2420a265088f95a491b65c807b3174254c5223a22dfffa2ffb3757ec70f7fe49b5722e37e17babd81cd44d32cf56#npm:6.1.0"],\
+ ["@emotion/is-prop-valid", "npm:1.2.1"],\
+ ["@emotion/unitless", "npm:0.8.1"],\
+ ["@types/react", null],\
+ ["@types/react-dom", null],\
+ ["@types/stylis", "npm:4.2.0"],\
+ ["css-to-react-native", "npm:3.2.0"],\
+ ["csstype", "npm:3.1.2"],\
+ ["postcss", "npm:8.4.31"],\
+ ["react", "npm:18.2.0"],\
+ ["react-dom", null],\
+ ["shallowequal", "npm:1.1.0"],\
+ ["stylis", "npm:4.3.0"],\
+ ["tslib", "npm:2.6.0"]\
+ ],\
+ "packagePeers": [\
+ "@types/react-dom",\
+ "@types/react",\
+ "react-dom",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }],\
["virtual:d9650954c7f1725ea712d2e40695de9ea5d1c0fa9b89379134c62909d440d91b03d6c37e2823804a3404472b90a5bec73c82193190dd3c4db4c9e9edd75db91e#npm:6.0.4", {\
"packageLocation": "./.yarn/__virtual__/styled-components-virtual-b7f0a8e1c4/0/cache/styled-components-npm-6.0.4-fbf66c4137-15c5701c26.zip/node_modules/styled-components/",\
"packageDependencies": [\
@@ -23375,6 +23493,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
+ ["superjson", [\
+ ["npm:1.13.3", {\
+ "packageLocation": "./.yarn/cache/superjson-npm-1.13.3-25a5e9e483-f5aeb010f2.zip/node_modules/superjson/",\
+ "packageDependencies": [\
+ ["superjson", "npm:1.13.3"],\
+ ["copy-anything", "npm:3.0.5"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["supports-color", [\
["npm:5.5.0", {\
"packageLocation": "./.yarn/cache/supports-color-npm-5.5.0-183ac537bc-95f6f4ba5a.zip/node_modules/supports-color/",\
@@ -24385,10 +24513,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "SOFT"\
}],\
- ["virtual:9c24e36d42399c7366475b2b84f4a3444202f6f9cc946a653e07f347eca2ca0681be2719ea2726e1b9b80bdaf2fd4c0d80d7982698a10885f7731d7b508979ff#npm:1.2.0", {\
- "packageLocation": "./.yarn/__virtual__/use-sync-external-store-virtual-f62de996c5/0/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip/node_modules/use-sync-external-store/",\
+ ["virtual:d5698880712cc2658275f764622ac80990362299ce111d53d89856faba511f69edee2a268a8be764eec3062a426d656dda5c426f153eb88c399c1551841b08af#npm:1.2.0", {\
+ "packageLocation": "./.yarn/__virtual__/use-sync-external-store-virtual-8d8319ae0b/0/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip/node_modules/use-sync-external-store/",\
"packageDependencies": [\
- ["use-sync-external-store", "virtual:9c24e36d42399c7366475b2b84f4a3444202f6f9cc946a653e07f347eca2ca0681be2719ea2726e1b9b80bdaf2fd4c0d80d7982698a10885f7731d7b508979ff#npm:1.2.0"],\
+ ["use-sync-external-store", "virtual:d5698880712cc2658275f764622ac80990362299ce111d53d89856faba511f69edee2a268a8be764eec3062a426d656dda5c426f153eb88c399c1551841b08af#npm:1.2.0"],\
["@types/react", "npm:18.2.15"],\
["react", "npm:18.2.0"]\
],\
@@ -25387,6 +25515,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "HARD"\
}]\
+ ]],\
+ ["zipgo-layout", [\
+ ["npm:0.4.0", {\
+ "packageLocation": "./.yarn/cache/zipgo-layout-npm-0.4.0-bfdb260d66-0a6852d018.zip/node_modules/zipgo-layout/",\
+ "packageDependencies": [\
+ ["zipgo-layout", "npm:0.4.0"],\
+ ["react", "npm:18.2.0"],\
+ ["styled-components", "virtual:bfdb260d66a67fb440a36b5e7d3099e1ef4c2420a265088f95a491b65c807b3174254c5223a22dfffa2ffb3757ec70f7fe49b5722e37e17babd81cd44d32cf56#npm:6.1.0"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
]]\
]\
}'), {basePath: basePath || __dirname});
diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx
index 0d85ce0af..8e4f4c344 100644
--- a/frontend/.storybook/preview.tsx
+++ b/frontend/.storybook/preview.tsx
@@ -7,8 +7,20 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { withRouter } from 'storybook-addon-react-router-v6';
import handlers from '../src/mocks/handlers';
+import Loading from '../src/components/@common/Loading/Loading';
-initialize(); // msw-storybook-addon
+let options = {};
+
+if (location.hostname.includes('cloudfront')) {
+ options = {
+ serviceWorker: {
+ url: '/storybook/mockServiceWorker.js',
+ },
+ };
+}
+
+// Initialize MSW
+initialize(options);
const queryClient = new QueryClient({
defaultOptions: {
@@ -85,9 +97,9 @@ const preview: Preview = {
- Loading...}>
+
-
+
),
diff --git a/frontend/.stylelintrc.js b/frontend/.stylelintrc.js
index e3c169383..e3d4c38cf 100644
--- a/frontend/.stylelintrc.js
+++ b/frontend/.stylelintrc.js
@@ -34,5 +34,7 @@ module.exports = {
severity: 'error',
},
],
+
+ 'property-no-vendor-prefix': null,
},
};
diff --git a/frontend/.yarn/cache/@tanstack-match-sorter-utils-npm-8.8.4-488b98c113-d005f50075.zip b/frontend/.yarn/cache/@tanstack-match-sorter-utils-npm-8.8.4-488b98c113-d005f50075.zip
new file mode 100644
index 000000000..518400f58
Binary files /dev/null and b/frontend/.yarn/cache/@tanstack-match-sorter-utils-npm-8.8.4-488b98c113-d005f50075.zip differ
diff --git a/frontend/.yarn/cache/@tanstack-react-query-devtools-npm-4.36.1-8448e7912a-d4f7da4121.zip b/frontend/.yarn/cache/@tanstack-react-query-devtools-npm-4.36.1-8448e7912a-d4f7da4121.zip
new file mode 100644
index 000000000..6ccc64155
Binary files /dev/null and b/frontend/.yarn/cache/@tanstack-react-query-devtools-npm-4.36.1-8448e7912a-d4f7da4121.zip differ
diff --git a/frontend/.yarn/cache/copy-anything-npm-3.0.5-562d15fb3f-d39f6601c1.zip b/frontend/.yarn/cache/copy-anything-npm-3.0.5-562d15fb3f-d39f6601c1.zip
new file mode 100644
index 000000000..c1f7e5b8e
Binary files /dev/null and b/frontend/.yarn/cache/copy-anything-npm-3.0.5-562d15fb3f-d39f6601c1.zip differ
diff --git a/frontend/.yarn/cache/is-what-npm-4.1.15-328677a458-fe27f6cd4a.zip b/frontend/.yarn/cache/is-what-npm-4.1.15-328677a458-fe27f6cd4a.zip
new file mode 100644
index 000000000..3ca7651a3
Binary files /dev/null and b/frontend/.yarn/cache/is-what-npm-4.1.15-328677a458-fe27f6cd4a.zip differ
diff --git a/frontend/.yarn/cache/postcss-npm-8.4.31-385051a82b-1d8611341b.zip b/frontend/.yarn/cache/postcss-npm-8.4.31-385051a82b-1d8611341b.zip
new file mode 100644
index 000000000..dc7dd6671
Binary files /dev/null and b/frontend/.yarn/cache/postcss-npm-8.4.31-385051a82b-1d8611341b.zip differ
diff --git a/frontend/.yarn/cache/remove-accents-npm-0.4.2-7cb341092a-84a6988555.zip b/frontend/.yarn/cache/remove-accents-npm-0.4.2-7cb341092a-84a6988555.zip
new file mode 100644
index 000000000..dd33a9bc0
Binary files /dev/null and b/frontend/.yarn/cache/remove-accents-npm-0.4.2-7cb341092a-84a6988555.zip differ
diff --git a/frontend/.yarn/cache/styled-components-npm-6.1.0-309a24863e-989262a2be.zip b/frontend/.yarn/cache/styled-components-npm-6.1.0-309a24863e-989262a2be.zip
new file mode 100644
index 000000000..ff5d754ef
Binary files /dev/null and b/frontend/.yarn/cache/styled-components-npm-6.1.0-309a24863e-989262a2be.zip differ
diff --git a/frontend/.yarn/cache/superjson-npm-1.13.3-25a5e9e483-f5aeb010f2.zip b/frontend/.yarn/cache/superjson-npm-1.13.3-25a5e9e483-f5aeb010f2.zip
new file mode 100644
index 000000000..1a3a214c5
Binary files /dev/null and b/frontend/.yarn/cache/superjson-npm-1.13.3-25a5e9e483-f5aeb010f2.zip differ
diff --git a/frontend/.yarn/cache/zipgo-layout-npm-0.4.0-bfdb260d66-0a6852d018.zip b/frontend/.yarn/cache/zipgo-layout-npm-0.4.0-bfdb260d66-0a6852d018.zip
new file mode 100644
index 000000000..2b0b67594
Binary files /dev/null and b/frontend/.yarn/cache/zipgo-layout-npm-0.4.0-bfdb260d66-0a6852d018.zip differ
diff --git a/frontend/config/env.js b/frontend/config/env.js
index 1dffba0cb..3b1e5ad5f 100644
--- a/frontend/config/env.js
+++ b/frontend/config/env.js
@@ -6,23 +6,54 @@ const dotenvExpand = require('dotenv-expand');
const NODE_ENV = process.env.NODE_ENV;
-const dotenvFiles = [`${paths.dotenv}.local`, `${paths.dotenv}.${NODE_ENV}`, paths.dotenv].filter(Boolean);
+const isDevelopment = process.env.NODE_ENV !== 'production';
+const isProduction = process.env.NODE_ENV === 'production';
+const isLocal = isDevelopment && process.env.Local === 'on';
-dotenvFiles.forEach(file => fs.existsSync(file) && dotenvExpand.expand(dotenv.config({path: file})));
+const HTTPS = process.env.HTTPS === 'on';
+
+const localhost = 'http://localhost:3000';
+
+const dotenvFiles = [`${paths.dotenv}.local`, `${paths.dotenv}.${NODE_ENV}`, paths.dotenv].filter(
+ Boolean,
+);
+
+const setHomepage = homepage => {
+ if (!isLocal) return JSON.stringify(homepage);
+
+ if (HTTPS) return JSON.stringify(localhost.replace('http', 'https'));
+
+ return JSON.stringify(localhost);
+};
+
+dotenvFiles.forEach(
+ file => fs.existsSync(file) && dotenvExpand.expand(dotenv.config({ path: file })),
+);
const getClientEnvironment = publicUrl => {
- const raw = Object.keys(process.env).reduce((env, key) => ({...env, [key]: process.env[key]}), {
+ const raw = Object.keys(process.env).reduce((env, key) => ({ ...env, [key]: process.env[key] }), {
NODE_ENV: process.env.NODE_ENV || 'development',
PUBLIC_URL: publicUrl,
});
const stringified = {
- 'process.env': Object.keys(raw).reduce((env, key) => ({...env, [key]: JSON.stringify(raw[key])}), {}),
+ 'process.env': Object.keys(raw).reduce(
+ (env, key) => ({
+ ...env,
+ [key]: key === 'HOMEPAGE' ? setHomepage(raw[key]) : JSON.stringify(raw[key]),
+ }),
+ {},
+ ),
};
- return {stringified};
+ return { stringified };
};
module.exports = {
getClientEnvironment,
+ isDevelopment,
+ isProduction,
+ isLocal,
+ HTTPS,
+ localhost,
};
diff --git a/frontend/config/paths.js b/frontend/config/paths.js
index 35750763a..cf5eca0c1 100644
--- a/frontend/config/paths.js
+++ b/frontend/config/paths.js
@@ -43,4 +43,6 @@ module.exports = {
moduleFileExtensions,
'alias@': resolveApp('src/'),
resolveLib,
+ localhostKey: resolveApp('config/localhost+1-key.pem'),
+ cert: resolveApp('config/localhost+1.pem'),
};
diff --git a/frontend/config/webpack.common.js b/frontend/config/webpack.common.js
index 355fc096e..880fb78f5 100644
--- a/frontend/config/webpack.common.js
+++ b/frontend/config/webpack.common.js
@@ -1,6 +1,6 @@
const paths = require('./paths');
const webpackUtils = require('./webpackUtils');
-const { getClientEnvironment } = require('./env');
+const { getClientEnvironment, isDevelopment, isProduction, isLocal } = require('./env');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
@@ -12,11 +12,9 @@ const PUBLIC_PATH = '/';
const libListToCopy = ['axios'];
-const isDevelopment = process.env.NODE_ENV !== 'production';
-const isProduction = process.env.NODE_ENV === 'production';
-
const env = getClientEnvironment(PUBLIC_PATH);
+/** @type {import('webpack').Configuration} */
module.exports = {
context: __dirname,
entry: paths.appIndex,
@@ -61,7 +59,7 @@ module.exports = {
hash: true,
favicon: paths.appFavicon,
}),
- new DefinePlugin({ ...env.stringified, isDevelopment, isProduction }),
+ new DefinePlugin({ ...env.stringified, isDevelopment, isProduction, isLocal }),
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: paths.appTsConfig,
diff --git a/frontend/config/webpack.dev.js b/frontend/config/webpack.dev.js
index 92277d167..bc0cd2714 100644
--- a/frontend/config/webpack.dev.js
+++ b/frontend/config/webpack.dev.js
@@ -1,8 +1,10 @@
const paths = require('./paths.js');
+const { HTTPS } = require('./env.js');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const CopyPlugin = require('copy-webpack-plugin');
+const fs = require('fs');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = merge(common, {
@@ -11,6 +13,17 @@ module.exports = merge(common, {
devServer: {
historyApiFallback: true,
port: 3000,
+ ...(HTTPS
+ ? {
+ server: {
+ type: 'https',
+ options: {
+ key: fs.readFileSync(paths.localhostKey),
+ cert: fs.readFileSync(paths.cert),
+ },
+ },
+ }
+ : {}),
},
plugins: [
new ReactRefreshWebpackPlugin(),
diff --git a/frontend/package.json b/frontend/package.json
index c4eb7395b..0a40d93ac 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -4,7 +4,7 @@
"scripts": {
"type-check": "tsc --noEmit",
"type-check:watch": "yarn type-check --watch",
- "start:dev": "cross-env NODE_ENV=development MSW=on STRICT_MODE=on webpack serve --config config/webpack.dev.js",
+ "start:dev": "cross-env NODE_ENV=development MSW=on Local=on STRICT_MODE=on webpack serve --config config/webpack.dev.js",
"start:prod": "yarn build:prod && serve -s dist",
"start:sb": "storybook dev -p 6006",
"build:prod": "cross-env NODE_ENV=production webpack --progress --config config/webpack.prod.js",
@@ -91,13 +91,15 @@
},
"dependencies": {
"@tanstack/react-query": "^4.29.19",
+ "@tanstack/react-query-devtools": "^4.36.1",
"axios": "^1.4.0",
"browser-image-compression": "^2.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.14.2",
"react-router-dom": "^6.14.1",
- "styled-normalize": "^8.0.7"
+ "styled-normalize": "^8.0.7",
+ "zipgo-layout": "0.4.0"
},
"lint-staged": {
"**/*.{ts,tsx}": [
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7b5d20bc2..29efc943e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,10 +1,64 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
+import { ThemeProvider } from 'styled-components';
-import AxiosInterceptors from './components/@common/AxiosInterceptors/AxiosInterceptors';
+import { CriticalBoundary } from './components/@common/ErrorBoundary/ErrorBoundary';
+import QueryBoundary from './components/@common/ErrorBoundary/QueryBoundary/QueryBoundary';
+import GlobalStyle from './components/@common/GlobalStyle';
+import MobileToDesktop from './components/@common/MobileToDesktop/MobileToDesktop';
+import { ERROR_MESSAGE_KIT } from './constants/errors';
+import { ONE_HOUR } from './constants/time';
+import ToastProvider, { useToast } from './context/Toast/ToastContext';
+import useErrorBoundary from './hooks/@common/useErrorBoundary';
+import ErrorPage from './pages/Error/ErrorPage';
+import theme from './styles/theme';
+import { ErrorBoundaryValue } from './types/common/errorBoundary';
+import { UnexpectedError } from './utils/errors';
import { setScreenSize } from './utils/setScreenSize';
-const App = () => {
+const errorFallback = ({ reset, error }: ErrorBoundaryValue) => (
+
+);
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ suspense: true,
+ retry: false,
+ useErrorBoundary: true,
+ staleTime: ONE_HOUR,
+ cacheTime: ONE_HOUR,
+ },
+ },
+});
+
+const App = () => (
+
+
+
+
+
+
+
+
+
+ {isDevelopment && }
+
+
+
+
+
+
+);
+export default App;
+
+const GlobalEvent = () => {
+ const { toast } = useToast();
+
+ const { setError } = useErrorBoundary();
+
useEffect(() => {
setScreenSize();
}, []);
@@ -14,17 +68,30 @@ const App = () => {
setScreenSize();
};
+ const onUnhandledRejection = () => {
+ setError(new UnexpectedError('Unhandled rejection'));
+ };
+
+ const onOffline = () => {
+ toast.warning(ERROR_MESSAGE_KIT.OFFLINE);
+ };
+
+ const onOnline = () => {
+ toast.success(ERROR_MESSAGE_KIT.ONLINE);
+ };
+
window.addEventListener('resize', onResize);
+ window.addEventListener('online', onOnline);
+ window.addEventListener('offline', onOffline);
+ window.addEventListener('unhandledrejection', onUnhandledRejection);
return () => {
window.removeEventListener('resize', onResize);
+ window.removeEventListener('online', onOnline);
+ window.removeEventListener('offline', onOffline);
+ window.removeEventListener('unhandledrejection', onUnhandledRejection);
};
}, []);
- return (
-
-
-
- );
+ return null;
};
-export default App;
diff --git a/frontend/src/apis/auth.ts b/frontend/src/apis/auth.ts
index 4d73fa9ce..efda5ec4e 100644
--- a/frontend/src/apis/auth.ts
+++ b/frontend/src/apis/auth.ts
@@ -1,23 +1,43 @@
+import { KAKAO_REDIRECT_URI } from '@/constants/auth';
import {
AuthenticateUserRes,
LoginKakaoAuthRes,
LoginZipgoAuthReq,
LoginZipgoAuthRes,
+ RefreshZipgoAuthRes,
} from '@/types/auth/remote';
import { zipgoLocalStorage } from '@/utils/localStorage';
-import { client, clientBasic, createConfigWithAuth } from '.';
+import { client, clientBasic } from '.';
export const loginZipgoAuth = async ({ code }: LoginZipgoAuthReq) => {
const { data } = await client.post('/auth/login', null, {
params: {
code,
+ 'redirect-uri': KAKAO_REDIRECT_URI,
},
});
return data;
};
+export const refreshZipgoAuth = async () => {
+ const { refreshToken } = zipgoLocalStorage.getTokens({ required: true });
+
+ const {
+ data: { accessToken },
+ } = await clientBasic('/auth/refresh', {
+ headers: {
+ Refresh: `Zipgo ${refreshToken}`,
+ },
+ });
+
+ return {
+ accessToken,
+ refreshToken,
+ };
+};
+
export const loginKakaoAuth = async () => {
const { data } = await client('/login/kakao');
@@ -42,10 +62,11 @@ export const logoutKaKaoAuth = async () => {
export const authenticateUser = async () => {
const tokens = zipgoLocalStorage.getTokens();
- const { data } = await clientBasic.get(
- '/auth',
- createConfigWithAuth(tokens),
- );
+ const { data } = await client('/auth', {
+ headers: {
+ Refresh: `Zipgo ${tokens?.refreshToken}`,
+ },
+ });
return data;
};
diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts
index 7c7344579..6f7743cd1 100644
--- a/frontend/src/apis/index.ts
+++ b/frontend/src/apis/index.ts
@@ -1,26 +1,52 @@
-import axios from 'axios';
+import axios, { AxiosError, AxiosRequestConfig } from 'axios';
-import { Tokens } from '@/types/auth/client';
+import { APIError } from '@/utils/errors';
import { zipgoLocalStorage } from '@/utils/localStorage';
-export const { BASE_URL } = process.env;
+import { refreshZipgoAuth } from './auth';
-const tokens = zipgoLocalStorage.getTokens();
+export const { BASE_URL } = process.env;
-const defaultConfig = {
+const defaultConfig: AxiosRequestConfig = {
baseURL: BASE_URL,
};
-export const createConfigWithAuth = (tokens: Tokens | null) =>
- tokens
- ? {
- ...defaultConfig,
- headers: {
- Authorization: `Bearer ${tokens.accessToken}`,
- },
- }
- : defaultConfig;
-
export const clientBasic = axios.create(defaultConfig);
-export const client = axios.create(createConfigWithAuth(tokens));
+export const client = axios.create(defaultConfig);
+
+client.interceptors.request.use(config => {
+ const tokens = zipgoLocalStorage.getTokens();
+
+ if (tokens) {
+ // eslint-disable-next-line no-param-reassign
+ config.headers.Authorization = `Bearer ${tokens.accessToken}`;
+ }
+
+ return config;
+});
+
+client.interceptors.response.use(
+ response => response,
+ async (error: AxiosError) => {
+ const config = error.config;
+
+ if (APIError.isAuthError(error) && config?.headers.Authorization) {
+ try {
+ const tokens = await refreshZipgoAuth();
+
+ zipgoLocalStorage.setTokens(tokens);
+
+ config.headers.Authorization = `Bearer ${tokens.accessToken}`;
+
+ return client(config);
+ } catch (error) {
+ alert('세션이 만료되었습니다');
+
+ zipgoLocalStorage.clearAuth();
+ }
+ }
+
+ return Promise.reject(error);
+ },
+);
diff --git a/frontend/src/assets/svg/background_img.svg b/frontend/src/assets/svg/background_img.svg
new file mode 100644
index 000000000..4affe1fd9
--- /dev/null
+++ b/frontend/src/assets/svg/background_img.svg
@@ -0,0 +1,46 @@
+
diff --git a/frontend/src/assets/svg/close_square_icon_light.svg b/frontend/src/assets/svg/close_square_icon_light.svg
new file mode 100644
index 000000000..01562d13e
--- /dev/null
+++ b/frontend/src/assets/svg/close_square_icon_light.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/src/assets/svg/home_icon.svg b/frontend/src/assets/svg/home_icon.svg
index d26c3038c..6efead1a3 100644
--- a/frontend/src/assets/svg/home_icon.svg
+++ b/frontend/src/assets/svg/home_icon.svg
@@ -1,4 +1,4 @@
-