diff --git a/backend/src/docs/asciidoc/auth.adoc b/backend/src/docs/asciidoc/auth.adoc new file mode 100644 index 000000000..17900fe72 --- /dev/null +++ b/backend/src/docs/asciidoc/auth.adoc @@ -0,0 +1,7 @@ +==== 깃허브로 로그인/회원가입 + +operation::github-auth[snippets="curl-request,request-fields,http-response"] + +==== 로그아웃 + +operation::logout[snippets="curl-request,request-cookies,http-response"] diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 4d67754a7..0575b8a24 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -40,3 +40,11 @@ include::review-gather.adoc[] === 답변 하이라이트 include::highlight-answers.adoc[] + +== 인증 + +include::auth.adoc[] + +== 사용자 + +include::member.adoc[] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc new file mode 100644 index 000000000..9a51e2c94 --- /dev/null +++ b/backend/src/docs/asciidoc/member.adoc @@ -0,0 +1,3 @@ +==== 내 프로필 정보 + +operation::my-profile[snippets="curl-request,request-cookies,http-response,response-fields"] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/reviewgroup.adoc b/backend/src/docs/asciidoc/reviewgroup.adoc index c3eb2f803..79d70a9f5 100644 --- a/backend/src/docs/asciidoc/reviewgroup.adoc +++ b/backend/src/docs/asciidoc/reviewgroup.adoc @@ -9,3 +9,7 @@ operation::review-group-summary[snippets="curl-request,http-response,response-fi ==== 리뷰 요청 코드, 확인 코드 일치 여부 operation::review-group-check-access[snippets="curl-request,request-fields,http-response,response-cookies"] + +==== 자신이 만든 리뷰 그룹 목록 조회 + +operation::review-group-list[snippets="curl-request,request-cookies,http-response,response-fields"] diff --git a/backend/src/main/java/reviewme/auth/controller/AuthController.java b/backend/src/main/java/reviewme/auth/controller/AuthController.java new file mode 100644 index 000000000..14573f2fe --- /dev/null +++ b/backend/src/main/java/reviewme/auth/controller/AuthController.java @@ -0,0 +1,33 @@ +package reviewme.auth.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reviewme.auth.service.AuthService; +import reviewme.auth.service.dto.GithubCodeRequest; + +@RestController +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/v2/auth/github") + public ResponseEntity authWithGithub( + @Valid @RequestBody GithubCodeRequest request, + HttpServletRequest httpRequest + ) { + return ResponseEntity.ok().build(); + } + + @PostMapping("/v2/auth/logout") + public ResponseEntity logout( + HttpServletRequest httpRequest + ) { + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/reviewme/auth/service/AuthService.java b/backend/src/main/java/reviewme/auth/service/AuthService.java new file mode 100644 index 000000000..5458807af --- /dev/null +++ b/backend/src/main/java/reviewme/auth/service/AuthService.java @@ -0,0 +1,7 @@ +package reviewme.auth.service; + +import org.springframework.stereotype.Service; + +@Service +public class AuthService { +} diff --git a/backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java b/backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java new file mode 100644 index 000000000..b26511917 --- /dev/null +++ b/backend/src/main/java/reviewme/auth/service/dto/GithubCodeRequest.java @@ -0,0 +1,8 @@ +package reviewme.auth.service.dto; + +import jakarta.validation.constraints.NotBlank; + +public record GithubCodeRequest( + @NotBlank(message = "깃허브 임시 코드를 입력해주세요.") + String code) { +} diff --git a/backend/src/main/java/reviewme/member/controller/MemberController.java b/backend/src/main/java/reviewme/member/controller/MemberController.java new file mode 100644 index 000000000..9d95885df --- /dev/null +++ b/backend/src/main/java/reviewme/member/controller/MemberController.java @@ -0,0 +1,21 @@ +package reviewme.member.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reviewme.member.service.MemberService; +import reviewme.member.service.dto.ProfileResponse; + +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @GetMapping("/v2/members/profile/mine") + public ResponseEntity getProfile() { + ProfileResponse response = memberService.getProfile(); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/reviewme/member/service/MemberService.java b/backend/src/main/java/reviewme/member/service/MemberService.java new file mode 100644 index 000000000..ef828b848 --- /dev/null +++ b/backend/src/main/java/reviewme/member/service/MemberService.java @@ -0,0 +1,12 @@ +package reviewme.member.service; + +import org.springframework.stereotype.Service; +import reviewme.member.service.dto.ProfileResponse; + +@Service +public class MemberService { + + public ProfileResponse getProfile() { + return null; + } +} diff --git a/backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java b/backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java new file mode 100644 index 000000000..5ec6900cd --- /dev/null +++ b/backend/src/main/java/reviewme/member/service/dto/ProfileResponse.java @@ -0,0 +1,7 @@ +package reviewme.member.service.dto; + +public record ProfileResponse( + String nickname, + String profileImageUrl +) { +} diff --git a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java index b6c7a973c..e48a76a74 100644 --- a/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java +++ b/backend/src/main/java/reviewme/reviewgroup/controller/ReviewGroupController.java @@ -15,6 +15,7 @@ import reviewme.reviewgroup.service.dto.CheckValidAccessRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; +import reviewme.reviewgroup.service.dto.ReviewGroupListResponse; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; @RestController @@ -48,4 +49,11 @@ public ResponseEntity checkGroupAccessCode( session.setAttribute("reviewRequestCode", request.reviewRequestCode()); return ResponseEntity.noContent().build(); } + + @GetMapping("/v2/groups/mine") + public ResponseEntity getMyReviewGroups() { + // TODO: 세션을 활용한 권한 체계에 따른 추가 조치 필요 + ReviewGroupListResponse response = reviewGroupLookupService.getMyReviewGroups(); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java index 0567f6344..e2d9a3d82 100644 --- a/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java +++ b/backend/src/main/java/reviewme/reviewgroup/service/ReviewGroupLookupService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import reviewme.reviewgroup.service.dto.ReviewGroupListResponse; import reviewme.reviewgroup.service.exception.ReviewGroupNotFoundByReviewRequestCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; @@ -21,4 +22,9 @@ public ReviewGroupResponse getReviewGroupSummary(String reviewRequestCode) { return new ReviewGroupResponse(reviewGroup.getReviewee(), reviewGroup.getProjectName()); } + + public ReviewGroupListResponse getMyReviewGroups() { + // TODO: 생성일자 최신순 정렬 + return null; + } } diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupDetailResponse.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupDetailResponse.java new file mode 100644 index 000000000..d502834f7 --- /dev/null +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupDetailResponse.java @@ -0,0 +1,11 @@ +package reviewme.reviewgroup.service.dto; + +import java.time.LocalDate; + +public record ReviewGroupDetailResponse( + String revieweeName, + String projectName, + LocalDate createdAt, + String reviewRequestCode +) { +} diff --git a/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupListResponse.java b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupListResponse.java new file mode 100644 index 000000000..1963d4390 --- /dev/null +++ b/backend/src/main/java/reviewme/reviewgroup/service/dto/ReviewGroupListResponse.java @@ -0,0 +1,9 @@ +package reviewme.reviewgroup.service.dto; + +import java.util.List; + +public record ReviewGroupListResponse( + boolean isLastPage, + List reviewGroups +) { +} diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index 20d57db83..a656602bb 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -26,8 +26,12 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import reviewme.auth.controller.AuthController; +import reviewme.auth.service.AuthService; import reviewme.highlight.controller.HighlightController; import reviewme.highlight.service.HighlightService; +import reviewme.member.controller.MemberController; +import reviewme.member.service.MemberService; import reviewme.review.controller.ReviewController; import reviewme.review.service.ReviewDetailLookupService; import reviewme.review.service.ReviewGatheredLookupService; @@ -48,7 +52,9 @@ ReviewController.class, TemplateController.class, SectionController.class, - HighlightController.class + HighlightController.class, + MemberController.class, + AuthController.class }) @ExtendWith(RestDocumentationExtension.class) public abstract class ApiTest { @@ -85,6 +91,12 @@ public abstract class ApiTest { @MockBean protected HighlightService highlightService; + @MockBean + protected MemberService memberService; + + @MockBean + protected AuthService authService; + @MockBean private ReviewGroupSessionResolver reviewGroupSessionResolver; diff --git a/backend/src/test/java/reviewme/api/AuthApiTest.java b/backend/src/test/java/reviewme/api/AuthApiTest.java new file mode 100644 index 000000000..3bf6c61c9 --- /dev/null +++ b/backend/src/test/java/reviewme/api/AuthApiTest.java @@ -0,0 +1,59 @@ +package reviewme.api; + +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; + +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.cookies.CookieDescriptor; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; + +public class AuthApiTest extends ApiTest { + + @Test + void 깃허브로_인증한다() { + String request = """ + { + "code": "github_auth_code" + } + """; + + FieldDescriptor[] requestFieldDescriptors = { + fieldWithPath("code").description("깃허브 임시 인증 코드"), + }; + + RestDocumentationResultHandler handler = document( + "github-auth", + requestFields(requestFieldDescriptors) + ); + + givenWithSpec().log().all() + .body(request) + .when().post("/v2/auth/github") + .then().log().all() + .apply(handler) + .statusCode(200); + } + + @Test + void 로그아웃한다() { + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + RestDocumentationResultHandler handler = document( + "logout", + requestCookies(cookieDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "SESSION12345678") + .when().post("/v2/auth/logout") + .then().log().all() + .apply(handler) + .statusCode(204); + } +} diff --git a/backend/src/test/java/reviewme/api/MemberApiTest.java b/backend/src/test/java/reviewme/api/MemberApiTest.java new file mode 100644 index 000000000..ab1da50eb --- /dev/null +++ b/backend/src/test/java/reviewme/api/MemberApiTest.java @@ -0,0 +1,45 @@ +package reviewme.api; + +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; + +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.restdocs.cookies.CookieDescriptor; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import reviewme.member.service.dto.ProfileResponse; + +public class MemberApiTest extends ApiTest { + + @Test + void 내_프로필을_불러온다() { + BDDMockito.given(memberService.getProfile()) + .willReturn(new ProfileResponse("donghoony", "https://aru.image")); + + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("nickname").description("닉네임"), + fieldWithPath("profileImageUrl").description("프로필 이미지 URL") + }; + + RestDocumentationResultHandler handler = document( + "my-profile", + requestCookies(cookieDescriptors), + responseFields(responseFieldDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "SESSION12345678") + .when().get("/v2/members/profile/mine") + .then().log().all() + .apply(handler) + .statusCode(200); + } +} diff --git a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java index e87cb8b5c..ff5fcacac 100644 --- a/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewGroupApiTest.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -12,6 +13,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import java.time.LocalDate; +import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; import org.springframework.restdocs.cookies.CookieDescriptor; @@ -20,6 +23,8 @@ import org.springframework.restdocs.request.ParameterDescriptor; import reviewme.reviewgroup.service.dto.ReviewGroupCreationRequest; import reviewme.reviewgroup.service.dto.ReviewGroupCreationResponse; +import reviewme.reviewgroup.service.dto.ReviewGroupDetailResponse; +import reviewme.reviewgroup.service.dto.ReviewGroupListResponse; import reviewme.reviewgroup.service.dto.ReviewGroupResponse; class ReviewGroupApiTest extends ApiTest { @@ -121,4 +126,42 @@ class ReviewGroupApiTest extends ApiTest { .cookie("JSESSIONID") .statusCode(204); } + + @Test + void 회원이_생성한_프로젝트_목록을_반환한다() { + ReviewGroupListResponse response = new ReviewGroupListResponse(true, + List.of( + new ReviewGroupDetailResponse("이동훈", "우테코", LocalDate.of(2024, 1, 30), "WOOTECO1"), + new ReviewGroupDetailResponse("아루", "리뷰미", LocalDate.of(2024, 1, 5), "ABCD1234") + ) + ); + BDDMockito.given(reviewGroupLookupService.getMyReviewGroups()) + .willReturn(response); + + CookieDescriptor[] cookieDescriptors = { + cookieWithName("JSESSIONID").description("세션 ID") + }; + + FieldDescriptor[] responseFieldDescriptors = { + fieldWithPath("isLastPage").description("마지막 페이지 여부"), + fieldWithPath("reviewGroups[]").description("리뷰 그룹 목록 (생성일 기준 내림차순 정렬)"), + fieldWithPath("reviewGroups[].revieweeName").description("리뷰이 이름"), + fieldWithPath("reviewGroups[].projectName").description("프로젝트 이름"), + fieldWithPath("reviewGroups[].createdAt").description("생성일"), + fieldWithPath("reviewGroups[].reviewRequestCode").description("리뷰 요청 코드") + }; + + RestDocumentationResultHandler handler = document( + "review-group-list", + responseFields(responseFieldDescriptors), + requestCookies(cookieDescriptors) + ); + + givenWithSpec().log().all() + .cookie("JSESSIONID", "ABCDEFGHI1234") + .when().get("/v2/groups/mine") + .then().log().all() + .apply(handler) + .statusCode(200); + } }