@@ -130,6 +494,7 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
---
+
Step3. Week-5
@@ -269,11 +634,10 @@ UI 컴포넌트의 명칭과 이를 구현하는 능력은 필수적인 커뮤
**1. PR 제목과 내용을 아래와 같이 작성 해주세요.**
-> PR 제목 : 부산대_0조_아이템명_0주차
->
+PR 제목 : 부산대*0조*아이템명\_0주차
-
\ No newline at end of file
+
diff --git a/build.gradle b/build.gradle
index fb96d563..aa8c8e4f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,13 @@
buildscript {
ext {
apiServerUrl = System.getenv("API_SERVER_URL")
- new File('.env').getText('UTF-8').splitEachLine(/=/) {
- if (it[0] == "API_SERVER_URL") {
- apiServerUrl = it[1].replaceAll('"', '')
+ def envFile = new File('.env')
+
+ if (envFile.exists()) {
+ envFile.getText('UTF-8').splitEachLine(/=/) {
+ if (it[0] == "API_SERVER_URL") {
+ apiServerUrl = it[1].replaceAll('"', '')
+ }
}
}
}
@@ -63,6 +67,8 @@ dependencies {
// aws
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
+ implementation 'software.amazon.awssdk:s3:2.20.162'
+ implementation 'javax.xml.bind:jaxb-api:2.3.0'
}
test {
diff --git "a/docs/\353\262\210\352\260\234\353\263\274\353\247\201_\352\270\260\355\232\215\354\225\210_v2.3.pptx" "b/docs/\353\262\210\352\260\234\353\263\274\353\247\201_\352\270\260\355\232\215\354\225\210_v2.3.pptx"
new file mode 100644
index 00000000..6647ab92
Binary files /dev/null and "b/docs/\353\262\210\352\260\234\353\263\274\353\247\201_\352\270\260\355\232\215\354\225\210_v2.3.pptx" differ
diff --git a/flask/Dockerfile b/flask/Dockerfile
new file mode 100644
index 00000000..af5f83fe
--- /dev/null
+++ b/flask/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.8-alpine
+
+COPY . /app
+
+WORKDIR /app
+
+RUN pip install Flask-Mail
+
+RUN pip install flask
+
+RUN chmod +x /app/app.py
+
+CMD ["python3", "app.py"]
\ No newline at end of file
diff --git a/flask/app.py b/flask/app.py
new file mode 100644
index 00000000..8df67459
--- /dev/null
+++ b/flask/app.py
@@ -0,0 +1,42 @@
+from flask import Flask, request, Response
+import smtplib
+from email.mime.text import MIMEText
+
+app = Flask(__name__)
+
+# HTTP POST 요청을 처리하는 엔드포인트
+@app.route('/email', methods=['POST'])
+def sendEmailEndpoint():
+
+ try:
+ jsonRequest = request.get_json()
+
+ subject = str(jsonRequest.get('subject')[0])
+ text = str(jsonRequest.get('text')[0])
+ email = str(jsonRequest.get('email')[0])
+ username = str(jsonRequest.get('username')[0])
+ password = str(jsonRequest.get('password')[0])
+
+ smtp = smtplib.SMTP('smtp.gmail.com', 587)
+ smtp.ehlo()
+ smtp.starttls()
+ smtp.login(username, password)
+
+ msg = MIMEText(text, "html")
+ msg['Subject'] = subject
+
+ smtp.sendmail(username, email, msg.as_string())
+ smtp.quit()
+
+ response = Response("Email sent successfully", status=200)
+
+ return response
+
+ except Exception as e:
+ error_message = str(e)
+ response = Response("Failed to send email: " + error_message, status=500)
+
+ return response
+
+if __name__ == '__main__':
+ app.run('0.0.0.0', port=5000, debug=True)
\ No newline at end of file
diff --git a/flask/start.sh b/flask/start.sh
new file mode 100644
index 00000000..e1dc92d8
--- /dev/null
+++ b/flask/start.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# Docker 이미지 빌드
+docker build -t flask-app:v1 .
+
+# Docker 컨테이너 실행
+docker run -d -p 80:5000 flask-app:v1
\ No newline at end of file
diff --git a/k8s/backend.yaml b/k8s/backend.yaml
index 4ebd548e..5dfc6020 100644
--- a/k8s/backend.yaml
+++ b/k8s/backend.yaml
@@ -15,7 +15,7 @@ spec:
containers:
- name: backend
# 여러분의 backend image 주소를 입력해주세요. -> 빌드 후 빌드 이미지 경로 새로 넣기
- image: krmp-d2hub-idock.9rum.cc/dev-test/repo_f9940ca26849
+ image: krmp-d2hub-idock.9rum.cc/dev-test/repo_64cf4065f88f
env:
- name: TOKEN_SECRET
valueFrom:
@@ -37,6 +37,11 @@ spec:
secretKeyRef:
name: secrets
key: GMAIL_APPLICATION_PASSWORD
+ - name: AWS_S3_END_POINT
+ valueFrom:
+ secretKeyRef:
+ name: secrets
+ key: AWS_S3_END_POINT
- name: AWS_ACCESS_KEY
valueFrom:
secretKeyRef:
@@ -47,8 +52,13 @@ spec:
secretKeyRef:
name: secrets
key: AWS_SECRET_KEY
+ - name: MYSQL_ROOT_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: secrets
+ key: MYSQL_ROOT_PASSWORD
- name: MYSQL_USERNAME
- valueFrom:
+ valueFrom:
secretKeyRef:
name: secrets
key: MYSQL_USERNAME
@@ -67,6 +77,11 @@ spec:
secretKeyRef:
name: secrets
key: API_SERVER_URL
+ - name: FLASK_MAIL_SERVER
+ valueFrom:
+ secretKeyRef:
+ name: secrets
+ key: FLASK_MAIL_SERVER
ports:
- containerPort: 8080
resources:
diff --git a/k8s/configs/default.conf b/k8s/configs/default.conf
index b6b6b61b..6d32d6e0 100644
--- a/k8s/configs/default.conf
+++ b/k8s/configs/default.conf
@@ -1,11 +1,19 @@
server {
- listen 80;
+ listen 80;
+ server_tokens off; #nginx 버전 정보 숨기기
- # location / {
- # proxy_pass http://frontend.default.svc.cluster.local:3000;
- # }
+ error_log /tmp/error.log;
+ access_log /tmp/access.log main;
- location /api/ {
- proxy_pass http://backend.default.svc.cluster.local:8080;
- }
-}
+ location / {
+ proxy_pass http://frontend.default.svc.cluster.local:3000;
+ }
+
+ location /api/ {
+ proxy_pass http://backend.default.svc.cluster.local:8080;
+
+ proxy_connect_timeout 60s; # 연결 타임아웃 설정
+ proxy_send_timeout 60s; # 소켓 타임아웃 설정
+ proxy_read_timeout 300s; # 프록시 서버로부터 응답을 읽어들이는 데 허용되는 시간
+ }
+}
\ No newline at end of file
diff --git a/k8s/create-k8s-secret.sh b/k8s/create-k8s-secret.sh
index 561936bf..8e2dd98f 100755
--- a/k8s/create-k8s-secret.sh
+++ b/k8s/create-k8s-secret.sh
@@ -13,13 +13,17 @@ kubectl create secret generic $SECRET_NAME \
--from-literal=TOKEN_SECRET="$TOKEN_SECRET" \
--from-literal=GMAIL_USERNAME="$GMAIL_USERNAME" \
--from-literal=GMAIL_APPLICATION_PASSWORD="$GMAIL_APPLICATION_PASSWORD" \
+ --from-literal=AWS_S3_END_POINT="$AWS_S3_END_POINT" \
--from-literal=AWS_ACCESS_KEY="$AWS_ACCESS_KEY" \
--from-literal=AWS_SECRET_KEY="$AWS_SECRET_KEY" \
+ --from-literal=MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" \
--from-literal=MYSQL_USERNAME="$MYSQL_USERNAME" \
--from-literal=MYSQL_PASSWORD="$MYSQL_PASSWORD" \
--from-literal=DOMAIN="$DOMAIN" \
--from-literal=GOOGLE_MAP_API_KEY="$GOOGLE_MAP_API_KEY" \
- --from-literal=API_SERVER_URL="$API_SERVER_URL"
+ --from-literal=API_SERVER_URL="$API_SERVER_URL" \
+ --from-literal=NEXT_PUBLIC_KAKAOMAP_APPKEY="$NEXT_PUBLIC_KAKAOMAP_APPKEY" \
+ --from-literal=FLASK_MAIL_SERVER="$FLASK_MAIL_SERVER"
echo "Kubernetes secret $SECRET_NAME has been created or updated with the environment variables."
diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml
new file mode 100644
index 00000000..08c2f5c1
--- /dev/null
+++ b/k8s/frontend.yaml
@@ -0,0 +1,29 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: frontend
+spec:
+ selector:
+ matchLabels:
+ app: frontend
+ template:
+ metadata:
+ labels:
+ app: frontend
+ spec:
+ containers:
+ - name: frontend
+ # 여러분의 image 주소를 입력해주세요.
+ image: krmp-d2hub-idock.9rum.cc/dev-test/repo_5a8de3147f88:latest
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: frontend
+spec:
+ ports:
+ - port: 3000
+ targetPort: 3000
+ selector:
+ app: frontend
\ No newline at end of file
diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml
new file mode 100644
index 00000000..227d3f39
--- /dev/null
+++ b/k8s/ingress.yaml
@@ -0,0 +1,18 @@
+apiVersion: networking.k8s.io/v1beta1
+kind: Ingress
+metadata:
+ annotations:
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+ labels:
+ app.kubernetes.io/managed-by: kargocd
+ name: krampoline
+ namespace: default
+spec:
+ rules:
+ - http:
+ paths:
+ - backend:
+ serviceName: frontend
+ servicePort: 3000
+ path: /
+ pathType: Prefix
\ No newline at end of file
diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml
index 03e1b54b..bc305652 100644
--- a/k8s/kustomization.yaml
+++ b/k8s/kustomization.yaml
@@ -4,6 +4,8 @@ resources:
- mysql.yaml
- backend.yaml
- redis.yaml
+ - frontend.yaml
+ #- ingress.yaml
configMapGenerator:
- name: nginx
files:
diff --git a/k8s/mysql.yaml b/k8s/mysql.yaml
index cee00c6b..966e35f7 100644
--- a/k8s/mysql.yaml
+++ b/k8s/mysql.yaml
@@ -20,6 +20,11 @@ spec:
env:
- name: TZ
value: Asia/Seoul
+ - name: MYSQL_ROOT_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: secrets
+ key: MYSQL_ROOT_PASSWORD
- name: MYSQL_USERNAME
valueFrom:
secretKeyRef:
diff --git a/nohup.sh b/nohup.sh
new file mode 100755
index 00000000..e21259e6
--- /dev/null
+++ b/nohup.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# logs 폴더 생성 확인
+mkdir -p ~/logs
+
+# 기존에 실행 중인 Java 프로세스 종료
+PID=$(ps -ef | grep '[j]ava -jar' | awk '{print $2}')
+if [ ! -z "$PID" ]; then
+ echo "기존 Java 프로세스 종료: $PID"
+ kill $PID
+fi
+
+date=`date +%y-%m-%dT%H-%M-%S`
+filePath=~/logs/springboot_nohup.$date.out
+
+# 애플리케이션 실행
+nohup java -jar -Dspring.profiles.active=product build/libs/server-0.0.1.jar >> "$filePath" 2>&1 &
+echo "애플리케이션 실행 중..."
diff --git a/src/main/java/com/bungaebowling/server/_core/config/AwsS3Config.java b/src/main/java/com/bungaebowling/server/_core/config/AwsS3Config.java
index b4bb2845..ccb3b25f 100644
--- a/src/main/java/com/bungaebowling/server/_core/config/AwsS3Config.java
+++ b/src/main/java/com/bungaebowling/server/_core/config/AwsS3Config.java
@@ -1,16 +1,25 @@
package com.bungaebowling.server._core.config;
+import com.amazonaws.ClientConfiguration;
+import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+@Slf4j
@Configuration
public class AwsS3Config {
+ @Value("${cloud.aws.s3.endpoint}")
+ private String endpoint;
+
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@@ -20,7 +29,14 @@ public class AwsS3Config {
@Value("${cloud.aws.region.static}")
private String region;
+ @Value("krmp-proxy.9rum.cc")
+ private String proxyHost;
+
+ @Value("3128")
+ private int proxyPort;
+
@Bean
+ @Profile({"local", "product", "test"})
public AmazonS3 amazonS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
@@ -30,4 +46,27 @@ public AmazonS3 amazonS3Client() {
.withRegion(region)
.build();
}
-}
+
+ @Bean
+ @Profile("deploy")
+ public AmazonS3 amazonS3ClientForDeploy() {
+ AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
+
+ ClientConfiguration clientConfiguration = new ClientConfiguration();
+ clientConfiguration.setConnectionTimeout(60000); // 연결 타임아웃 시간 60000ms = 60s 설정
+ clientConfiguration.setSocketTimeout(60000); // 소켓 타임아웃 시간 60000ms = 60s 설정
+ clientConfiguration.setProxyHost(proxyHost);
+ clientConfiguration.setProxyPort(proxyPort);
+ clientConfiguration.setProxyProtocol(Protocol.HTTP);
+
+ AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(endpoint, null);
+
+ return AmazonS3ClientBuilder
+ .standard()
+ //.withEndpointConfiguration(endpointConfiguration)
+ .withCredentials(new AWSStaticCredentialsProvider(credentials))
+ .withClientConfiguration(clientConfiguration)
+ .withRegion(region)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/bungaebowling/server/_core/config/Configs.java b/src/main/java/com/bungaebowling/server/_core/config/Configs.java
index 9ce662db..76bfdba0 100644
--- a/src/main/java/com/bungaebowling/server/_core/config/Configs.java
+++ b/src/main/java/com/bungaebowling/server/_core/config/Configs.java
@@ -18,9 +18,11 @@ private void setDomain(String value) {
}
- public final static List
CORS = Collections.unmodifiableList(
+ public static final List CORS = Collections.unmodifiableList(
List.of("http://localhost:3000", // 리액트 개발용 3000포트
- "http://127.0.0.1:3000")
+ "http://127.0.0.1:3000",
+ "https://bungae.jagaldol.dev",
+ "https://ka02fa9a0d9a2a.user-app.krampoline.com")
);
public static List getFullCORS() {
diff --git a/src/main/java/com/bungaebowling/server/_core/config/MailConfig.java b/src/main/java/com/bungaebowling/server/_core/config/MailConfig.java
index 6183d0f5..0cc4bb7d 100644
--- a/src/main/java/com/bungaebowling/server/_core/config/MailConfig.java
+++ b/src/main/java/com/bungaebowling/server/_core/config/MailConfig.java
@@ -3,6 +3,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
@@ -20,6 +21,7 @@ public class MailConfig {
private String password;
@Bean
+ @Profile({"local", "product", "test", "deploy"})
public JavaMailSender javaMailService() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
diff --git a/src/main/java/com/bungaebowling/server/_core/config/RestTemplateConfig.java b/src/main/java/com/bungaebowling/server/_core/config/RestTemplateConfig.java
new file mode 100644
index 00000000..b55fcf7f
--- /dev/null
+++ b/src/main/java/com/bungaebowling/server/_core/config/RestTemplateConfig.java
@@ -0,0 +1,37 @@
+package com.bungaebowling.server._core.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+@Configuration
+public class RestTemplateConfig {
+
+ @Value("krmp-proxy.9rum.cc")
+ private String proxyHost;
+
+ @Value("3128")
+ private int proxyPort;
+
+ @Bean
+ @Profile("deploy")
+ public RestTemplate restTemplateForDeploy() {
+ SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+ Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
+ requestFactory.setProxy(proxy);
+
+ return new RestTemplate(requestFactory);
+ }
+
+ @Bean
+ @Profile({"local", "product", "test"})
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+}
diff --git a/src/main/java/com/bungaebowling/server/_core/errors/GlobalExceptionHandler.java b/src/main/java/com/bungaebowling/server/_core/errors/GlobalExceptionHandler.java
index c978717f..133a4119 100644
--- a/src/main/java/com/bungaebowling/server/_core/errors/GlobalExceptionHandler.java
+++ b/src/main/java/com/bungaebowling/server/_core/errors/GlobalExceptionHandler.java
@@ -3,13 +3,11 @@
import com.bungaebowling.server._core.errors.exception.CustomException;
import com.bungaebowling.server._core.errors.exception.ErrorCode;
import com.bungaebowling.server._core.utils.ApiUtils;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
-@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
@@ -19,7 +17,6 @@ public ResponseEntity> customError(CustomException e) {
@ExceptionHandler(Exception.class)
public ResponseEntity> unknownServerError(Exception e) {
- log.error("unknown 에러 발생", e);
var status = HttpStatus.INTERNAL_SERVER_ERROR;
var response = ApiUtils.error(e.getMessage(), ErrorCode.UNKNOWN_SERVER_ERROR);
return ResponseEntity.status(status).body(response);
diff --git a/src/main/java/com/bungaebowling/server/_core/errors/exception/ErrorCode.java b/src/main/java/com/bungaebowling/server/_core/errors/exception/ErrorCode.java
index 66d53588..32b48e5d 100644
--- a/src/main/java/com/bungaebowling/server/_core/errors/exception/ErrorCode.java
+++ b/src/main/java/com/bungaebowling/server/_core/errors/exception/ErrorCode.java
@@ -17,6 +17,7 @@ public enum ErrorCode {
FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제에 실패하였습니다."),
INVALID_FILE_UPLOAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 파일 업로드 요청입니다."),
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "허용되지 않는 파일 확장자입니다."),
+ FILE_REQUEST_FAILED(HttpStatus.BAD_REQUEST, "잘못된 파일 요청입니다."),
SCORE_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "점수 등록에 실패하였습니다."),
SCORE_UPDATE_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "점수 정보에 대한 수정 권한이 없습니다."),
@@ -28,6 +29,7 @@ public enum ErrorCode {
POST_NOT_CLOSE(HttpStatus.FORBIDDEN, "모집글이 마감되지 않았습니다."),
POST_UPDATE_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "모집글에 대한 수정 권한이 없습니다."),
POST_DELETE_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "모집글에 대한 삭제 권한이 없습니다."),
+ POST_DELETE_NOT_ALLOWED(HttpStatus.FORBIDDEN, "마감된 모집글은 삭제할 수 없습니다."),
POST_CLOSE_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "모집글에 대한 마감 권한이 없습니다."),
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 댓글입니다."),
@@ -51,6 +53,7 @@ public enum ErrorCode {
USER_EMAIL_DUPLICATED(HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."),
USER_NAME_DUPLICATED(HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."),
+ USER_UPDATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "프로필 수정에 실패하였습니다."),
WRONG_PASSWORD(HttpStatus.BAD_REQUEST, "기존 비밀번호가 일치하지 않습니다."),
EMAIL_SEND_LIMIT_EXCEEDED(HttpStatus.INTERNAL_SERVER_ERROR, "서버 이메일 전송 한도가 초과되었습니다. 내일 다시 시도해주세요."),
diff --git a/src/main/java/com/bungaebowling/server/_core/utils/AwsS3Service.java b/src/main/java/com/bungaebowling/server/_core/utils/AwsS3Service.java
index 542a8b72..f8e47268 100644
--- a/src/main/java/com/bungaebowling/server/_core/utils/AwsS3Service.java
+++ b/src/main/java/com/bungaebowling/server/_core/utils/AwsS3Service.java
@@ -35,6 +35,13 @@ public class AwsS3Service {
@Value("${spring.servlet.multipart.max-request-size}")
private Long totalFilesMaxSize;
+ private final List allowedExtensions = List.of(
+ ".png",
+ ".gif",
+ ".jpeg",
+ ".jpg"
+ );
+
// 점수 단일 파일용
public String uploadScoreFile(Long userId, Long postId, String category, LocalDateTime time, MultipartFile multipartFile) {
String fileName = CommonUtils.buildScoreFileName(userId, postId, category, time, Objects.requireNonNull(multipartFile.getOriginalFilename()));
@@ -106,6 +113,8 @@ public String getImageAccessUrl(String fileName) {
private void uploadFileToS3(String fileName, MultipartFile multipartFile) {
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(multipartFile.getContentType());
+ objectMetadata.setContentLength(multipartFile.getSize());
+
try (InputStream inputStream = multipartFile.getInputStream()) {
amazonS3Client.putObject(
@@ -133,15 +142,10 @@ private String fileWhiteList(String fileName) {
}
String caseInSensitiveFileName = fileName.toLowerCase();
- if (
- caseInSensitiveFileName.endsWith(".png") ||
- caseInSensitiveFileName.endsWith(".gif") ||
- caseInSensitiveFileName.endsWith(".jpeg") ||
- caseInSensitiveFileName.endsWith(".jpg")
- ) {
- return caseInSensitiveFileName;
- } else {
+ var isNotAllowedExtension = allowedExtensions.stream().noneMatch(caseInSensitiveFileName::endsWith);
+ if (isNotAllowedExtension)
throw new CustomException(ErrorCode.INVALID_FILE_EXTENSION);
- }
+
+ return caseInSensitiveFileName;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/bungaebowling/server/_core/utils/CommonUtils.java b/src/main/java/com/bungaebowling/server/_core/utils/CommonUtils.java
index a0acfbca..4b2ff221 100644
--- a/src/main/java/com/bungaebowling/server/_core/utils/CommonUtils.java
+++ b/src/main/java/com/bungaebowling/server/_core/utils/CommonUtils.java
@@ -1,6 +1,10 @@
package com.bungaebowling.server._core.utils;
+import com.bungaebowling.server._core.errors.exception.CustomException;
+import com.bungaebowling.server._core.errors.exception.ErrorCode;
+
import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
public class CommonUtils {
private static final String FILE_EXTENSION_SEPARATOR = ".";
@@ -10,27 +14,34 @@ public class CommonUtils {
// 점수 등록용
public static String buildScoreFileName(Long userId, Long postId, String category, LocalDateTime time, String originalFileName) {
- int fileExtensionIndex = originalFileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); // 파일 확장자 구분선
+ int fileExtensionIndex = getFileExtensionIndex(originalFileName);
String fileExtension = originalFileName.substring(fileExtensionIndex); // 파일 확장자
- String fileName = originalFileName.substring(0, fileExtensionIndex); // 파일 이름
String now = String.valueOf(time); // 파일 업로드 시간
// 작성자/게시글ID/score/파일명/파일업로드시간.확장자 -> 이런 방식으로 저장됨
- return "user" + WORD_SEPARATOR + userId + CATEGORY_PREFIX + postId + CATEGORY_PREFIX + category + CATEGORY_PREFIX + fileName + TIME_SEPARATOR + now + fileExtension;
+ return "user" + WORD_SEPARATOR + userId + CATEGORY_PREFIX + postId + CATEGORY_PREFIX + category + CATEGORY_PREFIX + now + fileExtension;
}
//프로필 등록
public static String buildProfileFileName(Long userId, String category, LocalDateTime time, String originalFileName) {
- int fileExtensionIndex = originalFileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); // 파일 확장자 구분선
+ int fileExtensionIndex = getFileExtensionIndex(originalFileName);
String fileExtension = originalFileName.substring(fileExtensionIndex); // 파일 확장자
- String fileName = originalFileName.substring(0, fileExtensionIndex); // 파일 이름
String now = String.valueOf(time); // 파일 업로드 시간
//작성자(user_1)/profile/파일명/파일업로드시간.확장자
- return "user" + WORD_SEPARATOR + userId + CATEGORY_PREFIX + category + CATEGORY_PREFIX + fileName + TIME_SEPARATOR + now + fileExtension;
+ return "user" + WORD_SEPARATOR + userId + CATEGORY_PREFIX + category + CATEGORY_PREFIX + now + fileExtension;
+ }
+
+ private static int getFileExtensionIndex(String originalFileName) {
+ int fileExtensionIndex = originalFileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); // 파일 확장자 구분선
+
+ if (fileExtensionIndex == -1) {
+ throw new CustomException(ErrorCode.FILE_REQUEST_FAILED);
+ }
+ return fileExtensionIndex;
}
- // 단일 파일용 -> 이것도 사용 용도에 맞게 custom해서 써야 함
+ // 단일 파일용 Template
public static String buildFileName(String category, String originalFileName) {
int fileExtensionIndex = originalFileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); // 파일 확장자 구분선
String fileExtension = originalFileName.substring(fileExtensionIndex); // 파일 확장자
diff --git a/src/main/java/com/bungaebowling/server/applicant/repository/ApplicantRepository.java b/src/main/java/com/bungaebowling/server/applicant/repository/ApplicantRepository.java
index cfe470ea..66752012 100644
--- a/src/main/java/com/bungaebowling/server/applicant/repository/ApplicantRepository.java
+++ b/src/main/java/com/bungaebowling/server/applicant/repository/ApplicantRepository.java
@@ -1,6 +1,7 @@
package com.bungaebowling.server.applicant.repository;
import com.bungaebowling.server.applicant.Applicant;
+import com.bungaebowling.server.post.Post;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@@ -32,4 +33,8 @@ public interface ApplicantRepository extends JpaRepository {
@Query("SELECT a FROM Applicant a JOIN FETCH a.user u WHERE a.post.id = :postId and a.status = true ORDER BY u.id DESC")
List findAllByPostIdAndStatusTrueOrderByUserIdDesc(@Param("postId") Long postId);
+
+ List findAllByPost(Post post);
+
+ void deleteAllByPost(Post post);
}
\ No newline at end of file
diff --git a/src/main/java/com/bungaebowling/server/comment/repository/CommentRepository.java b/src/main/java/com/bungaebowling/server/comment/repository/CommentRepository.java
index 32dfe8a4..9efa18d3 100644
--- a/src/main/java/com/bungaebowling/server/comment/repository/CommentRepository.java
+++ b/src/main/java/com/bungaebowling/server/comment/repository/CommentRepository.java
@@ -2,6 +2,7 @@
import com.bungaebowling.server.comment.Comment;
+import com.bungaebowling.server.post.Post;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@@ -34,4 +35,6 @@ public interface CommentRepository extends JpaRepository {
@Query("SELECT c FROM Comment c WHERE c.id = :id AND c.post.id = :postId AND c.parent = null")
Optional findByIdAndPostIdAndParentNull(@Param("id") Long id, @Param("postId") Long postId);
+
+ void deleteAllByPost(Post post);
}
diff --git a/src/main/java/com/bungaebowling/server/post/dto/PostResponse.java b/src/main/java/com/bungaebowling/server/post/dto/PostResponse.java
index cf4101a0..e6f31cfd 100644
--- a/src/main/java/com/bungaebowling/server/post/dto/PostResponse.java
+++ b/src/main/java/com/bungaebowling/server/post/dto/PostResponse.java
@@ -113,7 +113,8 @@ public static GetParticipationRecordsDto of(
Map> members,
Map> rates,
Map applicantIdMap,
- Map currentNumberMap
+ Map currentNumberMap,
+ Map districtNameMap
) {
return new GetParticipationRecordsDto(
nextCursorRequest,
@@ -124,7 +125,8 @@ public static GetParticipationRecordsDto of(
members.get(post.getId()),
rates.get(post.getId()),
applicantIdMap.get(post.getId()),
- currentNumberMap.get(post.getId())
+ currentNumberMap.get(post.getId()),
+ districtNameMap.get(post.getId())
)).toList()
);
}
@@ -141,13 +143,21 @@ public record PostDto(
List scores,
List members
) {
- public PostDto(Post post, List scores, List users, List rates, Long applicantId, Long currentNumber) {
+ public PostDto(
+ Post post,
+ List scores,
+ List users,
+ List rates,
+ Long applicantId,
+ Long currentNumber,
+ String districtName
+ ) {
this(
post.getId(),
applicantId,
post.getTitle(),
post.getDueTime(),
- post.getDistrictName(),
+ districtName,
post.getStartTime(),
currentNumber,
post.getIsClose(),
diff --git a/src/main/java/com/bungaebowling/server/post/service/PostService.java b/src/main/java/com/bungaebowling/server/post/service/PostService.java
index f7ff501c..359370d9 100644
--- a/src/main/java/com/bungaebowling/server/post/service/PostService.java
+++ b/src/main/java/com/bungaebowling/server/post/service/PostService.java
@@ -7,6 +7,7 @@
import com.bungaebowling.server.applicant.repository.ApplicantRepository;
import com.bungaebowling.server.city.country.district.District;
import com.bungaebowling.server.city.country.district.repository.DistrictRepository;
+import com.bungaebowling.server.comment.repository.CommentRepository;
import com.bungaebowling.server.post.Post;
import com.bungaebowling.server.post.dto.PostRequest;
import com.bungaebowling.server.post.dto.PostResponse;
@@ -44,6 +45,7 @@ public class PostService {
private final ScoreRepository scoreRepository;
private final ApplicantRepository applicantRepository;
private final UserRateRepository userRateRepository;
+ private final CommentRepository commentRepository;
public static final int DEFAULT_SIZE = 20;
@@ -141,7 +143,9 @@ private List findPosts(CursorRequest cursorRequest, Long cityId, Long coun
public void update(Long userId, Long postId, PostRequest.UpdatePostDto request) {
Post post = findById(postId); // post 찾는 코드 빼서 함수화
-
+ if (post.getIsClose()) {
+ throw new CustomException(ErrorCode.POST_DELETE_NOT_ALLOWED, "마감된 모집글은 수정할 수 없습니다.");
+ }
if (!post.isMine(userId)) {
throw new CustomException(ErrorCode.POST_UPDATE_PERMISSION_DENIED);
}
@@ -161,11 +165,12 @@ public void update(Long userId, Long postId, PostRequest.UpdatePostDto request)
public void delete(Long userId, Long postId) {
Post post = findById(postId); // post 찾는 코드 빼서 함수화
-
+ if (post.getIsClose()) {
+ throw new CustomException(ErrorCode.POST_DELETE_NOT_ALLOWED);
+ }
if (!post.isMine(userId)) {
throw new CustomException(ErrorCode.POST_DELETE_PERMISSION_DENIED);
}
-
deletePost(post);
}
@@ -190,6 +195,7 @@ public PostResponse.GetParticipationRecordsDto getParticipationRecords(CursorReq
Map> rateMap = getRateMap(userId, posts, applicantMap);
Map applicantIdMap = getApplicantIdMap(userId, posts, applicantMap);
Map currentNumberMap = getCurrentNumberMap(posts, applicantMap);
+ Map districtNameMap = getDistrictNameMap(posts);
Long lastKey = getLastKey(posts);
return PostResponse.GetParticipationRecordsDto.of(
@@ -199,7 +205,8 @@ public PostResponse.GetParticipationRecordsDto getParticipationRecords(CursorReq
memberMap,
rateMap,
applicantIdMap,
- currentNumberMap
+ currentNumberMap,
+ districtNameMap
);
}
@@ -274,7 +281,25 @@ private Map getCurrentNumberMap(List posts, Map getDistrictNameMap(List posts) {
+ return posts.stream().collect(Collectors.toMap(
+ Post::getId,
+ post -> {
+ District district = districtRepository.findById(post.getDistrict().getId())
+ .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND));
+ return district.getCountry().getCity().getName() + " " +
+ district.getCountry().getName() + " " +
+ district.getName();
+ }
+ ));
+ }
+
private void deletePost(Post post) { // 삭제 로직 따로 분리
+ List applicants = applicantRepository.findAllByPost(post);
+ applicants.stream().forEach(applicant -> userRateRepository.deleteAllByApplicant(applicant));
+ applicantRepository.deleteAllByPost(post);
+ commentRepository.deleteAllByPost(post);
+ scoreRepository.deleteAllByPost(post);
postRepository.delete(post);
}
diff --git a/src/main/java/com/bungaebowling/server/post/service/PostSpecification.java b/src/main/java/com/bungaebowling/server/post/service/PostSpecification.java
index 127d4224..593de926 100644
--- a/src/main/java/com/bungaebowling/server/post/service/PostSpecification.java
+++ b/src/main/java/com/bungaebowling/server/post/service/PostSpecification.java
@@ -1,8 +1,6 @@
package com.bungaebowling.server.post.service;
import com.bungaebowling.server.applicant.Applicant;
-import com.bungaebowling.server.city.country.Country;
-import com.bungaebowling.server.city.country.district.District;
import com.bungaebowling.server.post.Post;
import com.bungaebowling.server.user.User;
import jakarta.persistence.criteria.*;
@@ -19,9 +17,6 @@ public class PostSpecification {
public static Specification conditionEqual(String condition, Long userId) {
return (root, query, criteriaBuilder) -> {
Join userJoin = root.join("user", JoinType.LEFT);
- Fetch districtFetch = root.fetch("district", JoinType.LEFT);
- Fetch countryFetch = districtFetch.fetch("country", JoinType.LEFT);
- countryFetch.fetch("city", JoinType.LEFT);
Subquery subquery = query.subquery(Long.class);
Root applicantRoot = subquery.from(Applicant.class);
diff --git a/src/main/java/com/bungaebowling/server/score/Score.java b/src/main/java/com/bungaebowling/server/score/Score.java
index be73f9b3..fee99c92 100644
--- a/src/main/java/com/bungaebowling/server/score/Score.java
+++ b/src/main/java/com/bungaebowling/server/score/Score.java
@@ -63,4 +63,8 @@ public void updateWithFile(String resultImageUrl, String accessImageUrl) {
public void updateScoreNum(Integer scoreNum) {
this.scoreNum = scoreNum;
}
+
+ public Boolean isMine(Long userId) { // 내 점수 인지 아닌지 확인
+ return this.user.getId().equals(userId);
+ }
}
diff --git a/src/main/java/com/bungaebowling/server/score/controller/ScoreController.java b/src/main/java/com/bungaebowling/server/score/controller/ScoreController.java
index 1ba42d3d..99025375 100644
--- a/src/main/java/com/bungaebowling/server/score/controller/ScoreController.java
+++ b/src/main/java/com/bungaebowling/server/score/controller/ScoreController.java
@@ -18,14 +18,17 @@ public class ScoreController {
private final ScoreService scoreService;
@GetMapping("/{postId}/scores")
- public ResponseEntity> getScores(@PathVariable Long postId) {
- ScoreResponse.GetScoresDto response = scoreService.readScores(postId);
+ public ResponseEntity> getScores(
+ @PathVariable Long postId,
+ @RequestParam(value = "userId", required = false) Long userId
+ ) {
+ ScoreResponse.GetScoresDto response = scoreService.readScores(postId, userId);
return ResponseEntity.ok().body(ApiUtils.success(response));
}
// multipart/form-data를 처리하고 json을 반환
- @PostMapping(value = "/{postId}/scores", produces = "application/json", consumes = "multipart/form-data")
+ @PostMapping(value = "/{postId}/scores", produces = "application/json", consumes = {"multipart/form-data"})
public ResponseEntity> createScore(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long postId,
@@ -37,7 +40,7 @@ public ResponseEntity> createScore(
return ResponseEntity.ok().body(ApiUtils.success());
}
- @PutMapping(value = "/{postId}/scores/{scoreId}", produces = "application/json", consumes = "multipart/form-data")
+ @PutMapping(value = "/{postId}/scores/{scoreId}", produces = "application/json", consumes = {"multipart/form-data"})
public ResponseEntity> updateScore(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long postId,
@@ -56,7 +59,7 @@ public ResponseEntity> deleteScoreImage(
@PathVariable Long postId,
@PathVariable Long scoreId
) {
- scoreService.deleteImage(userDetails.getId(), postId, scoreId);
+ scoreService.deleteImage(userDetails.getId(), scoreId);
return ResponseEntity.ok(ApiUtils.success());
}
@@ -67,8 +70,8 @@ public ResponseEntity> deleteScore(
@PathVariable Long postId,
@PathVariable Long scoreId
) {
- scoreService.delete(userDetails.getId(), postId, scoreId);
+ scoreService.delete(userDetails.getId(), scoreId);
return ResponseEntity.ok(ApiUtils.success());
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/bungaebowling/server/score/repository/ScoreRepository.java b/src/main/java/com/bungaebowling/server/score/repository/ScoreRepository.java
index 7ef713d5..bd430ccd 100644
--- a/src/main/java/com/bungaebowling/server/score/repository/ScoreRepository.java
+++ b/src/main/java/com/bungaebowling/server/score/repository/ScoreRepository.java
@@ -1,5 +1,6 @@
package com.bungaebowling.server.score.repository;
+import com.bungaebowling.server.post.Post;
import com.bungaebowling.server.score.Score;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@@ -12,8 +13,13 @@
public interface ScoreRepository extends JpaRepository {
List findAllByPostId(Long postId);
+ @Query("SELECT s FROM Score s WHERE s.post.id = :postId AND s.user.id = :userId")
+ List findAllByPostIdAndUserId(Long postId, Long userId);
+
List findAllByUserId(Long userId);
@Query("SELECT s FROM Score s JOIN FETCH s.user u WHERE u.id = :userId and s.post.id = :postId ORDER BY s.id ASC")
List findAllByUserIdAndPostIdOrderById(@Param("userId") Long userId, @Param("postId") Long postId);
+
+ void deleteAllByPost(Post post);
}
\ No newline at end of file
diff --git a/src/main/java/com/bungaebowling/server/score/service/ScoreService.java b/src/main/java/com/bungaebowling/server/score/service/ScoreService.java
index 74919329..c5f5660d 100644
--- a/src/main/java/com/bungaebowling/server/score/service/ScoreService.java
+++ b/src/main/java/com/bungaebowling/server/score/service/ScoreService.java
@@ -17,7 +17,6 @@
import java.time.LocalDateTime;
import java.util.List;
-import java.util.Optional;
@Transactional(readOnly = true)
@RequiredArgsConstructor
@@ -30,13 +29,13 @@ public class ScoreService {
private final UserRepository userRepository;
private final PostRepository postRepository;
- public ScoreResponse.GetScoresDto readScores(Long postId) {
- List scores = findScores(postId);
+ public ScoreResponse.GetScoresDto readScores(Long postId, Long userId) {
+ List scores = findScores(postId, userId);
return ScoreResponse.GetScoresDto.of(scores);
}
- private List findScores(Long postId) {
- return scoreRepository.findAllByPostId(postId);
+ private List findScores(Long postId, Long userId) {
+ return userId == null ? scoreRepository.findAllByPostId(postId) : scoreRepository.findAllByPostIdAndUserId(postId, userId);
}
@Transactional
@@ -102,12 +101,10 @@ private void saveScoreWithImage(Long userId, Post post, Integer scoreNum, Multip
@Transactional
public void update(Long userId, Long postId, Long scoreId, Integer scoreNum, MultipartFile image) {
- Post post = findPostById(postId);
-
- checkPostPermission(userId, post);
-
Score score = findScoreById(scoreId);
+ checkScoreUpdatePermission(userId, score);
+
updateScore(scoreNum, image, postId, userId, score);
}
@@ -143,13 +140,11 @@ private void validateScoreNum(Integer scoreNum) {
}
@Transactional
- public void deleteImage(Long userId, Long postId, Long scoreId) {
- Post post = findPostById(postId);
-
- checkPostPermission(userId, post);
-
+ public void deleteImage(Long userId, Long scoreId) {
Score score = findScoreById(scoreId);
+ checkScoreDeletePermission(userId, score);
+
checkImageExist(score);
deleteScoreImage(score);
@@ -162,22 +157,26 @@ private void deleteScoreImage(Score score) {
}
@Transactional
- public void delete(Long userId, Long postId, Long scoreId) {
- Post post = findPostById(postId);
-
- checkPostPermission(userId, post);
-
+ public void delete(Long userId, Long scoreId) {
Score score = findScoreById(scoreId);
+ checkScoreDeletePermission(userId, score);
+
deleteScore(score);
}
- private void checkPostPermission(Long userId, Post post) {
- if (!post.isMine(userId)) {
+ private void checkScoreDeletePermission(Long userId, Score score) {
+ if (!score.isMine(userId)) {
throw new CustomException(ErrorCode.SCORE_DELETE_PERMISSION_DENIED);
}
}
+ private void checkScoreUpdatePermission(Long userId, Score score) {
+ if (!score.isMine(userId)) {
+ throw new CustomException(ErrorCode.SCORE_UPDATE_PERMISSION_DENIED);
+ }
+ }
+
private void deleteScore(Score score) {
deleteImageIfExist(score);
scoreRepository.delete(score);
@@ -215,4 +214,4 @@ public int findMinScore(List scores) {
.min()
.orElse(0);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/bungaebowling/server/user/controller/UserController.java b/src/main/java/com/bungaebowling/server/user/controller/UserController.java
index db47f2a2..aea51af9 100644
--- a/src/main/java/com/bungaebowling/server/user/controller/UserController.java
+++ b/src/main/java/com/bungaebowling/server/user/controller/UserController.java
@@ -27,7 +27,7 @@
@RequestMapping("/api")
public class UserController {
- final private UserService userService;
+ private final UserService userService;
@PostMapping("/join")
public ResponseEntity> join(@RequestBody @Valid UserRequest.JoinDto requestDto, Errors errors) throws URISyntaxException {
@@ -58,8 +58,10 @@ public ResponseEntity> logout(@AuthenticationPrincipal CustomUserDetails userD
userService.logout(userDetails.getId());
ResponseCookie responseCookie = ResponseCookie.from("refreshToken", "")
- .maxAge(0)
+ .httpOnly(true) // javascript 접근 방지
+ .secure(true) // https 통신 강제
.sameSite("None")
+ .maxAge(0)
.build();
var response = ApiUtils.success();
diff --git a/src/main/java/com/bungaebowling/server/user/dto/UserRequest.java b/src/main/java/com/bungaebowling/server/user/dto/UserRequest.java
index 718e8e9c..5b8ef338 100644
--- a/src/main/java/com/bungaebowling/server/user/dto/UserRequest.java
+++ b/src/main/java/com/bungaebowling/server/user/dto/UserRequest.java
@@ -1,6 +1,7 @@
package com.bungaebowling.server.user.dto;
import com.bungaebowling.server.city.country.district.District;
+import com.bungaebowling.server.user.Role;
import com.bungaebowling.server.user.User;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
diff --git a/src/main/java/com/bungaebowling/server/user/rate/repository/UserRateRepository.java b/src/main/java/com/bungaebowling/server/user/rate/repository/UserRateRepository.java
index 5c2f0455..68e7f725 100644
--- a/src/main/java/com/bungaebowling/server/user/rate/repository/UserRateRepository.java
+++ b/src/main/java/com/bungaebowling/server/user/rate/repository/UserRateRepository.java
@@ -1,5 +1,6 @@
package com.bungaebowling.server.user.rate.repository;
+import com.bungaebowling.server.applicant.Applicant;
import com.bungaebowling.server.user.rate.UserRate;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@@ -13,4 +14,7 @@ public interface UserRateRepository extends JpaRepository {
@Query("SELECT ur FROM UserRate ur JOIN FETCH ur.user u WHERE ur.applicant.id = :applicantId")
List findAllByApplicantId(@Param("applicantId") Long applicantId);
+
+ void deleteAllByApplicant(Applicant applicant);
+
}
diff --git a/src/main/java/com/bungaebowling/server/user/repository/UserRepository.java b/src/main/java/com/bungaebowling/server/user/repository/UserRepository.java
index 7326d219..f2538744 100644
--- a/src/main/java/com/bungaebowling/server/user/repository/UserRepository.java
+++ b/src/main/java/com/bungaebowling/server/user/repository/UserRepository.java
@@ -13,6 +13,8 @@ public interface UserRepository extends JpaRepository {
Optional findByName(String name);
+ Boolean existsByName(String name);
+
List findAllByNameContainingOrderByIdDesc(@Param("name") String name, Pageable pageable);
List findAllByNameContainingAndIdLessThanOrderByIdDesc(@Param("name") String name, @Param("key") Long key, Pageable pageable);
diff --git a/src/main/java/com/bungaebowling/server/user/service/UserService.java b/src/main/java/com/bungaebowling/server/user/service/UserService.java
index f52b96dc..db3e64bb 100644
--- a/src/main/java/com/bungaebowling/server/user/service/UserService.java
+++ b/src/main/java/com/bungaebowling/server/user/service/UserService.java
@@ -22,18 +22,26 @@
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.security.SecureRandom;
import java.time.LocalDateTime;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -57,12 +65,21 @@ public class UserService {
private final PasswordEncoder passwordEncoder;
private final JavaMailSender javaMailSender;
+ private final RestTemplate restTemplate;
private final AwsS3Service awsS3Service;
private final ScoreService scoreService;
+ private final Environment environment;
+
@Value("${bungaebowling.domain}")
private String domain;
+ @Value("${mail.server}")
+ private String mailServer;
+ @Value("${mail.username}")
+ private String username;
+ @Value("${mail.password}")
+ private String password;
@Transactional
public UserResponse.JoinDto join(UserRequest.JoinDto requestDto) {
@@ -147,6 +164,35 @@ public void sendVerificationMail(Long userId) {
String subject = "[번개볼링] 이메일 인증을 완료해주세요.";
String text = "링크를 클릭하여 인증을 완료해주세요!";
+ if (Arrays.asList(environment.getActiveProfiles()).contains("deploy")) {
+ sendMailToMailServer(user, subject, text);
+ } else {
+ sendMail(user, subject, text);
+ }
+ }
+
+ private void sendMailToMailServer(User user, String subject, String text) {
+ try {
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+
+ MultiValueMap requests = new LinkedMultiValueMap<>();
+ requests.add("subject", subject);
+ requests.add("text", text);
+ requests.add("email", user.getEmail());
+ requests.add("username", username);
+ requests.add("password", password);
+
+ HttpEntity> request = new HttpEntity<>(requests, httpHeaders);
+ String requestURL = "http://" + mailServer + "/email";
+
+ restTemplate.postForEntity(requestURL, request, String.class);
+ } catch (Exception e) {
+ throw new CustomException(ErrorCode.EMAIL_SEND_LIMIT_EXCEEDED);
+ }
+ }
+
+ private void sendMail(User user, String subject, String text) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
@@ -210,15 +256,23 @@ public void updateMyProfile(MultipartFile profileImage, String name, Long distri
User user = findUserById(userId);
+ if (userRepository.existsByName(name)) {
+ throw new CustomException(ErrorCode.USER_NAME_DUPLICATED);
+ }
+
District district = districtId == null ? null :
districtRepository.findById(districtId).orElseThrow(
() -> new CustomException(ErrorCode.REGION_NOT_FOUND)
);
- if (profileImage == null) {
- user.updateProfile(name, district, null, null);
- } else {
- updateProfileWithImage(user, name, district, profileImage);
+ try {
+ if (profileImage == null) {
+ user.updateProfile(name, district, null, null);
+ } else {
+ updateProfileWithImage(user, name, district, profileImage);
+ }
+ } catch (Exception e) {
+ throw new CustomException(ErrorCode.USER_UPDATE_FAILED);
}
}
@@ -263,15 +317,10 @@ public void sendVerificationMailForPasswordReset(UserRequest.SendVerificationMai
String subject = "[번개볼링] 비밀번호 초기화 및 임시 비밀번호 발급을 위한 이메일 인증을 완료해주세요.";
String text = "링크를 클릭하여 인증을 완료해주세요!";
- try {
- MimeMessage mimeMessage = javaMailSender.createMimeMessage();
- MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
- helper.setTo(user.getEmail());
- helper.setSubject(subject);
- helper.setText(text, true);
- javaMailSender.send(mimeMessage);
- } catch (Exception e) {
- throw new CustomException(ErrorCode.EMAIL_SEND_LIMIT_EXCEEDED);
+ if (Arrays.asList(environment.getActiveProfiles()).contains("deploy")) {
+ sendMailToMailServer(user, subject, text);
+ } else {
+ sendMail(user, subject, text);
}
}
@@ -285,17 +334,11 @@ public void confirmEmailAndSendTempPassword(UserRequest.ConfirmEmailAndSendTempP
String subject = "[번개볼링] 임시 비밀번호";
String text = "임시 비밀번호는 " + tempPassword + " 입니다.
*비밀번호를 변경해주세요." + "
*기존의 비밀번호는 사용할 수 없습니다.";
- try {
- MimeMessage mimeMessage = javaMailSender.createMimeMessage();
- MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
- helper.setTo(user.getEmail());
- helper.setSubject(subject);
- helper.setText(text, true);
- javaMailSender.send(mimeMessage);
- } catch (Exception e) {
- throw new CustomException(ErrorCode.EMAIL_SEND_LIMIT_EXCEEDED);
+ if (Arrays.asList(environment.getActiveProfiles()).contains("deploy")) {
+ sendMailToMailServer(user, subject, text);
+ } else {
+ sendMail(user, subject, text);
}
-
}
public UserResponse.GetRecordDto getRecords(Long userId) {
@@ -346,8 +389,5 @@ public String getRamdomPassword(int length) {
}
return stringBuilder.toString();
-
}
-
-
}
\ No newline at end of file
diff --git a/src/main/resources/application-deploy.yml b/src/main/resources/application-deploy.yml
index 2d54e8d4..6ca18ac8 100644
--- a/src/main/resources/application-deploy.yml
+++ b/src/main/resources/application-deploy.yml
@@ -43,7 +43,7 @@ logging:
# jwt token config
bungaebowling:
token_exp:
- access: 172800
+ access: 600
refresh: 2592000
secret: ${TOKEN_SECRET}
domain: ${DOMAIN}
@@ -55,6 +55,7 @@ mail:
port: 587
username: ${GMAIL_USERNAME}
password: ${GMAIL_APPLICATION_PASSWORD}
+ server: ${FLASK_MAIL_SERVER}
# aws s3 config
cloud:
@@ -63,6 +64,7 @@ cloud:
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
s3:
+ endpoint: ${AWS_S3_END_POINT}
bucket: bungaebowling-img-s3
region:
static: ap-northeast-2
@@ -73,4 +75,4 @@ cloud:
google:
api:
places:
- key: ${GOOGLE_MAP_API_KEY}
+ key: ${GOOGLE_MAP_API_KEY}
\ No newline at end of file
diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml
index a6af6627..60ebd2f3 100644
--- a/src/main/resources/application-local.yml
+++ b/src/main/resources/application-local.yml
@@ -61,6 +61,7 @@ mail:
port: 587
username: ${GMAIL_USERNAME}
password: ${GMAIL_APPLICATION_PASSWORD}
+ server: flask-mail-server
# aws s3 config
cloud:
@@ -69,6 +70,7 @@ cloud:
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
s3:
+ endpoint: aws-s3-endpoint
bucket: bungaebowling-img-s3
region:
static: ap-northeast-2
diff --git a/src/main/resources/application-product.yml b/src/main/resources/application-product.yml
index 821adfd2..ad18383e 100644
--- a/src/main/resources/application-product.yml
+++ b/src/main/resources/application-product.yml
@@ -49,7 +49,7 @@ logging:
# jwt token config
bungaebowling:
token_exp:
- access: 172800
+ access: 600
refresh: 2592000
secret: ${TOKEN_SECRET}
domain: ${DOMAIN}
@@ -61,6 +61,7 @@ mail:
port: 587
username: ${GMAIL_USERNAME}
password: ${GMAIL_APPLICATION_PASSWORD}
+ server: flask-mail-server
# aws s3 config
cloud:
@@ -69,6 +70,7 @@ cloud:
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
s3:
+ endpoint: aws-s3-endpoint
bucket: bungaebowling-img-s3
region:
static: ap-northeast-2
diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml
index 6065021d..701bf7ec 100644
--- a/src/main/resources/application-test.yml
+++ b/src/main/resources/application-test.yml
@@ -4,6 +4,7 @@ server:
charset: utf-8
force: true
port: 8080
+
spring:
datasource:
url: jdbc:h2:mem:test;MODE=MySQL
@@ -62,6 +63,7 @@ mail:
port: 587
username: bungaebowling55@gmail.com
password: gmailApplicationPassword
+ server: flask-mail-server
# aws s3 config
cloud:
@@ -70,6 +72,7 @@ cloud:
access-key: aws-access-key
secret-key: aws-secret-key
s3:
+ endpoint: aws-s3-endpoint
bucket: bungaebowling-img-s3
region:
static: ap-northeast-2
diff --git a/src/main/resources/test_db/teardown.sql b/src/main/resources/test_db/teardown.sql
index 697bb63a..4bce1b1f 100644
--- a/src/main/resources/test_db/teardown.sql
+++ b/src/main/resources/test_db/teardown.sql
@@ -745,35 +745,36 @@ VALUES ('불금 볼링 점수 내기 하실 분~', 1, 1, '2023-12-01', '2023-11-
('불금 볼링 점수 내기 하실 분~', 4, 2, '2023-08-01', '2023-11-29', '볼링 점수 내기합시다.', false),
('불금 볼링 점수 내기 하실 분~', 4, 2, '2023-08-01', '2023-11-29', '볼링 점수 내기합시다.', true);
-INSERT INTO applicant_tb (user_id, post_id, status)
-VALUES (1, 1, true),
- (1, 2, true),
- (1, 3, true),
- (1, 4, true),
- (1, 5, true),
- (3, 6, true),
- (3, 7, true),
- (3, 8, true),
- (3, 9, true),
- (3, 10, true),
- (4, 11, true),
- (4, 12, true),
- (4, 13, true),
- (4, 14, true),
- (4, 15, true),
+INSERT INTO applicant_tb (id, user_id, post_id, status)
+VALUES (1, 1, 1, true),
+ (2, 1, 2, true),
+ (3, 1, 3, true),
+ (4, 1, 4, true),
+ (5, 1, 5, true),
+ (6, 3, 6, true),
+ (7, 3, 7, true),
+ (8, 3, 8, true),
+ (9, 3, 9, true),
+ (10, 3, 10, true),
+ (11, 4, 11, true),
+ (12, 4, 12, true),
+ (13, 4, 13, true),
+ (14, 4, 14, true),
+ (15, 4, 15, true),
-- 여기까지 자신의 모집글에 자동 신청
- (1, 6, true),
- (1, 7, true),
- (1, 8, false),
- (1, 9, false),
- (1, 10, false),
- (1, 11, false),
- (1, 12, false),
- (1, 13, false),
- (1, 14, false),
- (1, 15, false),
- (3, 2, true),
- (4, 1, true);
+ (16, 1, 6, true),
+ (17, 1, 7, true),
+ (18, 1, 8, false),
+ (19, 1, 9, false),
+ (20, 1, 10, false),
+ (21, 1, 11, false),
+ (22, 1, 12, false),
+ (23, 1, 13, false),
+ (24, 1, 14, false),
+ (25, 1, 15, false),
+ (26, 3, 2, true),
+ (27, 4, 1, true),
+ (28, 4, 3, true);
INSERT INTO comment_tb (id, parent_id, post_id, user_id, content)
VALUES (1, null, 1, null, '삭제된 댓글입니다.'),
@@ -786,45 +787,45 @@ VALUES (1, null, 1, null, '삭제된 댓글입니다.'),
(8, 7, 1, 1, '네 있습니다');
-- 해당 post에 신청 수락되어야함 / 모집완료(is_close)되고 start가 지난 post에만 score 등록 /
-INSERT INTO score_tb (user_id, post_id, score_num)
-VALUES (1, 7, 100),
- (1, 1, 150);
+INSERT INTO score_tb (id, user_id, post_id, score_num, result_image_url)
+VALUES (1, 1, 7, 100, null),
+ (2, 1, 1, 150, 'https://kakao.com');
-- 해당 post에 신청 수락되어야함 / 모집완료(is_close)되고 start가 지난 post에만 score 등록 /
INSERT INTO user_rate_tb(applicant_id, user_id, star_count)
VALUES (1, 4, 5),
(17, 3, 1);
-INSERT INTO message_tb(user_id, opponent_user_id, content, is_receive, is_read)
-VALUES (1, 3, '1번이 3번에게 보낸 쪽지1', false, true),
- (3, 1, '1번이 3번에게 보낸 쪽지1', true, true),
- (1, 3, '1번이 3번에게 보낸 쪽지2', false, true),
- (3, 1, '1번이 3번에게 보낸 쪽지2', true, true),
- (1, 3, '1번이 3번에게 보낸 쪽지3', false, true),
- (3, 1, '1번이 3번에게 보낸 쪽지3', true, true),
- (1, 3, '1번이 3번에게 보낸 쪽지4', false, true),
- (3, 1, '1번이 3번에게 보낸 쪽지4', true, true),
- (1, 3, '1번이 3번에게 보낸 쪽지5', false, true),
- (3, 1, '1번이 3번에게 보낸 쪽지5', true, true),
+INSERT INTO message_tb(id, user_id, opponent_user_id, content, is_receive, is_read)
+VALUES (1, 1, 3, '1번이 3번에게 보낸 쪽지1', false, true),
+ (2, 3, 1, '1번이 3번에게 보낸 쪽지1', true, true),
+ (3, 1, 3, '1번이 3번에게 보낸 쪽지2', false, true),
+ (4, 3, 1, '1번이 3번에게 보낸 쪽지2', true, true),
+ (5, 1, 3, '1번이 3번에게 보낸 쪽지3', false, true),
+ (6, 3, 1, '1번이 3번에게 보낸 쪽지3', true, true),
+ (7, 1, 3, '1번이 3번에게 보낸 쪽지4', false, true),
+ (8, 3, 1, '1번이 3번에게 보낸 쪽지4', true, true),
+ (9, 1, 3, '1번이 3번에게 보낸 쪽지5', false, true),
+ (10, 3, 1, '1번이 3번에게 보낸 쪽지5', true, true),
- (3, 1, '3번이 1번에게 보낸 쪽지1', false, true),
- (1, 3, '3번이 1번에게 보낸 쪽지1', true, false),
- (3, 1, '3번이 1번에게 보낸 쪽지2', false, true),
- (1, 3, '3번이 1번에게 보낸 쪽지2', true, false),
- (3, 1, '3번이 1번에게 보낸 쪽지3', false, true),
- (1, 3, '3번이 1번에게 보낸 쪽지3', true, false),
- (3, 1, '3번이 1번에게 보낸 쪽지4', false, true),
- (1, 3, '3번이 1번에게 보낸 쪽지4', true, false),
- (3, 1, '3번이 1번에게 보낸 쪽지5', false, true),
- (1, 3, '3번이 1번에게 보낸 쪽지5', true, false),
+ (11, 3, 1, '3번이 1번에게 보낸 쪽지1', false, true),
+ (12, 1, 3, '3번이 1번에게 보낸 쪽지1', true, false),
+ (13, 3, 1, '3번이 1번에게 보낸 쪽지2', false, true),
+ (14, 1, 3, '3번이 1번에게 보낸 쪽지2', true, false),
+ (15, 3, 1, '3번이 1번에게 보낸 쪽지3', false, true),
+ (16, 1, 3, '3번이 1번에게 보낸 쪽지3', true, false),
+ (17, 3, 1, '3번이 1번에게 보낸 쪽지4', false, true),
+ (18, 1, 3, '3번이 1번에게 보낸 쪽지4', true, false),
+ (19, 3, 1, '3번이 1번에게 보낸 쪽지5', false, true),
+ (20, 1, 3, '3번이 1번에게 보낸 쪽지5', true, false),
- (4, 3, '4번이 3번에게 보낸 쪽지1', false, true),
- (3, 4, '4번이 3번에게 보낸 쪽지1', true, false),
- (4, 3, '4번이 3번에게 보낸 쪽지2', false, true),
- (3, 4, '4번이 3번에게 보낸 쪽지2', true, false),
- (4, 3, '4번이 3번에게 보낸 쪽지3', false, true),
- (3, 4, '4번이 3번에게 보낸 쪽지3', true, false),
- (4, 3, '4번이 3번에게 보낸 쪽지4', false, true),
- (3, 4, '4번이 3번에게 보낸 쪽지4', true, false),
- (4, 3, '4번이 3번에게 보낸 쪽지5', false, true),
- (3, 4, '4번이 3번에게 보낸 쪽지5', true, false);
\ No newline at end of file
+ (21, 4, 3, '4번이 3번에게 보낸 쪽지1', false, true),
+ (22, 3, 4, '4번이 3번에게 보낸 쪽지1', true, false),
+ (23, 4, 3, '4번이 3번에게 보낸 쪽지2', false, true),
+ (24, 3, 4, '4번이 3번에게 보낸 쪽지2', true, false),
+ (25, 4, 3, '4번이 3번에게 보낸 쪽지3', false, true),
+ (26, 3, 4, '4번이 3번에게 보낸 쪽지3', true, false),
+ (27, 4, 3, '4번이 3번에게 보낸 쪽지4', false, true),
+ (28, 3, 4, '4번이 3번에게 보낸 쪽지4', true, false),
+ (29, 4, 3, '4번이 3번에게 보낸 쪽지5', false, true),
+ (30, 3, 4, '4번이 3번에게 보낸 쪽지5', true, false);
\ No newline at end of file
diff --git a/src/test/java/com/bungaebowling/server/_core/commons/ApiTag.java b/src/test/java/com/bungaebowling/server/_core/commons/ApiTag.java
index d2f3a520..c7549f91 100644
--- a/src/test/java/com/bungaebowling/server/_core/commons/ApiTag.java
+++ b/src/test/java/com/bungaebowling/server/_core/commons/ApiTag.java
@@ -1,12 +1,12 @@
package com.bungaebowling.server._core.commons;
public enum ApiTag {
- AUTHORIZATION("회원가입 로그인 인증"),
+ AUTHORIZATION("회원가입, 로그인, 인증"),
CITY("행정 구역"),
POST("모집글"),
- APPLICANT("신청"),
+ APPLICANT("신청, 별점"),
COMMENT("댓글"),
- USER("개인 프로필/정보"),
+ USER("개인 프로필, 정보"),
RECORD("참여 기록"),
SCORE("볼링 점수(스코어)"),
MESSAGE("쪽지");
diff --git a/src/test/java/com/bungaebowling/server/applicant/controller/ApplicantControllerTest.java b/src/test/java/com/bungaebowling/server/applicant/controller/ApplicantControllerTest.java
new file mode 100644
index 00000000..f951decc
--- /dev/null
+++ b/src/test/java/com/bungaebowling/server/applicant/controller/ApplicantControllerTest.java
@@ -0,0 +1,432 @@
+package com.bungaebowling.server.applicant.controller;
+
+import com.bungaebowling.server.ControllerTestConfig;
+import com.bungaebowling.server._core.commons.ApiTag;
+import com.bungaebowling.server._core.commons.GeneralApiResponseSchema;
+import com.bungaebowling.server._core.commons.GeneralParameters;
+import com.bungaebowling.server._core.security.JwtProvider;
+import com.bungaebowling.server.applicant.dto.ApplicantRequest;
+import com.bungaebowling.server.user.Role;
+import com.bungaebowling.server.user.User;
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.epages.restdocs.apispec.Schema;
+import com.epages.restdocs.apispec.SimpleType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.web.context.WebApplicationContext;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.*;
+import static org.hamcrest.Matchers.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@ActiveProfiles(value = {"test"})
+@Sql(value = "classpath:test_db/teardown.sql", config = @SqlConfig(encoding = "UTF-8"))
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+class ApplicantControllerTest extends ControllerTestConfig {
+
+ @Autowired
+ public ApplicantControllerTest(WebApplicationContext context, ObjectMapper om) {
+ super(context, om);
+ }
+
+ @Test
+ @DisplayName("신청자 목록 조회 테스트")
+ void getApplicants() throws Exception {
+ // given
+ Long postId = 1L;
+ int size = 2;
+ int key = 30;
+
+ var userId = 1L; // 김볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/posts/{postId}/applicants", postId)
+ .param("key", Integer.toString(key))
+ .param("size", Integer.toString(size))
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.nextCursorRequest").exists(),
+ jsonPath("$.response.applicantNumber").isNumber(),
+ jsonPath("$.response.applicants[0].id").isNumber(),
+ jsonPath("$.response.applicants[0].user.id").isNumber(),
+ jsonPath("$.response.applicants[0].user.name").exists(),
+ jsonPath("$.response.applicants[0].user.profileImage").hasJsonPath(),
+ jsonPath("$.response.applicants[0].user.rating").isNumber(),
+ jsonPath("$.response.applicants[0].status").isBoolean(),
+ jsonPath("$.response.applicants[0].id").value(lessThan(key)),
+ jsonPath("$.response.applicants").value(hasSize(lessThanOrEqualTo(size)))
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[applicant] getApplicants",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("신청자 목록 조회")
+ .description("""
+ 모집글의 신청자 목록을 조회합니다.
+ """)
+ .tag(ApiTag.APPLICANT.getTagName())
+ .pathParameters(parameterWithName("postId").description("조회할 모집글 id"))
+ .queryParameters(
+ GeneralParameters.CURSOR_KEY.getParameterDescriptorWithType(),
+ GeneralParameters.SIZE.getParameterDescriptorWithType()
+ )
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .responseSchema(Schema.schema("신청자 목록 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.NEXT_CURSOR.getResponseDescriptor().and(
+ fieldWithPath("response.applicantNumber").description("총 신청자의 수"),
+ fieldWithPath("response.applicants[].id").description("신청의 ID(PK)"),
+ fieldWithPath("response.applicants[].user.id").description("신청자의 ID(PK)"),
+ fieldWithPath("response.applicants[].user.name").description("신청자의 닉네임"),
+ fieldWithPath("response.applicants[].user.profileImage").optional().type(SimpleType.STRING).description("신청자의 프로필 이미지 경로"),
+ fieldWithPath("response.applicants[].user.rating").description("신청자의 별점"),
+ fieldWithPath("response.applicants[].status").description("신청 수락 여부 | true: 수락, false: 수락 대기")
+ )
+ )
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집글에 대한 신청 테스트")
+ void create() throws Exception {
+ // given
+ Long postId = 2L;
+
+ var userId = 4L; // 박볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .post("/api/posts/{postId}/applicants", postId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[applicant] create",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글에 대한 신청")
+ .description("""
+ 마감되지 않은 모집글에 신청을 합니다.
+ """)
+ .tag(ApiTag.APPLICANT.getTagName())
+ .pathParameters(parameterWithName("postId").description("신청할 모집글 id"))
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집자의 신청 수락 테스트")
+ void accept() throws Exception {
+ // given
+ Long postId = 8L;
+
+ Long applicantId = 18L;
+
+ var userId = 3L; // 이볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ var requestDto = new ApplicantRequest.UpdateDto(true);
+ String requestBody = om.writeValueAsString(requestDto);
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .put("/api/posts/{postId}/applicants/{applicantId}", postId, applicantId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[applicant] accept",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집자의 신청 수락")
+ .description("""
+ 모집자가 신청을 승낙 할 수 있습니다.
+
+ status를 수정 가능합니다. true: 승낙 / false: 승낙 대기
+
+ 거절은 DELETE 요청으로 신청을 완전히 삭제 해주시길 바랍니다.
+ """)
+ .tag(ApiTag.APPLICANT.getTagName())
+ .pathParameters(
+ parameterWithName("postId").description("모집글 id"),
+ parameterWithName("applicantId").description("수락할 신청의 Id")
+ )
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("모집자의 신청 수락 요청 DTO"))
+ .requestFields(fieldWithPath("status").type(SimpleType.BOOLEAN).description("변경하고자 하는 신청의 상태 | true: 승낙, false: 승낙 대기"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집자의 신청 거절 테스트")
+ void reject() throws Exception {
+ // given
+ Long postId = 8L;
+
+ Long applicantId = 18L;
+
+ var userId = 3L; // 이볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .delete("/api/posts/{postId}/applicants/{applicantId}", postId, applicantId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[applicant] reject",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집자의 신청 거절")
+ .description("""
+ 모집자가 신청을 거절 합니다.
+ """)
+ .tag(ApiTag.APPLICANT.getTagName())
+ .pathParameters(
+ parameterWithName("postId").description("모집글 id"),
+ parameterWithName("applicantId").description("거절할 신청의 Id")
+ )
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("자신의 신청 상태 조회 테스트")
+ void checkStatus() throws Exception {
+ // given
+ Long postId = 1L;
+
+ var userId = 4L; // 박볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/posts/{postId}/applicants/check-status", postId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.applicantId").isNumber(),
+ jsonPath("$.response.isApplied").isBoolean(),
+ jsonPath("$.response.isAccepted").isBoolean()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[applicant] checkStatus",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("자신의 신청 상태 조회")
+ .description("""
+ 해당 모집글에 대한 자신의 신청 상태를 확인 합니다.
+ """)
+ .tag(ApiTag.APPLICANT.getTagName())
+ .pathParameters(parameterWithName("postId").description("조회할 모집글 id"))
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .responseSchema(Schema.schema("자신의 신청 상태 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.SUCCESS.getResponseDescriptor().and(
+ fieldWithPath("response.applicantId").optional().type(SimpleType.NUMBER).description("신청의 ID(PK)"),
+ fieldWithPath("response.isApplied").description("신청 상태(true: 신청됨 / false: 신청되지 않음"),
+ fieldWithPath("response.isAccepted").description("승낙 상태(true: 승낙됨 / false: 승낙되지 않음")
+ )
+ )
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("참여자에게 별점 등록 테스트")
+ void rateUser() throws Exception {
+ // given
+ Long postId = 3L;
+
+ Long applicantId = 28L;
+
+ var userId = 4L; // 박볼링
+
+ var targetId = 1L;
+
+ var rating = 4;
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ var requestDto = new ApplicantRequest.RateDto(targetId, rating);
+ String requestBody = om.writeValueAsString(requestDto);
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .post("/api/posts/{postId}/applicants/{applicantId}/rating", postId, applicantId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[applicant] rateUser",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("참여자에게 별점 등록")
+ .description("""
+ 같은 모집글에 참여한 사람들에게 별점을 등록할 수 있습니다.
+
+ 별점은 모집완료(모집글의 is_close가 true) 후 게임 시작 시간(start_time)이 지나야 등록 가능합니다.
+ """)
+ .tag(ApiTag.APPLICANT.getTagName())
+ .pathParameters(
+ parameterWithName("postId").description("모집글 id"),
+ parameterWithName("applicantId").description("자신의 신청 id")
+ )
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("참여자에게 별점 등록 요청 DTO"))
+ .requestFields(
+ fieldWithPath("targetId").type(SimpleType.NUMBER).description("평가 대상 사용자의 id(PK)"),
+ fieldWithPath("rating").type(SimpleType.NUMBER).description("별점 | 1 ~ 5 범위 가능")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/bungaebowling/server/comment/controller/CommentControllerTest.java b/src/test/java/com/bungaebowling/server/comment/controller/CommentControllerTest.java
index c5d9950f..05da518d 100644
--- a/src/test/java/com/bungaebowling/server/comment/controller/CommentControllerTest.java
+++ b/src/test/java/com/bungaebowling/server/comment/controller/CommentControllerTest.java
@@ -174,6 +174,7 @@ void getCommentsWithPage() throws Exception {
GeneralParameters.CURSOR_KEY.getParameterDescriptorWithType(),
GeneralParameters.SIZE.getParameterDescriptorWithType()
)
+ .responseSchema(Schema.schema("댓글 조회 응답 DTO"))
.build()
)
)
diff --git a/src/test/java/com/bungaebowling/server/message/controller/MessageControllerTest.java b/src/test/java/com/bungaebowling/server/message/controller/MessageControllerTest.java
new file mode 100644
index 00000000..91f9537d
--- /dev/null
+++ b/src/test/java/com/bungaebowling/server/message/controller/MessageControllerTest.java
@@ -0,0 +1,351 @@
+package com.bungaebowling.server.message.controller;
+
+import com.bungaebowling.server.ControllerTestConfig;
+import com.bungaebowling.server._core.commons.ApiTag;
+import com.bungaebowling.server._core.commons.GeneralApiResponseSchema;
+import com.bungaebowling.server._core.commons.GeneralParameters;
+import com.bungaebowling.server._core.security.JwtProvider;
+import com.bungaebowling.server.message.dto.MessageRequest;
+import com.bungaebowling.server.user.Role;
+import com.bungaebowling.server.user.User;
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.epages.restdocs.apispec.Schema;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.web.context.WebApplicationContext;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@ActiveProfiles(value = {"test"})
+@Sql(value = "classpath:test_db/teardown.sql", config = @SqlConfig(encoding = "UTF-8"))
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+public class MessageControllerTest extends ControllerTestConfig {
+
+ @Autowired
+ public MessageControllerTest(WebApplicationContext context, ObjectMapper om) {
+ super(context, om);
+ }
+
+
+ @Test
+ @DisplayName("대화방(쪽지) 목록 조회 테스트")
+ void getOpponents() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+
+ int size = 20;
+ int key = 30;
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/messages/opponents")
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .param("key", Integer.toString(key))
+ .param("size", Integer.toString(size))
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.messages[0].opponentUserId").isNumber(),
+ jsonPath("$.response.messages[0].opponentUserName").exists(),
+ jsonPath("$.response.messages[0].recentMessage").exists(),
+ jsonPath("$.response.messages[0].recentTime").exists(),
+ jsonPath("$.response.messages[0].countNew").isNumber()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[message] getOpponents",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("대화방(쪽지) 목록 조회")
+ .description("""
+ 대화 목록를 조회합니다.
+ """)
+ .tag(ApiTag.MESSAGE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .queryParameters(
+ GeneralParameters.CURSOR_KEY.getParameterDescriptorWithType(),
+ GeneralParameters.SIZE.getParameterDescriptorWithType()
+ )
+ .responseSchema(Schema.schema("대화방 목록 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.NEXT_CURSOR.getResponseDescriptor().and(
+ fieldWithPath("response.messages[].opponentUserId").description("쪽지 상대 유저 ID"),
+ fieldWithPath("response.messages[].opponentUserName").description("쪽지 상대 유저 이름"),
+ fieldWithPath("response.messages[].opponentUserProfileImage").description("상대 프로필 사진 경로 | 사진이 없을 경우 null"),
+ fieldWithPath("response.messages[].recentMessage").description("쪽지 상대와의 가장 최근 메시지 내용"),
+ fieldWithPath("response.messages[].recentTime").description("쪽지 상대와의 가장 최근 송수신 시각"),
+ fieldWithPath("response.messages[].countNew").description("안 앍은 메시지의 수")
+ )
+ )
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("일대일 대화방 쪽지 조회 테스트")
+ void getMessagesAndUpdateToRead() throws Exception {
+ // given
+ Long userId = 1L;
+ Long opponentId = 3L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+
+ int size = 20;
+ int key = 30;
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/messages/opponents/{opponentId}", opponentId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .param("key", Integer.toString(key))
+ .param("size", Integer.toString(size))
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.opponentUserName").exists(),
+ jsonPath("$.response.messages[0].id").exists(),
+ jsonPath("$.response.messages[0].content").exists(),
+ jsonPath("$.response.messages[0].time").exists(),
+ jsonPath("$.response.messages[0].isRead").isBoolean(),
+ jsonPath("$.response.messages[0].isReceive").isBoolean()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[message] getMessagesAndUpdateToRead",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("일대일 대화방 쪽지 조회")
+ .description("""
+ 상대 유저와의 쪽지 목록을 조회합니다.
+ """)
+ .tag(ApiTag.MESSAGE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .queryParameters(
+ GeneralParameters.CURSOR_KEY.getParameterDescriptorWithType(),
+ GeneralParameters.SIZE.getParameterDescriptorWithType()
+ )
+ .pathParameters(
+ parameterWithName("opponentId").description("쪽지를 조회할 상대 유저의 Id")
+ )
+ .responseSchema(Schema.schema("일대일 대화방 쪽지 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.NEXT_CURSOR.getResponseDescriptor().and(
+ fieldWithPath("response.opponentUserName").description("쪽지 상대 유저 이름"),
+ fieldWithPath("response.opponentUserProfileImage").description("상대 프로필 사진 경로 | 사진이 없을 경우 null"),
+ fieldWithPath("response.messages[].id").description("쪽지 ID"),
+ fieldWithPath("response.messages[].content").description("쪽지 내용"),
+ fieldWithPath("response.messages[].time").description("쪽지 송신시간"),
+ fieldWithPath("response.messages[].isRead").description("쪽지대상이 읽었는지"),
+ fieldWithPath("response.messages[].isReceive").description("내가 받은 쪽지인지")
+ )
+ )
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("쪽지 보내기 테스트")
+ void sendMessage() throws Exception {
+ // given
+ Long userId = 1L;
+ Long opponentUserId = 3L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ MessageRequest.SendMessageDto requestDto = new MessageRequest.SendMessageDto("쪽지보내기 테스트");
+ String requestBody = om.writeValueAsString(requestDto);
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .post("/api/messages/opponents/{opponentId}", opponentUserId)
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[message] sendMessage",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("쪽지 보내기")
+ .description("""
+ 쪽지를 보냅니다.
+ """)
+ .tag(ApiTag.MESSAGE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("쪽지 보내기 요청 DTO"))
+ .requestFields(fieldWithPath("content").description("송신할 쪽지 내용"))
+ .pathParameters(
+ parameterWithName("opponentId").description("쪽지를 수신할 유저의 Id")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("쪽지함 삭제 테스트")
+ void deleteMessagesByOpponentId() throws Exception {
+ // given
+ Long userId = 1L;
+ Long opponentId = 3L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .delete("/api/messages/opponents/{opponentId}", opponentId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[message] deleteMessagesByOpponentId",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("쪽지함 삭제")
+ .description("""
+ 해당유저와의 모든 쪽지를 삭제합니다.(쪽지함을 삭제합니다.)
+ """)
+ .tag(ApiTag.MESSAGE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(
+ parameterWithName("opponentId").description("쪽지를 삭제할 대화 상대의 Id")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("쪽지 개별 삭제")
+ void deleteMessageById() throws Exception {
+ // given
+ Long userId = 1L;
+ Long messageId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .delete("/api/messages/{messageId}", messageId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[message] deleteMessageById",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("쪽지 개별 삭제")
+ .description("""
+ 쪽지를 삭제합니다.
+ """)
+ .tag(ApiTag.MESSAGE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(
+ parameterWithName("messageId").description("삭제할 쪽지 id")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+}
diff --git a/src/test/java/com/bungaebowling/server/message/MessageRepositoryTest.java b/src/test/java/com/bungaebowling/server/message/repository/MessageRepositoryTest.java
similarity index 98%
rename from src/test/java/com/bungaebowling/server/message/MessageRepositoryTest.java
rename to src/test/java/com/bungaebowling/server/message/repository/MessageRepositoryTest.java
index f5d1fe12..36937f27 100644
--- a/src/test/java/com/bungaebowling/server/message/MessageRepositoryTest.java
+++ b/src/test/java/com/bungaebowling/server/message/repository/MessageRepositoryTest.java
@@ -1,6 +1,6 @@
-package com.bungaebowling.server.message;
+package com.bungaebowling.server.message.repository;
-import com.bungaebowling.server.message.repository.MessageRepository;
+import com.bungaebowling.server.message.Message;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
diff --git a/src/test/java/com/bungaebowling/server/post/controller/PostControllerTest.java b/src/test/java/com/bungaebowling/server/post/controller/PostControllerTest.java
new file mode 100644
index 00000000..d98eaab2
--- /dev/null
+++ b/src/test/java/com/bungaebowling/server/post/controller/PostControllerTest.java
@@ -0,0 +1,535 @@
+package com.bungaebowling.server.post.controller;
+
+import com.bungaebowling.server.ControllerTestConfig;
+import com.bungaebowling.server._core.commons.ApiTag;
+import com.bungaebowling.server._core.commons.GeneralApiResponseSchema;
+import com.bungaebowling.server._core.commons.GeneralParameters;
+import com.bungaebowling.server._core.security.JwtProvider;
+import com.bungaebowling.server.post.dto.PostRequest;
+import com.bungaebowling.server.user.Role;
+import com.bungaebowling.server.user.User;
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.epages.restdocs.apispec.Schema;
+import com.epages.restdocs.apispec.SimpleType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.joda.time.DateTime;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.time.LocalDateTime;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@ActiveProfiles(value = {"test"})
+@Sql(value = "classpath:test_db/teardown.sql", config = @SqlConfig(encoding = "UTF-8"))
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+class PostControllerTest extends ControllerTestConfig {
+
+ @Autowired
+ public PostControllerTest(WebApplicationContext context, ObjectMapper om) {
+ super(context, om);
+ }
+
+ @Test
+ @DisplayName("모집글 목록 조회")
+ void getPosts() throws Exception {
+ //given
+ int size = 20;
+ int key = 30;
+ Long cityId = 1L;
+ Long countryId = 1L;
+ Long districtId = 1L;
+ Boolean all = Boolean.TRUE;
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/posts")
+ .param("key", Integer.toString(key))
+ .param("size", Integer.toString(size))
+ .param("cityId", Long.toString(cityId))
+ .param("countryId", Long.toString(countryId))
+ .param("districtId", Long.toString(districtId))
+ .param("all", Boolean.toString(all))
+
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.nextCursorRequest.key").isNumber(),
+ jsonPath("$.response.nextCursorRequest.size").isNumber(),
+ jsonPath("$.response.posts[0].id").isNumber(),
+ jsonPath("$.response.posts[0].title").exists(),
+ jsonPath("$.response.posts[0].dueTime").exists(),
+ jsonPath("$.response.posts[0].districtName").exists(),
+ jsonPath("$.response.posts[0].startTime").exists(),
+ jsonPath("$.response.posts[0].userName").exists(),
+ jsonPath("$.response.posts[0].currentNumber").isNumber(),
+ jsonPath("$.response.posts[0].isClose").isBoolean()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] getPosts",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글 목록 조회")
+ .description("""
+ 모집글 목록를 조회합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .queryParameters(
+ GeneralParameters.CURSOR_KEY.getParameterDescriptorWithType(),
+ GeneralParameters.SIZE.getParameterDescriptorWithType(),
+ parameterWithName("cityId").optional().type(SimpleType.NUMBER).description("시/도 ID (넘겨주지 않을 시 설정 시/도 설정 안 한 것)"),
+ parameterWithName("countryId").optional().type(SimpleType.NUMBER).description("시/군/구 ID (넘겨주지 않을 시 설정 시/군/구 설정 안 한 것)"),
+ parameterWithName("districtId").optional().type(SimpleType.NUMBER).description("읍/면/동 ID (넘겨주지 않을 시 설정 읍/면/동 설정 안 한 것)"),
+ parameterWithName("all").type(SimpleType.BOOLEAN).defaultValue(true).optional().description("전체 보기/모집 중 선택")
+
+ )
+ .responseSchema(Schema.schema("모집글 목록 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.NEXT_CURSOR.getResponseDescriptor().and(
+ fieldWithPath("response.posts[].id").description("조회된 모집글 ID"),
+ fieldWithPath("response.posts[].title").description("조회된 모집글 제목 "),
+ fieldWithPath("response.posts[].dueTime").description("조회된 모집글 모집 마감기한"),
+ fieldWithPath("response.posts[].districtName").description("조회된 모집글 행정구역"),
+ fieldWithPath("response.posts[].startTime").description("조회된 모집글 게임 예정 일시"),
+ fieldWithPath("response.posts[].userName").description("조회된 모집글 작성자 이름 "),
+ fieldWithPath("response.posts[].profileImage").optional().type(SimpleType.STRING).description("조회된 모집글 작성자 프로필 사진 경로 | 사진이 없을 경우 null"),
+ fieldWithPath("response.posts[].currentNumber").description("조회된 모집글 참석 인원"),
+ fieldWithPath("response.posts[].isClose").description("모집글 마감 여부")
+
+ )
+ )
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집글 상세 조회")
+ void getPost() throws Exception {
+ // given
+ Long postId = 1L;
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/posts/{postId}", postId)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.post.id").isNumber(),
+ jsonPath("$.response.post.title").exists(),
+ jsonPath("$.response.post.userId").isNumber(),
+ jsonPath("$.response.post.userName").exists(),
+ jsonPath("$.response.post.districtName").exists(),
+ jsonPath("$.response.post.currentNumber").isNumber(),
+ jsonPath("$.response.post.content").exists(),
+ jsonPath("$.response.post.startTime").exists(),
+ jsonPath("$.response.post.dueTime").exists(),
+ jsonPath("$.response.post.viewCount").isNumber(),
+ jsonPath("$.response.post.createdAt").exists(),
+ jsonPath("$.response.post.editedAt").exists(),
+ jsonPath("$.response.post.isClose").isBoolean()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] getPost",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글 상세 조회")
+ .description("""
+ 모집글의 상세 정보를 조회합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .pathParameters(
+ parameterWithName("postId").type(SimpleType.NUMBER).description("조회할 모집글 ID")
+ )
+ .responseSchema(Schema.schema("모집글 상세 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.SUCCESS.getResponseDescriptor().and(
+ fieldWithPath("response.post.id").description("모집글 ID"),
+ fieldWithPath("response.post.title").description("모집글 제목"),
+ fieldWithPath("response.post.userId").description("모집글 작성자 ID"),
+ fieldWithPath("response.post.userName").description("모집글 작성자 이름"),
+ fieldWithPath("response.post.profileImage").optional().type(SimpleType.NUMBER).description("조회된 모집글 작성자 프로필 사진 경로 | 사진이 없을 경우 null"),
+ fieldWithPath("response.post.districtName").description("모집글 행정 구역"),
+ fieldWithPath("response.post.currentNumber").description("현재 모집 확정 인원 수"),
+ fieldWithPath("response.post.content").description("모집글 내용"),
+ fieldWithPath("response.post.startTime").description("게임 예정 일시"),
+ fieldWithPath("response.post.dueTime").description("모집 마감기한"),
+ fieldWithPath("response.post.viewCount").description("조회 수"),
+ fieldWithPath("response.post.createdAt").description("모집글 생성 시간"),
+ fieldWithPath("response.post.editedAt").description("모집글 수정 시간 "),
+ fieldWithPath("response.post.isClose").description("모집글 마감 여부")
+ ))
+ .build()
+ )
+ )
+ );
+
+ }
+
+ @Test
+ @DisplayName("참여 기록 조회")
+ void getUserParticipationRecords() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ int size = 20;
+ int key = 30;
+ Long cityId = 1L;
+ String condition = "all";
+ String status = "all";
+ DateTime start = DateTime.now().minusMonths(3);
+ DateTime end = DateTime.now();
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/posts/users/{userId}/participation-records", userId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .param("key", Integer.toString(key))
+ .param("size", Integer.toString(size))
+ .param("condition", condition)
+ .param("status", status)
+ .param("cityId", Long.toString(cityId))
+ .param("start", start.toString("yyyy-MM-dd"))
+ .param("end", end.toString("yyyy-MM-dd"))
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.nextCursorRequest.key").isNumber(),
+ jsonPath("$.response.nextCursorRequest.size").isNumber(),
+ jsonPath("$.response.posts[0].id").isNumber(),
+ jsonPath("$.response.posts[0].applicantId").isNumber(),
+ jsonPath("$.response.posts[0].title").exists(),
+ jsonPath("$.response.posts[0].dueTime").exists(),
+ jsonPath("$.response.posts[0].districtName").exists(),
+ jsonPath("$.response.posts[0].startTime").exists(),
+ jsonPath("$.response.posts[0].currentNumber").isNumber(),
+ jsonPath("$.response.posts[0].isClose").isBoolean(),
+ jsonPath("$.response.posts[0].scores").exists(),
+ jsonPath("$.response.posts[0].members[0].id").exists()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] getUserParticipationRecords",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("참여 기록 조회")
+ .description("""
+ 참여 기록을 조회합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(
+ parameterWithName("userId").type(SimpleType.NUMBER).description("조회할 유저의 ID")
+ )
+ .queryParameters(
+ GeneralParameters.CURSOR_KEY.getParameterDescriptorWithType(),
+ GeneralParameters.SIZE.getParameterDescriptorWithType(),
+ parameterWithName("condition").optional().type(SimpleType.STRING).defaultValue("all").description("모집글 유형 (종류 : 전체 보기 - all, 작성한 글 - created, 참여한 글 - participated)"),
+ parameterWithName("status").optional().type(SimpleType.STRING).defaultValue("all").description("모집글 상태 (종류 : 전체 보기 - all, 모집중 - open, 모집 완료 - closed)"),
+ parameterWithName("cityId").optional().type(SimpleType.NUMBER).description("모집 장소 (시/군/구)"),
+ parameterWithName("start").optional().type(SimpleType.STRING).defaultValue(start.toString("yyyy-MM-dd")).description("조회 시작일자, 기본 값: 3개월 전"),
+ parameterWithName("end").optional().type(SimpleType.STRING).defaultValue(end.toString("yyyy-MM-dd")).description("조회 종료일자, 기본 값: 현재 날짜")
+ )
+ .responseSchema(Schema.schema("참여 기록 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.NEXT_CURSOR.getResponseDescriptor().and(
+ fieldWithPath("response.posts[].id").description("모집글 ID"),
+ fieldWithPath("response.posts[].applicantId").description("조회하고자 하는 유저의 ID가 모집글에 신청한 ID"),
+ fieldWithPath("response.posts[].title").description("모집글 제목 "),
+ fieldWithPath("response.posts[].dueTime").description("게임 마감 일시 "),
+ fieldWithPath("response.posts[].districtName").description("지역"),
+ fieldWithPath("response.posts[].startTime").description("게임 예정 일시 "),
+ fieldWithPath("response.posts[].currentNumber").description("현재 모집 확정 인원 수"),
+ fieldWithPath("response.posts[].isClose").description("모집글 마감 여부"),
+ fieldWithPath("response.posts[].scores").description("스코어 정보"),
+ fieldWithPath("response.posts[].scores[].id").optional().type(SimpleType.NUMBER).description("스코어 ID"),
+ fieldWithPath("response.posts[].scores[].score").optional().type(SimpleType.NUMBER).description("등록된 사용자 스코어"),
+ fieldWithPath("response.posts[].scores[].scoreImage").optional().type(SimpleType.STRING).description("등록된 사용자 스코어 사진 경로 | 사진이 없을 경우 null"),
+ fieldWithPath("response.posts[].members").description("모집 멤버 정보"),
+ fieldWithPath("response.posts[].members[].id").optional().type(SimpleType.NUMBER).description("모집 멤버 ID"),
+ fieldWithPath("response.posts[].members[].name").optional().type(SimpleType.STRING).description("모집 멤버 이름"),
+ fieldWithPath("response.posts[].members[].profileImage").optional().type(SimpleType.STRING).description("모집 멤버 프로필 사진 경로 | 사진이 없을 경우 null"),
+ fieldWithPath("response.posts[].members[].isRated").optional().type(SimpleType.BOOLEAN).description("모집 멤버 별점 입력 여부")
+ ))
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집글 등록")
+ void createPost() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ PostRequest.CreatePostDto requestDto = new PostRequest.CreatePostDto("테스트", LocalDateTime.now().plusDays(2), LocalDateTime.now().plusDays(1), "테스트", 1L);
+ String requestBody = om.writeValueAsString(requestDto);
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .post("/api/posts")
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200),
+ jsonPath("$.response.id").isNumber()
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] createPost",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글 등록")
+ .description("""
+ 모집글을 등록합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("모집글 등록 요청 DTO"))
+ .requestFields(
+ fieldWithPath("title").description("모집글 제목"),
+ fieldWithPath("districtId").description("모집글 행정구역 ID"),
+ fieldWithPath("startTime").description("게임 예정 일시"),
+ fieldWithPath("dueTime").description("모집 마감기한"),
+ fieldWithPath("content").description("모집글 내용")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.CREATED.getName()))
+ .responseFields(GeneralApiResponseSchema.CREATED.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집글 수정")
+ void updatePost() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ Long postId = 2L;
+ PostRequest.UpdatePostDto requestDto = new PostRequest.UpdatePostDto("테스트", LocalDateTime.now().plusDays(2), LocalDateTime.now().plusDays(1), "테스트");
+ String requestBody = om.writeValueAsString(requestDto);
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .put("/api/posts/{postId}", postId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] updatePost",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글 수정")
+ .description("""
+ 모집글을 수정합니다.
+
+ 모집마감된 모집글은 수정이 불가능합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("모집글 수정 요청 DTO"))
+ .requestFields(
+ fieldWithPath("title").description("모집글 제목"),
+ fieldWithPath("startTime").description("게임 예정 일시"),
+ fieldWithPath("dueTime").description("모집 마감기한"),
+ fieldWithPath("content").description("모집글 내용")
+ )
+ .pathParameters(parameterWithName("postId").type(SimpleType.NUMBER).description("수정할 모집글의 ID"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집글 삭제")
+ void deletePost() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ Long postId = 2L;
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .delete("/api/posts/{postId}", postId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] deletePost",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글 삭제")
+ .description("""
+ 모집글을 삭제합니다.
+
+ 모집마감된 모집글은 삭제가 불가능합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(parameterWithName("postId").type(SimpleType.NUMBER).description("삭제할 모집글의 ID"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("모집글 마감")
+ void patchPost() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ Long postId = 1L;
+ Boolean isClose = true;
+ PostRequest.UpdatePostIsCloseDto requestDto = new PostRequest.UpdatePostIsCloseDto(isClose);
+ String requestBody = om.writeValueAsString(requestDto);
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .patch("/api/posts/{postId}", postId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[post] patchPost",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("모집글 마감")
+ .description("""
+ 모집글을 마감합니다.
+ """)
+ .tag(ApiTag.POST.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("모집글 마감 요청 DTO"))
+ .requestFields(
+ fieldWithPath("isClose").description("마감 여부")
+ )
+ .pathParameters(parameterWithName("postId").type(SimpleType.NUMBER).description("마감할 모집글의 ID"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/bungaebowling/server/score/controller/ScoreControllerTest.java b/src/test/java/com/bungaebowling/server/score/controller/ScoreControllerTest.java
new file mode 100644
index 00000000..12c851eb
--- /dev/null
+++ b/src/test/java/com/bungaebowling/server/score/controller/ScoreControllerTest.java
@@ -0,0 +1,406 @@
+package com.bungaebowling.server.score.controller;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.bungaebowling.server.ControllerTestConfig;
+import com.bungaebowling.server._core.commons.ApiTag;
+import com.bungaebowling.server._core.commons.GeneralApiResponseSchema;
+import com.bungaebowling.server._core.security.JwtProvider;
+import com.bungaebowling.server.user.Role;
+import com.bungaebowling.server.user.User;
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.epages.restdocs.apispec.Schema;
+import com.epages.restdocs.apispec.SimpleType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.BDDMockito;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.mock.web.MockPart;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.*;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@ActiveProfiles(value = {"test"})
+@Sql(value = "classpath:test_db/teardown.sql", config = @SqlConfig(encoding = "UTF-8"))
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+class ScoreControllerTest extends ControllerTestConfig {
+
+ @MockBean
+ private AmazonS3 amazonS3Client;
+
+ @Autowired
+ public ScoreControllerTest(WebApplicationContext context, ObjectMapper om) {
+ super(context, om);
+ }
+
+ @Test
+ @DisplayName("점수 조회 테스트")
+ void getScores() throws Exception {
+ // given
+ Long postId = 1L;
+
+ Long userId = 1L;
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .get("/api/posts/{postId}/scores", postId)
+ .param("userId", Long.toString(userId))
+ );
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[score] getScores",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("점수 조회")
+ .description("""
+ 모집글의 점수들을 조회합니다.
+ """
+ )
+ .tag(ApiTag.SCORE.getTagName())
+ .pathParameters(parameterWithName("postId").description("모집글 id"))
+ .queryParameters(parameterWithName("userId").optional().type(SimpleType.NUMBER).description("점수를 확인할 사용자 id"))
+ .responseSchema(Schema.schema("점수 조회 응답 DTO"))
+ .responseFields(
+ GeneralApiResponseSchema.SUCCESS.getResponseDescriptor().and(
+ fieldWithPath("response.scores").description("점수 목록"),
+ fieldWithPath("response.scores[].id").description("점수의 ID(PK)"),
+ fieldWithPath("response.scores[].scoreNum").description("볼링 점수"),
+ fieldWithPath("response.scores[].scoreImage").optional().type(SimpleType.STRING).description("점수 첨부 이미지 경로")
+ )
+ )
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("점수 등록 테스트")
+ void createScore() throws Exception {
+ // given
+ Long postId = 1L;
+
+ var userId = 1L; // 김볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ int score = 153;
+ MockMultipartFile file = new MockMultipartFile("image", "image.png", MediaType.IMAGE_PNG_VALUE, "mockImageData".getBytes());
+
+ String imageUrl = "https://kakao.com";
+
+ BDDMockito.given(amazonS3Client.putObject(Mockito.any())).willReturn(null);
+ BDDMockito.given(amazonS3Client.getUrl(Mockito.any(), Mockito.any())).willReturn(new URL(imageUrl));
+
+ // when
+ var builder = RestDocumentationRequestBuilders
+ .multipart("/api/posts/{postId}/scores", postId);
+ builder.with(new RequestPostProcessor() {
+ @Override
+ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+ request.setMethod("POST");
+ return request;
+ }
+ });
+ ResultActions resultActions = mvc.perform(
+ builder
+ .file(file)
+ .part(new MockPart("score", Integer.toString(score).getBytes(StandardCharsets.UTF_8)))
+ .contentType(MediaType.MULTIPART_FORM_DATA)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[score] createScore",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("점수 등록")
+ .description("""
+ 모집 완료되어 게임 플레이 한 이후(start_time 이후) 자신이 참여한 모집글에 점수 등록이 가능합니다.
+
+ - 파일은 png, jpg, gif, jpeg만 업로드 가능합니다.
+ - 파일은 10MB의 크기 제한이 존재합니다.
+
+ 현재 사용 플러그인이 multipart/form-data의 파라미터에 대한 문서화가 지원되지 않습니다. (try it out 불가능)
+
+ | Part | Type | Description |
+ |-------|--------|------------------------------|
+ | score | number | 볼링 점수 (1~300) |
+ | image | Binary | 점수판 사진 등 첨부 이미지 파일 |
+
+ """)
+ .tag(ApiTag.SCORE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(parameterWithName("postId").type(SimpleType.NUMBER).description("모집글 id"))
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("점수 수정 테스트")
+ void updateScore() throws Exception {
+ // given
+ Long postId = 1L;
+
+ Long scoreId = 2L;
+
+ var userId = 1L; // 김볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ int score = 153;
+ MockMultipartFile file = new MockMultipartFile("image", "image.png", MediaType.IMAGE_PNG_VALUE, "mockImageData".getBytes());
+
+ String imageUrl = "https://kakao.com";
+
+ BDDMockito.given(amazonS3Client.putObject(Mockito.any())).willReturn(null);
+ BDDMockito.given(amazonS3Client.getUrl(Mockito.any(), Mockito.any())).willReturn(new URL(imageUrl));
+
+ // when
+ var builder = RestDocumentationRequestBuilders
+ .multipart("/api/posts/{postId}/scores/{scoreId}", postId, scoreId);
+ builder.with(new RequestPostProcessor() {
+ @Override
+ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+ request.setMethod("PUT");
+ return request;
+ }
+ });
+ ResultActions resultActions = mvc.perform(
+ builder
+ .file(file)
+ .part(new MockPart("score", Integer.toString(score).getBytes(StandardCharsets.UTF_8)))
+ .contentType(MediaType.MULTIPART_FORM_DATA)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[score] updateScore",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("점수 수정")
+ .description("""
+ 자신의 점수에 대한 정보를 수정 가능합니다.
+
+ 요청에 포함된 image나 score에 대하여 기존 정보를 수정합니다.
+
+ - 파일은 png, jpg, gif, jpeg만 업로드 가능합니다.
+ - 파일은 10MB의 크기 제한이 존재합니다.
+
+ 현재 사용 플러그인이 multipart/form-data의 파라미터에 대한 문서화가 지원되지 않습니다. (try it out 불가능)
+
+ 아래 파라미터 중 변경 할 요소만 포함하여 보내면 됩니다.
+
+ | Part | Type | Description |
+ |-------|--------|------------------------------|
+ | score | number | 볼링 점수 (1~300) |
+ | image | Binary | 점수판 사진 등 첨부 이미지 파일 |
+
+ """)
+ .tag(ApiTag.SCORE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(
+ parameterWithName("postId").type(SimpleType.NUMBER).description("모집글 id"),
+ parameterWithName("scoreId").type(SimpleType.NUMBER).description("점수 id")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("점수 이미지 삭제 테스트")
+ void deleteScoreImage() throws Exception {
+ // given
+ Long postId = 1L;
+
+ Long scoreId = 2L;
+
+ var userId = 1L; // 김볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ BDDMockito.willAnswer(invocation -> {
+ return null;
+ }).given(amazonS3Client).deleteObject(Mockito.any());
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .delete("/api/posts/{postId}/scores/{scoreId}/image", postId, scoreId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[score] deleteScoreImage",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("점수 이미지 삭제")
+ .description("""
+ 점수는 유지한 채 이미지만 삭제합니다.
+ """)
+ .tag(ApiTag.SCORE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(
+ parameterWithName("postId").type(SimpleType.NUMBER).description("모집글 id"),
+ parameterWithName("scoreId").type(SimpleType.NUMBER).description("점수 id")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("점수 삭제 테스트")
+ void deleteScore() throws Exception {
+ // given
+ Long postId = 1L;
+
+ Long scoreId = 2L;
+
+ var userId = 1L; // 김볼링
+
+ var accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ );
+
+ BDDMockito.willAnswer(invocation -> {
+ return null;
+ }).given(amazonS3Client).deleteObject(Mockito.any());
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .delete("/api/posts/{postId}/scores/{scoreId}", postId, scoreId)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+
+ // then
+ var responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[score] deleteScore",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("점수 삭제")
+ .description("""
+ 점수 행을 완전히 삭제합니다.
+ """)
+ .tag(ApiTag.SCORE.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .pathParameters(
+ parameterWithName("postId").type(SimpleType.NUMBER).description("모집글 id"),
+ parameterWithName("scoreId").type(SimpleType.NUMBER).description("점수 id")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/bungaebowling/server/user/controller/UserControllerTest.java b/src/test/java/com/bungaebowling/server/user/controller/UserControllerTest.java
index cec06173..d0c23ff0 100644
--- a/src/test/java/com/bungaebowling/server/user/controller/UserControllerTest.java
+++ b/src/test/java/com/bungaebowling/server/user/controller/UserControllerTest.java
@@ -1001,4 +1001,174 @@ void getUserRecords() throws Exception {
)
);
}
+
+ @Test
+ @DisplayName("비밀번호 변경")
+ void updatePassword() throws Exception {
+ // given
+ Long userId = 1L;
+ String accessToken = JwtProvider.createAccess(
+ User.builder()
+ .id(userId)
+ .role(Role.ROLE_USER)
+ .build()
+ ); // 김볼링
+ UserRequest.UpdatePasswordDto requestDto = new UserRequest.UpdatePasswordDto("test12!@", "qwer1234!");
+ String requestBody = om.writeValueAsString(requestDto);
+
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .patch("/api/users/password")
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ .header(HttpHeaders.AUTHORIZATION, accessToken)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[user] updatePassword",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("비밀번호 변경")
+ .description("""
+ 비밀번호를 변경합니다.
+ """)
+ .tag(ApiTag.USER.getTagName())
+ .requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("access token"))
+ .requestSchema(Schema.schema("비밀번호 변경 요청 DTO"))
+ .requestFields(
+ fieldWithPath("password").description("기존 비밀번호"),
+ fieldWithPath("newPassword").description("새로운 비밀번호")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("비밀번호 찾기 - 본인 인증 메일 발송")
+ void sendVerificationMailForPasswordReset() throws Exception {
+ // given
+ UserRequest.SendVerificationMailForPasswordResetDto requestDto = new UserRequest.SendVerificationMailForPasswordResetDto("test@test.com");
+ String requestBody = om.writeValueAsString(requestDto);
+
+ JavaMailSender javaMailSenderImpl = new JavaMailSenderImpl();
+
+ BDDMockito.given(javaMailSender.createMimeMessage()).willReturn(javaMailSenderImpl.createMimeMessage());
+ BDDMockito.willAnswer(invocation -> {
+ return null;
+ }).given(javaMailSender).send(Mockito.any(MimeMessagePreparator.class));
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .post("/api/password/email-verification")
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[user] sendVerificationMailForPasswordReset",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("비밀번호 찾기 - 본인 인증 메일 발송")
+ .description("""
+ 계정 정보의 email로 인증 메일을 보냅니다. email에는 url이 삽입되어 보내집니다.
+
+ (e.g.) https://bungaebowling.com/password/email-verification?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIyIiwicm9sZSI6IlJPTEVfVVNFUiIsInR5cGUiOiJlbWFpbC12ZXJpZmljYXRpb24iLCJleHAiOjE2OTU2MzU5MDh9.3AWusXvtgBiQN0GoegjKJw-fnaYSGVO1Ue0sSrtuWCVOQwzfIwh6KELN2NHOOXIO6MK-D11PndbtwcHetibZVQ
+
+ 인증을 위해서 이 토큰 값을 그대로 /api/password/email-confirm의 데이터로 요청 보내주시길 바랍니다.
+ """)
+ .tag(ApiTag.AUTHORIZATION.getTagName())
+ .requestSchema(Schema.schema("비밀번호 찾기 - 본인 인증 메일 발송 요청 DTO"))
+ .requestFields(
+ fieldWithPath("email").description("가입한 이메일")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("비밀번호 찾기 - 본인 인증 메일 확인 및 임시 비밀번호 메일 발송")
+ void confirmEmailAndSendTempPassword() throws Exception {
+ // given
+ User user = User.builder()
+ .id(1L)
+ .build();
+ String token = JwtProvider.createEmailVerificationForPassword(user);
+ UserRequest.ConfirmEmailAndSendTempPasswordDto requestDto = new UserRequest.ConfirmEmailAndSendTempPasswordDto(token);
+ String requestBody = om.writeValueAsString(requestDto);
+
+ JavaMailSender javaMailSenderImpl = new JavaMailSenderImpl();
+
+ BDDMockito.given(javaMailSender.createMimeMessage()).willReturn(javaMailSenderImpl.createMimeMessage());
+ BDDMockito.willAnswer(invocation -> {
+ return null;
+ }).given(javaMailSender).send(Mockito.any(MimeMessagePreparator.class));
+ // when
+ ResultActions resultActions = mvc.perform(
+ RestDocumentationRequestBuilders
+ .post("/api/password/email-confirm")
+ .content(requestBody)
+ .contentType(MediaType.APPLICATION_JSON)
+ );
+ // then
+ String responseBody = resultActions.andReturn().getResponse().getContentAsString();
+ Object json = om.readValue(responseBody, Object.class);
+ System.out.println("[response]\n" + om.writerWithDefaultPrettyPrinter().writeValueAsString(json));
+
+ resultActions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.status").value(200)
+ ).andDo(
+ MockMvcRestDocumentationWrapper.document(
+ "[user] confirmEmailAndSendTempPassword",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .summary("비밀번호 찾기 - 본인 인증 메일 확인 및 임시 비밀번호 메일 발송")
+ .description("""
+ 본인 인증 확인 후 기존 비밀번호를 삭제하고 임시 비밀번호를 생성합니다.
+
+ 임시 비밀번호를 메일로 발송합니다.
+ """)
+ .tag(ApiTag.AUTHORIZATION.getTagName())
+ .requestSchema(Schema.schema("비밀번호 찾기 - 본인 인증 메일 발송 요청 DTO"))
+ .requestFields(
+ fieldWithPath("token").description("메일로 발송된 링크에 첨부된 토큰")
+ )
+ .responseSchema(Schema.schema(GeneralApiResponseSchema.SUCCESS.getName()))
+ .responseFields(GeneralApiResponseSchema.SUCCESS.getResponseDescriptor())
+ .build()
+ )
+ )
+ );
+ }
}
\ No newline at end of file