diff --git a/build.gradle b/build.gradle index 2cb717157..12cdf5c63 100644 --- a/build.gradle +++ b/build.gradle @@ -82,6 +82,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.awaitility:awaitility' + + // S3 AWS + implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE' + testImplementation 'io.findify:s3mock_2.13:0.2.6' } tasks.named('test') { diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 04ab06f14..98e88c9f0 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -12,25 +12,31 @@ === 보호소 로그인 ==== Request + operation::auth-controller-test/shelter-login[snippets='http-request,request-fields'] ==== Response + operation::auth-controller-test/shelter-login[snippets='http-response,response-fields,response-cookies'] === 봉사자 로그인 ==== Request + operation::auth-controller-test/volunteer-login[snippets='http-request,request-fields'] ==== Response + operation::auth-controller-test/volunteer-login[snippets='http-response,response-fields,response-cookies'] === 액세스 토큰 갱신 ==== Request + operation::auth-controller-test/refresh-access-token[snippets='http-request,request-cookies'] ==== Response + operation::auth-controller-test/refresh-access-token[snippets='http-response,response-fields,response-cookies'] == 1. 봉사자 @@ -83,20 +89,14 @@ operation::review-controller-test/find-shelter-reviews-by-volunteer[snippets='ht == 2. 보호소 -=== 보호소 비밀번호 변경 - -==== Request -operation::shelter-controller-test/update-password[snippets='http-request,request-headers,request-fields'] - -==== Response -operation::shelter-controller-test/update-password[snippets='http-response'] - === 보호소 마이 페이지 조회 ==== Request + operation::shelter-controller-test/find-shelter-my-page[snippets='http-request,request-headers'] ==== Response + operation::shelter-controller-test/find-shelter-my-page[snippets='http-response,response-fields'] == 3. 봉사 모집 diff --git a/src/main/java/com/clova/anifriends/global/config/S3Config.java b/src/main/java/com/clova/anifriends/global/config/S3Config.java new file mode 100644 index 000000000..1f7e7fccd --- /dev/null +++ b/src/main/java/com/clova/anifriends/global/config/S3Config.java @@ -0,0 +1,35 @@ +package com.clova.anifriends.global.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3Client() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/com/clova/anifriends/global/image/ImageController.java b/src/main/java/com/clova/anifriends/global/image/ImageController.java new file mode 100644 index 000000000..03587b5bb --- /dev/null +++ b/src/main/java/com/clova/anifriends/global/image/ImageController.java @@ -0,0 +1,26 @@ +package com.clova.anifriends.global.image; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/images") +public class ImageController { + + private final S3Service s3Service; + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity uploadImages( + @ModelAttribute @Valid UploadImagesRequest uploadImagesRequest + ) { + return ResponseEntity.ok( + UploadImagesResponse.from(s3Service.uploadImages(uploadImagesRequest.images()))); + } +} diff --git a/src/main/java/com/clova/anifriends/global/image/S3BadRequestException.java b/src/main/java/com/clova/anifriends/global/image/S3BadRequestException.java new file mode 100644 index 000000000..06b5c7f5d --- /dev/null +++ b/src/main/java/com/clova/anifriends/global/image/S3BadRequestException.java @@ -0,0 +1,12 @@ +package com.clova.anifriends.global.image; + +import static com.clova.anifriends.global.exception.ErrorCode.BAD_REQUEST; + +import com.clova.anifriends.global.exception.BadRequestException; + +public class S3BadRequestException extends BadRequestException { + + public S3BadRequestException(String message) { + super(BAD_REQUEST, message); + } +} diff --git a/src/main/java/com/clova/anifriends/global/image/S3Service.java b/src/main/java/com/clova/anifriends/global/image/S3Service.java new file mode 100644 index 000000000..84d9eb073 --- /dev/null +++ b/src/main/java/com/clova/anifriends/global/image/S3Service.java @@ -0,0 +1,70 @@ +package com.clova.anifriends.global.image; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class S3Service { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + private static final String FOLDER = "images"; + + public List uploadImages(List multipartFileList) { + ObjectMetadata objectMetadata = new ObjectMetadata(); + List list = new ArrayList<>(); + + for (MultipartFile multipartFile : multipartFileList) { + String fileName = createFileName(multipartFile.getOriginalFilename()); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + + try (InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject( + new PutObjectRequest(bucket + "/" + FOLDER, fileName, inputStream, + objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + list.add(amazonS3.getUrl(bucket + "/" + FOLDER, fileName).toString()); + } catch (IOException e) { + throw new S3BadRequestException("S3에 이미지를 업로드하는데 실패했습니다."); + } + } + return list; + } + + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + if (fileName.length() == 0) { + throw new S3BadRequestException("잘못된 파일입니다."); + } + ArrayList fileValidate = new ArrayList<>(); + fileValidate.add(".jpg"); + fileValidate.add(".jpeg"); + fileValidate.add(".png"); + fileValidate.add(".JPG"); + fileValidate.add(".JPEG"); + fileValidate.add(".PNG"); + String idxFileName = fileName.substring(fileName.lastIndexOf(".")); + if (!fileValidate.contains(idxFileName)) { + throw new S3BadRequestException("잘못된 파일 형식입니다."); + } + return fileName.substring(fileName.lastIndexOf(".")); + } +} diff --git a/src/main/java/com/clova/anifriends/global/image/UploadImagesRequest.java b/src/main/java/com/clova/anifriends/global/image/UploadImagesRequest.java new file mode 100644 index 000000000..cc0f10199 --- /dev/null +++ b/src/main/java/com/clova/anifriends/global/image/UploadImagesRequest.java @@ -0,0 +1,14 @@ +package com.clova.anifriends.global.image; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; +import org.springframework.web.multipart.MultipartFile; + +public record UploadImagesRequest( + @NotNull(message = "이미지는 필수 입력 항목입니다.") + @Size(min = 1, max = 5, message = "이미지는 1개 이상 5개 이하로 선택하세요.") + List images +) { + +} diff --git a/src/main/java/com/clova/anifriends/global/image/UploadImagesResponse.java b/src/main/java/com/clova/anifriends/global/image/UploadImagesResponse.java new file mode 100644 index 000000000..47913a5e7 --- /dev/null +++ b/src/main/java/com/clova/anifriends/global/image/UploadImagesResponse.java @@ -0,0 +1,11 @@ +package com.clova.anifriends.global.image; + +import java.util.List; + +public record UploadImagesResponse( + List imageUrls +) { + public static UploadImagesResponse from(List imageUrls) { + return new UploadImagesResponse(imageUrls); + } +} diff --git a/src/main/resources/backend-config b/src/main/resources/backend-config index df0c8d6fa..cc1a30e86 160000 --- a/src/main/resources/backend-config +++ b/src/main/resources/backend-config @@ -1 +1 @@ -Subproject commit df0c8d6fa515549e616414b499743ae1f89ac865 +Subproject commit cc1a30e86ed18b48e145ecbeeb4a4ea1315d5542 diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 8ebdb7f34..ed5e3f407 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -1,588 +1,2267 @@ - - - - -API 문서 - - - + + + + + API 문서 + + +
-

공통 권한

-
-

0. 인증

-
-
-

보호소 로그인

-
-

Request

-
-
HTTP request
-
-
+

공통 권한

+
+

0. 인증

+
+
+

보호소 로그인

+
+

Request

+
+
HTTP + request
+
+
POST /api/auth/shelters/login HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 64
-X-CSRF-TOKEN: zDssgZAPnieEdSk5UT5nSPNigg2A7t1lYXW4evhK4tXeLn4kr1hJt6k9rkSpQRsPZhNTecdWrzS317hIB0eJSM98g-PmF0gc
+X-CSRF-TOKEN: A5xJQPLZ1uY-JBhpGQ-GIepe2eAZDp9TO_gq7qe5wICbLixkNah8I8a94tUTRytYfSKyQ9xv9IEhO_5-WZpJjJDf-eWrGRkF
 Host: localhost:8080
 
 {
   "email" : "email@email.com",
   "password" : "password123!"
 }
-
-
-
-
-
Request fields
- ------- - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값제약설명

email

String

true

보호소 이메일

password

String

true

보호소 패스워드

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Request + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
필드명타입필수값제약설명

+ email

+ String

true

보호소 이메일

+ password

+ String

true

보호소 패스워드

+
+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Set-Cookie: refreshToken=refreshToken; Path=/api/auth; Domain=localhost; HttpOnly; SameSite=None
 Content-Type: application/json;charset=UTF-8
@@ -598,131 +2277,148 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

userId

Number

사용자 ID

role

String

사용자 역할

accessToken

String

액세스 토큰

-
-
-
Response cookies
- ---- - - - - - - - - - - - - -
NameDescription

refreshToken

리프레시 토큰

-
-
-
-
-

봉사자 로그인

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

+ userId

+ Number

사용자 ID

+ role

+ String

사용자 역할

accessToken +

+ String

액세스 토큰

+
+
+
Response + cookies
+ + + + + + + + + + + + + + + + + +
NameDescription

refreshToken +

리프레시 토큰

+
+
+
+
+

봉사자 로그인

+
+

Request

+
+
HTTP + request
+
+
POST /api/auth/volunteers/login HTTP/1.1
 Content-Type: application/json;charset=UTF-8
 Content-Length: 64
-X-CSRF-TOKEN: lwvQlUhASCsmqnIoMaS5aIG8uKUEFJutxPMoM-DtFpAwGB4s82rjpn50fR8LyEdKBImNDbCLlcRidamA9socBtOPcqhWKnoV
+X-CSRF-TOKEN: yxDJ3FeXIyr3qDPW5E5gIUweMhQO9EO9TL2xCXWNmJLvir78-iGr5WHxEUvaywLugGNUQnUtHy0-xXGQLouEOke4oaaLuIfL
 Host: localhost:8080
 
 {
   "email" : "email@email.com",
   "password" : "password123!"
 }
-
-
-
-
-
Request fields
- ------- - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값제약설명

email

String

true

봉사자 이메일

password

String

true

봉사자 패스워드

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Request + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
필드명타입필수값제약설명

+ email

+ String

true

봉사자 이메일

+ password

+ String

true

봉사자 패스워드

+
+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Set-Cookie: refreshToken=refreshToken; Path=/api/auth; Domain=localhost; HttpOnly; SameSite=None
 Content-Type: application/json;charset=UTF-8
@@ -738,112 +2434,127 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

userId

Number

사용자 ID

role

String

사용자 역할

accessToken

String

액세스 토큰

-
-
-
Response cookies
- ---- - - - - - - - - - - - - -
NameDescription

refreshToken

리프레시 토큰

-
-
-
-
-

액세스 토큰 갱신

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

+ userId

+ Number

사용자 ID

+ role

+ String

사용자 역할

accessToken +

+ String

액세스 토큰

+
+
+
Response + cookies
+ + + + + + + + + + + + + + + + + +
NameDescription

refreshToken +

리프레시 토큰

+
+
+
+
+

액세스 토큰 갱신

+
+

Request

+
+
HTTP + request
+
+
POST /api/auth/refresh HTTP/1.1
-X-CSRF-TOKEN: B2Bq18TLcAJKK8veKudRLQezb1GIp_B6eKL4n1ep5vXPEs70NwEI5_GvFjBnGqrvSMplHmPWQmi9wZVXGcPLrjKc05D4cPbF
+X-CSRF-TOKEN: ZpHrcqTCXUq5SP_fI4JmD-2j9qbw9sQYNr3bDKUOO_9ZRIX6UqfZSsfzbn6UeMa5Gq9SatSb28eSz_I1Aoq_P8BtD8hhcraY
 Host: localhost:8080
-Cookie: refreshToken=eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzAsInN1YiI6IjEiLCJleHAiOjE2OTk1MjgyMzAsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.Gq_pZJ5XDYC3wZtmXp59qxZPqlHetlSHxnv3TKVePXOxde_dca4f0cxdr88xLm9K
+Cookie: refreshToken=eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjUsInN1YiI6IjEiLCJleHAiOjE2OTk1MjgxMjUsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.2hTDkjRYZBzjaebFGhwRvGyVOzmx6LKjY-td7qxhVDrsDF80pTM40SYKcV1iKWQ6
 Content-Type: application/x-www-form-urlencoded
-
-
-
-
-
Request cookies
- ---- - - - - - - - - - - - - -
NameDescription

refreshToken

리프레시 토큰

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Request + cookies
+ + + + + + + + + + + + + + + + + +
NameDescription

refreshToken +

리프레시 토큰

+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
-Set-Cookie: refreshToken=eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzAsInN1YiI6IjEiLCJleHAiOjE2OTk1MjgyMzAsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.Gq_pZJ5XDYC3wZtmXp59qxZPqlHetlSHxnv3TKVePXOxde_dca4f0cxdr88xLm9K; Path=/api/auth; Domain=localhost; HttpOnly; SameSite=None
+Set-Cookie: refreshToken=eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjUsInN1YiI6IjEiLCJleHAiOjE2OTk1MjgxMjUsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.2hTDkjRYZBzjaebFGhwRvGyVOzmx6LKjY-td7qxhVDrsDF80pTM40SYKcV1iKWQ6; Path=/api/auth; Domain=localhost; HttpOnly; SameSite=None
 Content-Type: application/json;charset=UTF-8
 X-Content-Type-Options: nosniff
 X-XSS-Protection: 0
@@ -855,149 +2566,172 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

userId

Number

사용자 ID

role

String

사용자 역할

accessToken

String

갱신된 액세스 토큰

-
-
-
Response cookies
- ---- - - - - - - - - - - - - -
NameDescription

refreshToken

갱신된 리프레시 토큰

-
-
-
-
-
-
-

1. 봉사자

-
-
-

봉사자가 완료한 봉사 모집글 리스트 조회

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

+ userId

+ Number

사용자 ID

+ role

+ String

사용자 역할

accessToken +

+ String

갱신된 액세스 토큰

+
+
+
+
Response + cookies
+ + + + + + + + + + + + + + + + + +
NameDescription

refreshToken +

갱신된 리프레시 토큰

+
+
+
+
+
+
+
+

1. 봉사자

+
+
+

봉사자가 완료한 봉사 + 모집글 리스트 조회

+
+

Request

+
+
HTTP + request
+
+
GET /api/volunteers/1/recruitments/completed?pageNumber=0&pageSize=10 HTTP/1.1
-X-CSRF-TOKEN: RmoK4J-et28f51gWg24OXdqJX9oFg5O8f0q--6_nOrJJQN2bIlw8166vj1gyg2En5kM6aLy-crgwtKqRGiuKmZiGDoRweeT5
+X-CSRF-TOKEN: TrqvH7c3uuKmjH0GFINogY7B-HQb1_ISjHRL--rHeoycm0a-KIycedNVidCLux5nIa5cteik1RUutcU_vEQund7yGO_--nOL
 Host: localhost:8080
-
-
-
-
-
Path parameters
- - ---- - - - - - - - - - - - - -
Table 1. /api/volunteers/{volunteerId}/recruitments/completed
ParameterDescription

volunteerId

봉사자 ID

-
-
-
Query parameters
- ------ - - - - - - - - - - - - - - - - - - - - - - -
필드명필수값제약설명

pageNumber

true

페이지 번호

pageSize

true

페이지 사이즈

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Path + parameters
+ + + + + + + + + + + + + + + + + + +
Table 1. /api/volunteers/{volunteerId}/recruitments/completed +
ParameterDescription

volunteerId +

봉사자 ID

+
+
+
Query + parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
필드명필수값제약설명

+ pageNumber

true

페이지 번호

+ pageSize

true

페이지 사이즈

+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 X-Content-Type-Options: nosniff
@@ -1011,7 +2745,7 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

recruitments

Array

봉사 모집글 리스트

recruitments[].recruitmentId

Number

봉사 모집글 ID

recruitments[].recruitmentTitle

String

봉사 모집글 제목

recruitments[].recruitmentStartTime

String

봉사 날짜

recruitments[].shelterName

String

보호소 이름

pageInfo

Object

페이지 정보

pageInfo.totalElements

Number

총 요소 개수

pageInfo.hasNext

Boolean

다음 페이지 여부

-
-
-
-
-
-
-

보호 동물

-
-
-

보호 동물 상세 조회

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

recruitments +

+ Array

봉사 모집글 리스트

+

recruitments[].recruitmentId +

+ Number

봉사 모집글 ID

+

recruitments[].recruitmentTitle +

+ String

봉사 모집글 제목

+

recruitments[].recruitmentStartTime +

+ String

봉사 날짜

recruitments[].shelterName +

+ String

보호소 이름

+ pageInfo

+ Object

페이지 정보

pageInfo.totalElements +

+ Number

총 요소 개수

pageInfo.hasNext +

+ Boolean

다음 페이지 여부

+
+
+
+
+
+
+
+

보호 동물

+
+
+

보호 동물 상세 조회

+
+

Request

+
+
HTTP + request
+
+
GET /api/animals/1 HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMjcsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkyMjcsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.EIuJL1skYCEZVdopzpyZWsYyxdLpjYGCjjH5g8Y_mSt0b9wHc5BXlom74rl8ytdI
-X-CSRF-TOKEN: b3FE1ljFJTc-0r-LKuPj_Sn86gUwu5soey22XRabatCMpbYkDklz5GHwRwQT54ayTM7XxUuex2dV364FGk7VbXf6CeTvx9BH
+Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjIsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkxMjIsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.4EexAANZf8_YVsHF_-ge3yOfr3DCK4ovOqmxu07rlaNg9aEdLqAuxp6BxSPGm8Pi
+X-CSRF-TOKEN: uF0p4nCBOnfw3H3trdBg3vY06gpFXt7tlb5TORSs54G3pCPRjj4c0xbkXEfd7k2Im_1Uus4Ax2snaefAoIhlWCOV0-eHwRvo
 Host: localhost:8080
-
-
-
-
-
Path parameters
- - ---- - - - - - - - - - - - - -
Table 1. /api/animals/{animalId}
ParameterDescription

animalId

보호 동물 ID

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Path + parameters
+ + + + + + + + + + + + + + + + + + +
Table 1. /api/animals/{animalId}
ParameterDescription

+ animalId

보호 동물 ID

+
+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 X-Content-Type-Options: nosniff
@@ -1156,214 +2917,278 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

animalId

Number

보호 동물 ID

animalName

String

보호 동물 이름

animalBirthDate

String

보호 동물 출생 날짜

animalType

String

보호 동물 종류

animalBreed

String

보호 동물 품종

animalGender

String

보호 동물 성별

animalIsNeutered

Boolean

보호 동물 중성화 유무

animalActive

String

보호 동물 활동성

animalWeight

Number

보호 동물 몸무게

animalInformation

String

보호 동물 기타 정보

animalImageUrls[]

Array

보호 동물 이미지 url 리스트

animalIsAdopted

Boolean

보호 동물 입양 여부

-
-
-
-
-
-

보호소

-
-

1. 봉사자

-
-
-

1) 봉사 모집글 조회 & 검색

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

+ animalId

+ Number

보호 동물 ID

+

+ animalName

+ String

보호 동물 이름

+

animalBirthDate +

+ String

보호 동물 출생 날짜

+

+ animalType

+ String

보호 동물 종류

+

animalBreed +

+ String

보호 동물 품종

+

animalGender +

+ String

보호 동물 성별

+

animalIsNeutered +

+ Boolean

보호 동물 중성화 유무

+

animalActive +

+ String

보호 동물 활동성

+

animalWeight +

+ Number

보호 동물 몸무게

+

animalInformation +

+ String

보호 동물 기타 정보

+

animalImageUrls[] +

+ Array

보호 동물 이미지 url + 리스트

animalIsAdopted +

+ Boolean

보호 동물 입양 여부

+
+
+
+
+
+
+

보호소

+
+

1. 봉사자

+
+
+

1) 봉사 모집글 조회 & 검색 +

+
+

Request

+
+
HTTP + request
+
+
GET /api/volunteers/recruitments?keyword=%EA%B2%85%EC%83%89%EC%96%B4&startDate=2023-11-09&endDate=2023-11-09&isClosed=false&title=true&content=false&shelterName=false&pageNumber=0&pageSize=10 HTTP/1.1
-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzIsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkyMzIsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.-Sj-MvVG9hOijBPFaSVHF5rtaSY0FFmPbxt6ifGJ591fBDjbbMwM45mF97lR1AR7
-X-CSRF-TOKEN: axNfjmnB6HsESQK6oeFJ2wNRJ_MPyTG0kkS1dzsgaINU8XK5XSo9u1z43EIpcTbfk8x96Tc3CpI98FeZ83bRR11ECbAwwRGM
+Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjYsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkxMjYsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.i81tSkYawd7N6Kqk7bGY_Jtb3gIZGZXlAuS-KJnse14d1lQsGmxhTAEnI_Kd2S0r
+X-CSRF-TOKEN: AsnXdal1CvQAVk-RypPAD9zG7bBMSDIekmSMxJso9zm6iunqN6u1F8wTa8ItZ3n0-b70OO2nwNF7fwYzolK0pv0QkV2C6YrZ
 Host: localhost:8080
-
-
-
-
-
Request headers
- ---- - - - - - - - - - - - - -
NameDescription

Authorization

봉사자 액세스 토큰

-
-
-
Query parameters
- ------ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명필수값제약설명

keyword

검색어

startDate

yyyy-MM-dd

검색 시작일

endDate

yyyy-MM-dd

검색 종료일

isClosed

true, false

마감 여부

title

기본값 true

제목 포함 검색

content

기본값 true

본문 포함 검색

shelterName

기본값 true

보호소 이름 포함 검색

pageNumber

true

페이지 번호

pageSize

true

페이지 사이즈

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Request + headers
+ + + + + + + + + + + + + + + + + +
NameDescription

Authorization +

봉사자 액세스 토큰

+
+
+
+
Query + parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
필드명필수값제약설명

+ keyword

검색어

+ startDate

yyyy-MM-dd

+

검색 시작일

+ endDate

yyyy-MM-dd

+

검색 종료일

+ isClosed

true, false

+

마감 여부

+ title

기본값 true

+

제목 포함 검색

+

+ content

기본값 true

+

본문 포함 검색

+

shelterName +

기본값 true

+

보호소 이름 포함 검색

+

+ pageNumber

true

페이지 번호

+ pageSize

true

페이지 사이즈

+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 X-Content-Type-Options: nosniff
@@ -1377,8 +3202,8 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

recruitments

Array

봉사 모집글 리스트

recruitments[].recruitmentId

Number

봉사 모집글 ID

recruitments[].recruitmentTitle

String

봉사 모집글 제목

recruitments[].recruitmentStartTime

String

봉사 시작 시간

recruitments[].recruitmentEndTime

String

봉사 종료 시간

recruitments[].recruitmentIsClosed

Boolean

봉사 모집 마감 여부

recruitments[].recruitmentApplicantCount

Number

봉사 신청 인원

recruitments[].recruitmentCapacity

Number

봉사 정원

recruitments[].shelterName

String

보호소 이름

recruitments[].shelterImageUrl

String

보호소 이미지 url

pageInfo

Object

페이지 정보

pageInfo.totalElements

Number

총 요소 개수

pageInfo.hasNext

Boolean

다음 페이지 여부

-
-
-
-
-

보호소에 달린 후기 리스트 조회

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

recruitments +

+ Array

봉사 모집글 리스트

+

recruitments[].recruitmentId +

+ Number

봉사 모집글 ID

+

recruitments[].recruitmentTitle +

+ String

봉사 모집글 제목

+

recruitments[].recruitmentStartTime +

+ String

봉사 시작 시간

+

recruitments[].recruitmentEndTime +

+ String

봉사 종료 시간

+

recruitments[].recruitmentIsClosed +

+ Boolean

봉사 모집 마감 여부

+

recruitments[].recruitmentApplicantCount +

+ Number

봉사 신청 인원

+

recruitments[].recruitmentCapacity +

+ Number

봉사 정원

recruitments[].shelterName +

+ String

보호소 이름

recruitments[].shelterImageUrl +

+ String

보호소 이미지 url

+

+ pageInfo

+ Object

페이지 정보

pageInfo.totalElements +

+ Number

총 요소 개수

pageInfo.hasNext +

+ Boolean

다음 페이지 여부

+
+
+
+
+
+

보호소에 달린 후기 리스트 조회 +

+
+

Request

+
+
HTTP + request
+
+
GET /api/volunteers/shelters/1/reviews?pageNumber=0&pageSize=10 HTTP/1.1
-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzIsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkyMzIsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.-Sj-MvVG9hOijBPFaSVHF5rtaSY0FFmPbxt6ifGJ591fBDjbbMwM45mF97lR1AR7
-X-CSRF-TOKEN: kngMsIgkMKfPEq-0n6mQ3yTQbflC6RHJL-E1ncfUZrJqq9Os8Es7ge5CAZPiIcmM-YSk5xbpQJgm3SPkStNQ-_blVNNbmOfJ
+Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjYsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkxMjYsInJvbGUiOiJST0xFX1ZPTFVOVEVFUiJ9.i81tSkYawd7N6Kqk7bGY_Jtb3gIZGZXlAuS-KJnse14d1lQsGmxhTAEnI_Kd2S0r
+X-CSRF-TOKEN: foV4pQdDuj4hPL2txEToOQds1a9qoXiMSIth0sMQCqmFvmPVRrRNkWYg2AoMWdnPp2ncDTQP-M5cxUGhLbpQtPtzOZ7j2Fq3
 Host: localhost:8080
-
-
-
-
-
Path parameters
- - ---- - - - - - - - - - - - - -
Table 1. /api/volunteers/shelters/{shelterId}/reviews
ParameterDescription

shelterId

보호소 ID

-
-
-
Query parameters
- ------ - - - - - - - - - - - - - - - - - - - - - - -
필드명필수값제약설명

pageNumber

true

페이지 번호

pageSize

true

페이지 사이즈

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Path + parameters
+ + + + + + + + + + + + + + + + + + +
Table 1. /api/volunteers/shelters/{shelterId}/reviews
ParameterDescription

+ shelterId

보호소 ID

+
+
+
Query + parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
필드명필수값제약설명

+ pageNumber

true

페이지 번호

+ pageSize

true

페이지 사이즈

+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 X-Content-Type-Options: nosniff
@@ -1563,13 +3434,13 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

reviews

Array

리뷰 리스트

reviews[].reviewId

Number

리뷰 ID

reviews[].reviewCreatedAt

String

리뷰 생성일

reviews[].reviewContent

String

리뷰 내용

reviews[].reviewImageUrls

Array

리뷰 이미지 url 리스트

reviews[].volunteerEmail

String

봉사자 이메일

reviews[].volunteerTemperature

Number

봉사자 온도

pageInfo

Object

페이지 정보

pageInfo.totalElements

Number

총 요소 개수

pageInfo.hasNext

Boolean

다음 페이지 여부

-
-
-
-
-
-
-

2. 보호소

-
-
-

보호소 비밀번호 변경

-
-

Request

-
-
HTTP request
-
-
-
PATCH /api/shelters/me/passwords HTTP/1.1
-Content-Type: application/json;charset=UTF-8
-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzIsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkyMzIsInJvbGUiOiJST0xFX1NIRUxURVIifQ.IYP90yhFJk0RbULVJM00oNvAqfjvmwGjLws3esYyLZA1i_98B0p7p93Jyc48qDLj
-Content-Length: 76
-X-CSRF-TOKEN: 4GjSVL-6M-b2ng0tcCmG_zQQ82GslrSiibGMquN72QqchtSFhVriZovZANXbrGlMSASyzFIj3lma8oWPv4nqztIZ7DKo5OCw
-Host: localhost:8080
-
-{
-  "oldPassword" : "oldPassword123!",
-  "newPassword" : "newPassword123!"
-}
-
-
-
-
-
Request headers
- ---- - - - - - - - - - - - - -
NameDescription

Authorization

보호소 액세스 토큰

-
-
-
Request fields
- ------- - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값제약설명

oldPassword

String

true

6자 이상, 16자 이하

현재 비밀번호

newPassword

String

true

6자 이상, 16자 이하

변경할 비밀번호

-
-
-
-

Response

-
-
HTTP response
-
-
-
HTTP/1.1 204 No Content
-X-Content-Type-Options: nosniff
-X-XSS-Protection: 0
-Cache-Control: no-cache, no-store, max-age=0, must-revalidate
-Pragma: no-cache
-Expires: 0
-
-
-
-
-
-
-

보호소 마이 페이지 조회

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

+ reviews

+ Array

리뷰 리스트

reviews[].reviewId +

+ Number

리뷰 ID

reviews[].reviewCreatedAt +

+ String

리뷰 생성일

reviews[].reviewContent +

+ String

리뷰 내용

reviews[].reviewImageUrls +

+ Array

리뷰 이미지 url + 리스트

reviews[].volunteerEmail +

+ String

봉사자 이메일

reviews[].volunteerTemperature +

+ Number

봉사자 온도

+ pageInfo

+ Object

페이지 정보

pageInfo.totalElements +

+ Number

총 요소 개수

pageInfo.hasNext +

+ Boolean

다음 페이지 여부

+
+
+
+
+
+
+
+

2. 보호소

+
+
+

보호소 마이 페이지 조회

+
+

Request

+
+
HTTP + request
+
+
GET /api/shelters/me HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzIsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkyMzIsInJvbGUiOiJST0xFX1NIRUxURVIifQ.IYP90yhFJk0RbULVJM00oNvAqfjvmwGjLws3esYyLZA1i_98B0p7p93Jyc48qDLj
-X-CSRF-TOKEN: ucbWnkSn3q59U0XtnziP7imBeCGEjdSzgry_TjhLLxT4sDpB2qXi-yKRvZZQanSPrBW73Bm4VRm37raeut_dKg8tH3XO1Ap1
+Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjYsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkxMjYsInJvbGUiOiJST0xFX1NIRUxURVIifQ.btjnyyW5xiSbfmQFdq1aD1TEDNJkvPDgA5OQdoEeKiYqeUa22kIYzh96jHUoo5E8
+X-CSRF-TOKEN: L5rVcPcjN4ke_dLScpmiWR3KhdA16ekPoJ648E26Hc4im3HJFq63EsAVDuwzn7TiRLSWbn6pqLIE2Nwilvzdw3mNeP5AqBT9
 Host: localhost:8080
-
-
-
-
-
Request headers
- ---- - - - - - - - - - - - - -
NameDescription

Authorization

보호소 액세스 토큰

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Request + headers
+ + + + + + + + + + + + + + + + + +
NameDescription

Authorization +

보호소 액세스 토큰

+
+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 X-Content-Type-Options: nosniff
@@ -1820,207 +3622,266 @@ 
-
-
-
-
-
Response fields
- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathTypeDescription

shelterId

Number

보호소 ID

shelterName

String

보호소 이름

shelterEmail

String

보호소 이메일

shelterAddress

String

보호소 주소

shelterAddressDetail

String

보호소 상세주소

shelterIsOpenedAddress

Boolean

보호소 상세 주소 공개 여부

shelterPhoneNumber

String

보호소 전화번호

shelterSparePhoneNumber

String

보호소 임시 전화번호

shelterImageUrl

String

보호소 이미지 Url

-
-
-
-
-
-
-

3. 봉사 모집

-
-
-

1) 봉사 모집글 등록

-
-

Request

-
-
HTTP request
-
-
+
+
+
+
+
Response + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

+ shelterId

+ Number

보호소 ID

shelterName +

+ String

보호소 이름

shelterEmail +

+ String

보호소 이메일

shelterAddress +

+ String

보호소 주소

shelterAddressDetail +

+ String

보호소 상세주소

+

shelterIsOpenedAddress +

+ Boolean

보호소 상세 주소 공개 + 여부

shelterPhoneNumber +

+ String

보호소 전화번호

+

shelterSparePhoneNumber +

+ String

보호소 임시 전화번호

+

shelterImageUrl +

+ String

보호소 이미지 Url

+
+
+
+
+
+
+
+

3. 봉사 모집

+
+
+

1) 봉사 모집글 등록

+
+

Request

+
+
HTTP + request
+
+
POST /api/shelters/recruitments HTTP/1.1
 Content-Type: application/json;charset=UTF-8
-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgyMzIsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkyMzIsInJvbGUiOiJST0xFX1NIRUxURVIifQ.IYP90yhFJk0RbULVJM00oNvAqfjvmwGjLws3esYyLZA1i_98B0p7p93Jyc48qDLj
+Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJpc3N1ZXIiLCJpYXQiOjE2OTk1MTgxMjYsInN1YiI6IjEiLCJleHAiOjE2OTk1MTkxMjYsInJvbGUiOiJST0xFX1NIRUxURVIifQ.btjnyyW5xiSbfmQFdq1aD1TEDNJkvPDgA5OQdoEeKiYqeUa22kIYzh96jHUoo5E8
 Content-Length: 223
-X-CSRF-TOKEN: wy7XI0frSl0X1dy3d_pyTmskhzQO42-pJPMRXZuGoKDpK9Pq8hfhRyTbeT864brUR9dGfwkWqgw72l2EFMQoZa_jlJKNTrKL
+X-CSRF-TOKEN: CgdcQ5ZcC72nzlwPK5haQq3Rcm0MWM4DDNNwvDUdwyDa_2lWOmJsIPRpb4WK_24-GrVuJ5_hXw9tO_guP-tGhFcs9RTvzApu
 Host: localhost:8080
 
 {
   "title" : "title",
-  "startTime" : "2023-11-10T17:23:52.147347",
-  "endTime" : "2023-11-10T18:23:52.147347",
-  "deadline" : "2023-11-09T22:23:52.147347",
+  "startTime" : "2023-11-10T17:22:06.192548",
+  "endTime" : "2023-11-10T18:22:06.192548",
+  "deadline" : "2023-11-09T22:22:06.192548",
   "capacity" : 10,
   "content" : "content",
   "imageUrls" : [ ]
 }
-
-
-
-
-
Request headers
- ---- - - - - - - - - - - - - -
NameDescription

Authorization

보호소 액세스 토큰

-
-
-
Request fields
- ------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필드명타입필수값제약설명

title

String

true

1자 이상, 50자 이하

봉사 모집글 제목

startTime

String

true

yyyy-MM-dd’T’HH:mm:ss

봉사 시작 시간

endTime

String

true

yyyy-MM-dd’T’HH:mm:ss

봉사 종료 시간

deadline

String

true

yyyy-MM-dd’T’HH:mm:ss

봉사 모집 마감 시간

capacity

Number

true

1명 이상, 99명 이하

봉사 모집 정원

content

String

true

1자 이상, 1000자 이하

봉사 모집글 본문

imageUrls

Array

0장 이상, 5장 이하

봉사 모집글 이미지

-
-
-
-

Response

-
-
HTTP response
-
-
+
+
+
+
+
Request + headers
+ + + + + + + + + + + + + + + + + +
NameDescription

Authorization +

보호소 액세스 토큰

+
+
+
+
Request + fields
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
필드명타입필수값제약설명

+ title

+ String

true

1자 이상, 50자 + 이하

봉사 모집글 제목

+

+ startTime

+ String

true

yyyy-MM-dd’T’HH:mm:ss

+

봉사 시작 시간

+

+ endTime

+ String

true

yyyy-MM-dd’T’HH:mm:ss

+

봉사 종료 시간

+

+ deadline

+ String

true

yyyy-MM-dd’T’HH:mm:ss

+

봉사 모집 마감 시간

+

+ capacity

+ Number

true

1명 이상, 99명 + 이하

봉사 모집 정원

+

+ content

+ String

true

1자 이상, 1000자 + 이하

봉사 모집글 본문

+

+ imageUrls

+ Array

0장 이상, 5장 이하

+

봉사 모집글 이미지

+
+
+
+
+

Response

+
+
HTTP + response
+
+
HTTP/1.1 201 Created
 Location: /api/recruitments/1
 X-Content-Type-Options: nosniff
@@ -2028,273 +3889,295 @@ 
-
Response headers
- ---- - - - - - - - - - - - - -
NameDescription

Location

생성된 리소스에 대한 접근 api

-
-
-
-
-
- +
+
+
Response + headers
+ + + + + + + + + + + + + + + + + +
NameDescription

+ Location

생성된 리소스에 대한 접근 + api

+
+
+
+
+
+ - + - + -

봉사자

-
-

1. 봉사자

-
+
+
+

봉사자

+
+

1. 봉사자

+
-
-
-
-

2. 보호소

-
+
+
+
+

2. 보호소

+
-
-
- + - + - + - + -

Enum 문서화

-
-

1. 보호 동물 성격

-
-
-

|보호 동물 성격

-
- ---- - - - - - - - - - - - - - - - - - - - - - - - - -
코드코드명

QUIET

QUIET

NORMAL

NORMAL

ACTIVE

ACTIVE

VERY_ACTIVE

VERY_ACTIVE

-
-
-
-

2. 보호 동물 성별

-
-
-

|보호 동물 성별

-
- ---- - - - - - - - - - - - - - - - - -
코드코드명

MALE

MALE

FEMALE

FEMALE

-
-
-
-

3. 보호 동물 종류

-
-
-

|보호 동물 종류

-
- ---- - - - - - - - - - - - - - - - - - - - - -
코드코드명

DOG

DOG

CAT

CAT

ETC

ETC

-
-
-
-

4. 봉사 신청자 상태

-
-
-

|보호 동물 종류

-
- ---- - - - - - - - - - - - - - - - - - - - - -
코드코드명

DOG

DOG

CAT

CAT

ETC

ETC

-
-
-
-

5. 봉사자 성별

-
-
-

|보호 동물 종류

-
- ---- - - - - - - - - - - - - - - - - - - - - -
코드코드명

DOG

DOG

CAT

CAT

ETC

ETC

-
-
+
+
+

Enum 문서화

+
+

1. 보호 동물 성격

+
+
+

|보호 동물 성격

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
코드코드명

QUIET

+

QUIET

NORMAL +

NORMAL

ACTIVE +

ACTIVE

+ VERY_ACTIVE

VERY_ACTIVE

+
+
+
+

2. 보호 동물 성별

+
+
+

|보호 동물 성별

+
+ + + + + + + + + + + + + + + + + + + + + +
코드코드명

MALE

+

MALE

FEMALE +

FEMALE

+
+
+
+

3. 보호 동물 종류

+
+
+

|보호 동물 종류

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
코드코드명

DOG

+

DOG

CAT

+

CAT

ETC

+

ETC

+
+
+
+

4. 봉사 신청자 상태

+
+
+

|보호 동물 종류

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
코드코드명

DOG

+

DOG

CAT

+

CAT

ETC

+

ETC

+
+
+
+

5. 봉사자 성별

+
+
+

|보호 동물 종류

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
코드코드명

DOG

+

DOG

CAT

+

CAT

ETC

+

ETC

+
+
\ No newline at end of file diff --git a/src/test/java/com/clova/anifriends/base/BaseControllerTest.java b/src/test/java/com/clova/anifriends/base/BaseControllerTest.java index ab2f5af2c..d15afa63e 100644 --- a/src/test/java/com/clova/anifriends/base/BaseControllerTest.java +++ b/src/test/java/com/clova/anifriends/base/BaseControllerTest.java @@ -21,6 +21,7 @@ import com.clova.anifriends.domain.volunteer.service.VolunteerService; import com.clova.anifriends.global.config.SecurityConfig; import com.clova.anifriends.global.config.WebMvcConfig; +import com.clova.anifriends.global.image.S3Service; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Properties; import org.junit.jupiter.api.BeforeAll; @@ -100,6 +101,9 @@ public JwtAuthenticationProvider jwtAuthenticationProvider(JwtProvider jwtProvid @MockBean protected ReviewService reviewService; + @MockBean + protected S3Service s3Service; + protected final String volunteerAccessToken = AuthFixture.volunteerAccessToken(); protected String shelterAccessToken = AuthFixture.shelterAccessToken(); diff --git a/src/test/java/com/clova/anifriends/global/image/ImageControllerTest.java b/src/test/java/com/clova/anifriends/global/image/ImageControllerTest.java new file mode 100644 index 000000000..a8df8687b --- /dev/null +++ b/src/test/java/com/clova/anifriends/global/image/ImageControllerTest.java @@ -0,0 +1,55 @@ +package com.clova.anifriends.global.image; + +import static java.sql.JDBCType.ARRAY; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.clova.anifriends.base.BaseControllerTest; +import com.clova.anifriends.docs.format.DocumentationFormatGenerator; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.multipart.MultipartFile; + +class ImageControllerTest extends BaseControllerTest { + + @Test + @DisplayName("이미지 업로드 API 호출 시") + void uploadImages() throws Exception { + // given + List imageFiles = List.of( + new MockMultipartFile("test1", "test1.PNG", MediaType.IMAGE_PNG_VALUE, "test1".getBytes()), + new MockMultipartFile("test2", "test2.PNG", MediaType.IMAGE_PNG_VALUE, "test2".getBytes()) + ); + + // when + ResultActions resultActions = mockMvc.perform( + multipart("/api/images") + .file("images", imageFiles.get(0).getBytes()) + .file("images", imageFiles.get(1).getBytes()) + .with(requestPostProcessor -> { + requestPostProcessor.setMethod("POST"); + return requestPostProcessor; + }) + .contentType(MediaType.MULTIPART_FORM_DATA)); + + // then + resultActions.andExpect(status().isOk()) + .andDo(restDocs.document( + requestParts( + partWithName("images").description("이미지 파일") + .attributes(DocumentationFormatGenerator.getConstraint("이미지 파일은 1 이상 5이하")) + ), + responseFields( + fieldWithPath("imageUrls").type(ARRAY).description("이미지 URL 목록") + ) + )); + } +} diff --git a/src/test/java/com/clova/anifriends/global/image/S3ServiceTest.java b/src/test/java/com/clova/anifriends/global/image/S3ServiceTest.java new file mode 100644 index 000000000..d7a2f1d98 --- /dev/null +++ b/src/test/java/com/clova/anifriends/global/image/S3ServiceTest.java @@ -0,0 +1,123 @@ +package com.clova.anifriends.global.image; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.s3.AmazonS3; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +class S3ServiceTest { + + @Mock + private AmazonS3 amazonS3; + + private S3Service s3Service; + + @BeforeEach + void setUp() { + amazonS3 = Mockito.mock(AmazonS3.class); + s3Service = new S3Service(amazonS3); + ReflectionTestUtils.setField(s3Service, "bucket", "bucket-name"); + } + + @Nested + @DisplayName("uploadImages 메서드 실행 시") + class UploadImagesTest { + + @Test + @DisplayName("성공") + void testUploadImages() { + // given + MockMultipartFile file1 = new MockMultipartFile("file1", "test1.jpg", "image/jpeg", "file content".getBytes()); + MockMultipartFile file2 = new MockMultipartFile("file2", "test2.jpg", "image/jpeg", "file content".getBytes()); + List expectedUrls = Arrays.asList( + "https://example.com/bucket-name/images/random-uuid.jpg", + "https://example.com/bucket-name/images/random-uuid.jpg" + ); + + when(amazonS3.putObject(any())).thenReturn(null); + + when(amazonS3.getUrl(any(), any())) + .thenAnswer(invocation -> { + String bucketName = invocation.getArgument(0); + String fileName = invocation.getArgument(1); + return new java.net.URL("https", "example.com", "/" + bucketName + "/" + fileName); + }); + + // when + List uploadedUrls = s3Service.uploadImages(Arrays.asList(file1, file2)); + + // then + assertThat(uploadedUrls.size()).isEqualTo(expectedUrls.size()); + } + + @Test + @DisplayName("예외(S3BadRequestException): 파일이 확장자가 잘못된 경우") + void throwExceptionWhenFileExtensionIsWrong() { + // given + MockMultipartFile file1 = new MockMultipartFile("file1", "test1.abcd", "image/jpeg", "file content".getBytes()); + MockMultipartFile file2 = new MockMultipartFile("file2", "test2.sdf", "image/jpeg", "file content".getBytes()); + List expectedUrls = Arrays.asList( + "https://example.com/bucket-name/images/random-uuid.jpg", + "https://example.com/bucket-name/images/random-uuid.jpg" + ); + + when(amazonS3.putObject(any())).thenReturn(null); + + when(amazonS3.getUrl(any(), any())) + .thenAnswer(invocation -> { + String bucketName = invocation.getArgument(0); + String fileName = invocation.getArgument(1); + return new java.net.URL("https", "example.com", "/" + bucketName + "/" + fileName); + }); + + // when + Exception exception = catchException( + () -> s3Service.uploadImages(Arrays.asList(file1, file2)) + ); + + // then + assertThat(exception).isInstanceOf(S3BadRequestException.class); + } + + @Test + @DisplayName("예외(S3BadRequestException): 파일 이름 길이가 0인 경우") + void throwExceptionWhenFileNameLengthIsZero() { + // given + MockMultipartFile file1 = new MockMultipartFile("file1", "", "image/jpeg", "file content".getBytes()); + MockMultipartFile file2 = new MockMultipartFile("file2", "", "image/jpeg", "file content".getBytes()); + List expectedUrls = Arrays.asList( + "https://example.com/bucket-name/images/random-uuid.jpg", + "https://example.com/bucket-name/images/random-uuid.jpg" + ); + + when(amazonS3.putObject(any())).thenReturn(null); + + when(amazonS3.getUrl(any(), any())) + .thenAnswer(invocation -> { + String bucketName = invocation.getArgument(0); + String fileName = invocation.getArgument(1); + return new java.net.URL("https", "example.com", "/" + bucketName + "/" + fileName); + }); + + // when + Exception exception = catchException( + () -> s3Service.uploadImages(Arrays.asList(file1, file2)) + ); + + // then + assertThat(exception).isInstanceOf(S3BadRequestException.class); + } + } +}