Skip to content

Commit

Permalink
Merge pull request #80 from reading-log/feature/#78-swagger
Browse files Browse the repository at this point in the history
회원, 북로그 도메인 Swagger API 문서화
  • Loading branch information
enjoy89 authored Apr 2, 2024
2 parents 8d18dfe + 59388d2 commit 920383a
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 19 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,19 +23,32 @@
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")
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<BookLogResponse> 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<Page<SummaryResponse>> bookLogs(@PageableDefault(sort = "createdAt", direction = Direction.DESC)
Pageable pageable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.api.readinglog.domain.email.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import lombok.Getter;

@Getter
public class AuthCodeVerificationRequest {

@Email(message = "이메일 형식이 올바르지 않습니다.")
@Schema(description = "인증에 사용한 이메일")
private String email;

@Schema(description = "이메일로 발급 받은 인증 코드")
private String authCode;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.api.readinglog.domain.email.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import lombok.Getter;

@Getter
public class EmailRequest {

@Email(message = "이메일 형식이 올바르지 않습니다.")
@Schema(description = "사용자 이메일")
private String email;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")
Expand All @@ -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<Void> 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 = "[email protected]", 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<Void> 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<Void> 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<MemberDetailsResponse> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> sendEmailTempPassword(@AuthenticationPrincipal CustomUserDetail user,
@RequestBody @Valid EmailRequest request) {
emailService.sendTemporaryPassword(user.getId(), request.getEmail());
return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료!");
return Response.success(HttpStatus.OK, "임시 비밀번호 전송 완료");
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,5 +9,7 @@
@NoArgsConstructor
@AllArgsConstructor
public class DeleteRequest {

@Schema(description = "회원 탈퇴에 필요한 비밀번호")
private String password;
}
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]")
private String email;

@Schema(description = "비밀번호", example = "Password123!")
private String password;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

0 comments on commit 920383a

Please sign in to comment.