From c0070772f5a0447ca4f5a0a7e1efba3668057e65 Mon Sep 17 00:00:00 2001 From: jeondui Date: Tue, 2 Apr 2024 18:34:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=20API=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/AuthCodeVerificationRequest.java | 3 + .../domain/email/dto/EmailRequest.java | 2 + .../member/controller/MemberController.java | 131 +++++++++++++++--- .../controller/dto/request/DeleteRequest.java | 3 + .../controller/dto/request/LoginRequest.java | 4 + .../dto/request/UpdatePasswordRequest.java | 3 + 6 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java b/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java index 343afdc..df526e4 100644 --- a/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java +++ b/src/main/java/com/api/readinglog/domain/email/dto/AuthCodeVerificationRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.email.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import lombok.Getter; @@ -7,7 +8,9 @@ public class AuthCodeVerificationRequest { @Email(message = "이메일 형식이 올바르지 않습니다.") + @Schema(description = "인증에 사용한 이메일") private String email; + @Schema(description = "이메일로 발급 받은 인증 코드") private String authCode; } diff --git a/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java b/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java index 56e4aad..c49aa94 100644 --- a/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java +++ b/src/main/java/com/api/readinglog/domain/email/dto/EmailRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.email.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import lombok.Getter; @@ -7,5 +8,6 @@ public class EmailRequest { @Email(message = "이메일 형식이 올바르지 않습니다.") + @Schema(description = "사용자 이메일") private String email; } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java index 5d6891c..1f71ea5 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/MemberController.java @@ -16,7 +16,12 @@ import com.api.readinglog.domain.member.controller.dto.response.MemberDetailsResponse; import com.api.readinglog.domain.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -33,7 +38,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Member", description = "Member API") +@Tag(name = "Members", description = "회원 API 목록입니다.") @Slf4j @RestController @RequestMapping("/api/members") @@ -43,96 +48,184 @@ public class MemberController { private final MemberService memberService; private final EmailService emailService; + @Operation(summary = "닉네임 중복 검사", description = "회원 가입 전, 닉네임 중복을 검사합니다.", + parameters = { + @Parameter(name = "nickname", description = "닉네임", required = true, + schema = @Schema(type = "string", implementation = String.class)) + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "닉네임 중복 검사 통과", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "닉네임 중복 검사 실패") + }) @PostMapping("/join-nickname") public Response join_nickname(@ModelAttribute @Valid JoinNicknameRequest request) { memberService.joinNickname(request); - return Response.success(HttpStatus.OK, "닉네임 검사 통과!"); + return Response.success(HttpStatus.OK, "닉네임 중복 검사 통과"); } - @Operation(summary = "Create member", description = "일반 회원가입") + @Operation(summary = "회원 가입", description = "일반 이메일 회원 가입입니다.", + parameters = { + @Parameter(name = "email", description = "이메일", example = "test@test.com", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "password", description = "비밀번호", example = "Password123!", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "passwordConfirm", description = "비밀번호 확인", example = "Password123!", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "nickname", description = "닉네임", example = "테스트닉네임", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "profileImage", description = "프로필 이미지", required = false, + schema = @Schema(type = "string", implementation = String.class)), + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "회원 가입 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "회원 가입 실패 (필수 입력값을 입력하지 않은 경우, 비밀번호와 비밀번호 확인이 일치하지 않는 경우)") + }) @PostMapping("/join") public Response join(@ModelAttribute @Valid JoinRequest request) { memberService.join(request); - return Response.success(HttpStatus.CREATED, "회원 가입 완료"); + return Response.success(HttpStatus.CREATED, "회원 가입 성공"); } - @Operation(summary = "Login member into the system", description = "일반 로그인") + @Operation(summary = "로그인", description = "일반 로그인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "401", description = "로그인 실패: 인증에 실패하였습니다.") + }) @PostMapping("/login") public Response login(@RequestBody LoginRequest request, HttpServletResponse response) { JwtToken jwtToken = memberService.login(request); response.addHeader("Authorization", jwtToken.getAccessToken()); CookieUtils.addCookie(response, "refreshToken", jwtToken.getRefreshToken(), 24 * 60 * 60 * 7); - return Response.success(HttpStatus.OK, "로그인 성공!"); + return Response.success(HttpStatus.OK, "로그인 성공"); } + @Operation(summary = "회원 정보 조회", description = "인증 토큰을 사용하여 회원 정보를 조회합니다.") @GetMapping("/me") public Response findMember(@AuthenticationPrincipal CustomUserDetail user) { MemberDetailsResponse member = memberService.getMemberDetails(user.getId()); - return Response.success(HttpStatus.OK, "회원 조회 성공!", member); + return Response.success(HttpStatus.OK, "회원 조회 성공", member); } - @Operation(summary = "Updated member", description = "회원정보 수정") + @Operation(summary = "회원 정보 수정", description = "회원 정보를 수정합니다.", + parameters = { + @Parameter(name = "nickname", description = "닉네임", example = "새로운닉네임", required = true, + schema = @Schema(type = "string", implementation = String.class)), + @Parameter(name = "profileImage", description = "프로필 이미지", required = false, + schema = @Schema(type = "string", implementation = String.class)) + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회원 수정 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "401", description = "로그인 실패: 인증에 실패하였습니다.") + }) @PatchMapping("/me") public Response updateProfile(@AuthenticationPrincipal CustomUserDetail user, @ModelAttribute @Valid UpdateProfileRequest request) { memberService.updateProfile(user.getId(), request); - return Response.success(HttpStatus.OK, "회원 수정 성공!"); + return Response.success(HttpStatus.OK, "회원 수정 성공"); } - @Operation(summary = "Logout member into the system", description = "로그아웃") + @Operation(summary = "로그아웃", description = "쿠키에 저장된 리프레시 토큰을 통해 로그아웃 합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "리프레시 토큰이 쿠키에 없습니다.") + }) @PostMapping("/logout") public Response logout(HttpServletRequest request, HttpServletResponse response) { String refreshToken = CookieUtils.extractRefreshToken(request); memberService.logout(refreshToken, response); - return Response.success(HttpStatus.OK, "로그아웃 성공!"); + return Response.success(HttpStatus.OK, "로그아웃 성공"); } - @Operation(summary = "Deleted member", description = "일반 회원 탈퇴") + @Operation(summary = "일반 회원 탈퇴", description = "일반 회원은 비밀번호 확인을 통해 회원 검증 후, 서비스를 탈퇴할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "일반 회원 탈퇴 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "회원이 존재하지 않습니다!") + }) @DeleteMapping("/me") public Response deleteMember(@AuthenticationPrincipal CustomUserDetail user, @RequestBody DeleteRequest request) { memberService.deleteMember(user.getId(), request); - return Response.success(HttpStatus.OK, "일반 회원 탈퇴 성공!"); + return Response.success(HttpStatus.OK, "일반 회원 탈퇴 성공"); } + @Operation(summary = "소셜 회원 탈퇴", description = "소셜 회원은 재로그인을 통해 회원 검증 후, 재발급 받은 액세스 토큰을 통해 서비스를 탈퇴할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "소셜 회원 탈퇴 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "회원이 존재하지 않습니다!") + }) @DeleteMapping("/social/me") public Response deleteSocialMember(@AuthenticationPrincipal CustomUserDetail user) { memberService.deleteSocialMember(user.getId()); - return Response.success(HttpStatus.OK, "소셜 회원 탈퇴 성공!"); + return Response.success(HttpStatus.OK, "소셜 회원 탈퇴 성공"); } + @Operation(summary = "토큰 재발급", description = "액세스 토큰이 만료된 경우, 리프레시 토큰을 이용하여 재발급 받을 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "리프레시 토큰이 쿠키에 없습니다.") + }) @GetMapping("/reissue") public Response reissue(HttpServletRequest request, HttpServletResponse response) { String refreshToken = CookieUtils.extractRefreshToken(request); JwtToken newToken = memberService.reissueToken(refreshToken); response.addHeader("Authorization", newToken.getAccessToken()); CookieUtils.addCookie(response, "refreshToken", newToken.getRefreshToken(), 24 * 60 * 60 * 7); - return Response.success(HttpStatus.OK, "토큰 재발급 성공!"); + return Response.success(HttpStatus.OK, "토큰 재발급 성공"); } + @Operation(summary = "비밀번호 변경", description = "비밀번호 변경입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "비밀번호 변경 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "비밀번호 변경 실패") + }) @PatchMapping("/password") public Response updatePassword(@AuthenticationPrincipal CustomUserDetail user, @RequestBody @Valid UpdatePasswordRequest request) { memberService.updatePassword(user.getId(), request); - return Response.success(HttpStatus.OK, "비밀번호 변경 성공!"); + return Response.success(HttpStatus.OK, "비밀번호 변경 성공"); } + @Operation(summary = "이메일 인증 코드 전송", description = "사용자 이메일로 인증 코드를 전송합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "이메일 인증 코드 전송 완료", + content = {@Content(schema = @Schema(implementation = Response.class))}) + }) @PostMapping("/send-authCode") public Response sendEmailAuthCode(@RequestBody @Valid EmailRequest request) { emailService.sendAuthCode(request.getEmail()); - return Response.success(HttpStatus.OK, "이메일 인증 코드 전송 완료!"); + return Response.success(HttpStatus.OK, "이메일 인증 코드 전송 완료"); } + @Operation(summary = "이메일 인증", description = "사용자 이메일로 보낸 인증 코드를 검증하여 이메일을 인증합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "이메일 인증 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "이메일 인증에 실패하였습니다.") + }) @PostMapping("/verify-authCode") public Response verifyAuthCode(@RequestBody @Valid AuthCodeVerificationRequest request) { emailService.verifyAuthCode(request.getEmail(), request.getAuthCode()); - return Response.success(HttpStatus.OK, "이메일 인증 성공!"); + return Response.success(HttpStatus.OK, "이메일 인증 성공"); } + @Operation(summary = "임시 비밀번호 전송", description = "사용자 이메일로 임시 비밀번호를 전송합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "임시 비밀번호 전송 완료", + content = {@Content(schema = @Schema(implementation = Response.class))}) + }) @PostMapping("/send-temporaryPassword") public Response sendEmailTempPassword(@AuthenticationPrincipal CustomUserDetail user, @RequestBody @Valid EmailRequest request) { emailService.sendTemporaryPassword(user.getId(), request.getEmail()); - return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료!"); + return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료"); } } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java index b4b4d1f..0345890 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/DeleteRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.member.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,5 +9,7 @@ @NoArgsConstructor @AllArgsConstructor public class DeleteRequest { + + @Schema(description = "회원 탈퇴에 필요한 비밀번호") private String password; } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java index 13a0516..0d1405f 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/LoginRequest.java @@ -1,10 +1,14 @@ package com.api.readinglog.domain.member.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter public class LoginRequest { + @Schema(description = "이메일", example = "test@test.com") private String email; + + @Schema(description = "비밀번호", example = "Password123!") private String password; } diff --git a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java index 00ee8c6..2099f4e 100644 --- a/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java +++ b/src/main/java/com/api/readinglog/domain/member/controller/dto/request/UpdatePasswordRequest.java @@ -1,5 +1,6 @@ package com.api.readinglog.domain.member.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Getter; @@ -14,8 +15,10 @@ public class UpdatePasswordRequest { @NotBlank(message = "비밀번호는 필수 입력 값입니다.") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&]).{8,20}$", message = "비밀번호는 8~20자의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다.") + @Schema(description = "새로운 비밀번호") private String newPassword; @NotBlank(message = "비밀번호 확인은 필수입니다.") + @Schema(description = "새로운 비밀번호 확인") private String newPasswordConfirm; } From 59388d2fa9d8315a4ab1b3549d98f030e38dce7e Mon Sep 17 00:00:00 2001 From: jeondui Date: Tue, 2 Apr 2024 18:35:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat:=20=EB=B6=81=EB=A1=9C=EA=B7=B8=20API?= =?UTF-8?q?=20=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../booklog/controller/BookLogController.java | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/build.gradle b/build.gradle index 3db55e1..168305e 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,9 @@ dependencies { /* Redis */ implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + /* Swagger */ + implementation group: 'io.swagger.core.v3', name: 'swagger-core-jakarta', version: '2.2.7' } tasks.named('bootBuildImage') { diff --git a/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java b/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java index 881ff84..9f0549f 100644 --- a/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java +++ b/src/main/java/com/api/readinglog/domain/booklog/controller/BookLogController.java @@ -5,6 +5,12 @@ import com.api.readinglog.domain.booklog.controller.dto.BookLogResponse; import com.api.readinglog.domain.booklog.service.BookLogService; import com.api.readinglog.domain.summary.controller.dto.response.SummaryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -17,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "BookLogs", description = "북로그 API 목록입니다.") @RestController @RequiredArgsConstructor @RequestMapping("/api/book-logs") @@ -24,12 +31,24 @@ public class BookLogController { private final BookLogService bookLogService; + @Operation(summary = "나의 로그 조회", description = "인증 토큰을 통해 특정 사용자가 작성한 로그 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "나의 로그 조회 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "400", description = "나의 로그 조회 실패") + }) @GetMapping("/{bookId}/me") public Response myLogs(@AuthenticationPrincipal CustomUserDetail user, @PathVariable Long bookId) { return Response.success(HttpStatus.OK, "나의 로그 조회 성공", bookLogService.myLogs(user.getId(), bookId)); } + @Operation(summary = "북로그 조회", description = "리딩 로그 서비스의 모든 북로그를 조회합니다. 비회원도 조회가 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "북로그 조회 성공", + content = {@Content(schema = @Schema(implementation = Response.class))}), + @ApiResponse(responseCode = "404", description = "북로그 목록이 존재하지 않습니다!") + }) @GetMapping public Response> bookLogs(@PageableDefault(sort = "createdAt", direction = Direction.DESC) Pageable pageable) {