Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: swagger와 restdocs 연동 #104

Merged
merged 15 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ out/

### VS Code ###
.vscode/

### Mac ###
.DS_Store

### RestDocs ###
openapi3.yaml
31 changes: 30 additions & 1 deletion backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
id 'com.epages.restdocs-api-spec' version '0.18.2'
}

group = 'com.zzang'
Expand Down Expand Up @@ -33,9 +34,37 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
// test -> openapi3 -> copyOasToSwagger -> bootJar

test {
useJUnitPlatform()
}

openapi3 {
servers = [{ url = 'http://13.124.137.6' },
{ url = 'http://localhost:8080' }]
title '총대마켓 API 명세서'
description '총대마켓 백엔드 API 명세서'
version '0.1.0'
format 'yaml'
}

tasks.register('copyOasToSwagger', Copy) {
dependsOn('openapi3')
doFirst {
delete file('src/main/resources/static/swagger-ui/openapi3.yaml')
}
from file("build/api-spec/openapi3.yaml")
into file("src/main/resources/static/swagger-ui/.")
}

bootJar {
dependsOn copyOasToSwagger
}
2 changes: 2 additions & 0 deletions backend/http/comment.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### 댓글방 목록 조회 API
GET {{base-url}}/comments?member-id=1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import com.zzang.chongdae.comment.service.dto.CommentAllResponse;
import com.zzang.chongdae.comment.service.dto.CommentRoomAllResponse;
import com.zzang.chongdae.comment.service.dto.CommentSaveRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -15,30 +13,26 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Comment(댓글)")
@RequiredArgsConstructor
@RestController
public class CommentController {

private final CommentService commentService;

@Operation(summary = "댓글 작성", description = "댓글을 작성합니다.")
@PostMapping("/comments")
public ResponseEntity<Void> saveComment(
@RequestBody CommentSaveRequest commentSaveRequest) {
commentService.saveComment(commentSaveRequest);
return ResponseEntity.ok().build();
}

@Operation(summary = "댓글방 목록 조회", description = "댓글방 목록을 조회합니다.")
@GetMapping("/comments")
public ResponseEntity<CommentRoomAllResponse> getAllCommentRoom(
@RequestParam(value = "member-id") Long loginMemberId) {
CommentRoomAllResponse response = commentService.getAllCommentRoom(loginMemberId);
return ResponseEntity.ok(response);
}

@Operation(summary = "댓글 목록 조회", description = "댓글 목록을 조회합니다.")
@GetMapping("/comments/{offering-id}")
public ResponseEntity<CommentAllResponse> getAllComment(
@PathVariable(value = "offering-id") Long offeringId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.zzang.chongdae.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class StaticRoutingConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public class MemberEntity extends BaseTimeEntity {
@Column(unique = true)
private String nickname;

public MemberEntity(String nickname) {
this(null, nickname);
}

public boolean isSameMember(Long memberId) {
return this.id.equals(memberId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,26 @@
import com.zzang.chongdae.offering.service.dto.OfferingAllResponse;
import com.zzang.chongdae.offering.service.dto.OfferingDetailResponse;
import com.zzang.chongdae.offering.service.dto.OfferingMeetingResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Offerring(공모)")
@RequiredArgsConstructor
@Controller
@RestController
public class OfferingController {

private final OfferingService offeringService;

@Operation(summary = "공모 상세 조회", description = "공모 id를 통해 공모의 상세 정보를 조회합니다.")
@GetMapping("/offerings/{offering-id}")
public ResponseEntity<OfferingDetailResponse> getOfferingDetail(
@PathVariable(value = "offering-id") Long offeringId, @RequestParam(value = "member-id") Long memberId) {
OfferingDetailResponse response = offeringService.getOfferingDetail(offeringId, memberId);
return ResponseEntity.ok(response);
}

@Operation(summary = "공모 목록 조회", description = "공모 목록을 조회합니다.")
@GetMapping("/offerings")
public ResponseEntity<OfferingAllResponse> getAllOffering(
@RequestParam(value = "last-id", defaultValue = "0") Long lastId,
Expand All @@ -37,7 +32,6 @@ public ResponseEntity<OfferingAllResponse> getAllOffering(
return ResponseEntity.ok(response);
}

@Operation(summary = "공모 일정 조회", description = "공모 id를 통해 공모의 일정 정보를 조회합니다.")
@GetMapping("/offerings/{offering-id}/meetings")
public ResponseEntity<OfferingMeetingResponse> getOfferingMeeting(
@PathVariable(value = "offering-id") Long offeringId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ public class OfferingEntity extends BaseTimeEntity {
@NotNull
private BigDecimal totalPrice;

public OfferingEntity(MemberEntity member, String title, String description, String thumbnailUrl, String productUrl,
LocalDateTime deadline, String meetingAddress, String meetingAddressDetail,
Integer totalCount, Integer currentCount, Boolean isManualConfirmed, BigDecimal totalPrice) {
this(null, member, title, description, thumbnailUrl, productUrl, deadline, meetingAddress,
meetingAddressDetail, totalCount, currentCount, isManualConfirmed, totalPrice);
}

public void updateCurrentCount() {
currentCount++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,19 @@

import com.zzang.chongdae.offeringmember.service.OfferingMemberService;
import com.zzang.chongdae.offeringmember.service.dto.ParticipationRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "OfferingMember(공모인원)")
@RequiredArgsConstructor
@Controller
@RestController
public class OfferingMemberController {

private final OfferingMemberService offeringMemberService;

@Operation(summary = "공모 참여", description = "게시된 공모에 참여합니다.")
@PostMapping("/participations")
ResponseEntity<Void> participate(@RequestBody ParticipationRequest participationRequest) {
Long id = offeringMemberService.participate(participationRequest);
Expand Down
4 changes: 1 addition & 3 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ spring:
hibernate:
format_sql: true
springdoc:
packages-to-scan: com.zzang.chongdae
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
swagger-ui:
path: swagger-ui.html
url: /static/swagger-ui/openapi3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.zzang.chongdae.comment.integration;

import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName;
import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder;
import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document;
import static com.epages.restdocs.apispec.Schema.schema;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;

import com.epages.restdocs.apispec.ParameterDescriptorWithType;
import com.epages.restdocs.apispec.ResourceSnippetParameters;
import com.zzang.chongdae.global.integration.IntegrationTest;
import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.offering.repository.entity.OfferingEntity;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.List;
import java.util.Map;
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.springframework.restdocs.payload.FieldDescriptor;

public class CommentIntegrationTest extends IntegrationTest {

@DisplayName("댓글 작성")
@Nested
class SaveComment {

List<FieldDescriptor> saveCommentRequestDescriptors = List.of(
fieldWithPath("memberId").description("회원 id"),
fieldWithPath("offeringId").description("공모 id"),
fieldWithPath("content").description("내용")
);
ResourceSnippetParameters snippets = builder()
.summary("댓글 작성")
.description("댓글을 작성합니다.")
.requestFields(saveCommentRequestDescriptors)
.requestSchema(schema("CommentSaveRequest"))
.build();
MemberEntity member;
OfferingEntity offering;

@BeforeEach
void setUp() {
member = memberFixture.createMember();
offering = offeringFixture.createOffering(member);
}

@DisplayName("댓글을 작성할 수 있다")
@Test
void should_saveCommentSuccess_when_givenCommentSaveRequest() {
Map<String, String> request = Map.of(
"memberId", member.getId().toString(),
"offeringId", offering.getId().toString(),
"content", "댓글 내용"
);

RestAssured.given(spec).log().all()
.filter(document("save-comment-success", resource(snippets)))
.contentType(ContentType.JSON)
.body(request)
.when().post("/comments")
.then().log().all()
.statusCode(200);
}
}

@DisplayName("댓글방 목록 조회")
@Nested
class GetAllCommentRoom {

List<ParameterDescriptorWithType> getAllCommentRoomQueryParameterDescriptors = List.of(
parameterWithName("member-id").description("회원 id")
);
List<FieldDescriptor> getAllCommentRoomResponseDescriptors = List.of(
fieldWithPath("offerings[].offeringId").description("공모 id"),
fieldWithPath("offerings[].offeringTitle").description("공모 제목"),
fieldWithPath("offerings[].latestComment.content").description("최신 댓글 내용"),
fieldWithPath("offerings[].latestComment.createdAt").description("최신 댓글 작성일"),
fieldWithPath("offerings[].isProposer").description("총대 여부")
);
ResourceSnippetParameters snippets = builder()
.summary("댓글방 목록 조회")
.description("댓글방 목록을 조회합니다.")
.queryParameters(getAllCommentRoomQueryParameterDescriptors)
.responseFields(getAllCommentRoomResponseDescriptors)
.responseSchema(schema("CommentRoomAllResponse"))
.build();
MemberEntity member;
OfferingEntity offering;

@BeforeEach
void setUp() {
member = memberFixture.createMember();
offering = offeringFixture.createOffering(member);
offeringMemberFixture.createProposer(member, offering);
commentFixture.createComment(member, offering);
}

@DisplayName("댓글방 목록을 조회할 수 있다")
@Test
void should_responseAllCommentRoom_when_givenMemberId() {
RestAssured.given(spec).log().all()
.filter(document("get-all-comment-room-success", resource(snippets)))
.queryParam("member-id", member.getId())
.when().get("/comments")
.then().log().all()
.statusCode(200);
}
}

@DisplayName("댓글 목록 조회")
@Nested
class GetAllComment {

List<ParameterDescriptorWithType> getAllCommentPathParameterDescriptors = List.of(
parameterWithName("offering-id").description("공모 id")
);
List<ParameterDescriptorWithType> getAllCommentQueryParameterDescriptors = List.of(
parameterWithName("member-id").description("회원 id")
);
List<FieldDescriptor> getAllCommentResponseDescriptors = List.of(
fieldWithPath("comments[].createdAt.date").description("작성 날짜"),
fieldWithPath("comments[].createdAt.time").description("작성 시간"),
fieldWithPath("comments[].content").description("내용"),
fieldWithPath("comments[].nickname").description("작성자 회원 닉네임"),
fieldWithPath("comments[].isProposer").description("총대 여부"),
fieldWithPath("comments[].isMine").description("내 댓글 여부")
);
ResourceSnippetParameters snippets = builder()
.summary("댓글 목록 조회")
.description("댓글 목록을 조회합니다.")
.pathParameters(getAllCommentPathParameterDescriptors)
.queryParameters(getAllCommentQueryParameterDescriptors)
.responseFields(getAllCommentResponseDescriptors)
.responseSchema(schema("CommentAllResponse"))
.build();
MemberEntity member;
OfferingEntity offering;

@BeforeEach
void setUp() {
member = memberFixture.createMember();
offering = offeringFixture.createOffering(member);
offeringMemberFixture.createProposer(member, offering);
commentFixture.createComment(member, offering);
}

@DisplayName("댓글 목록을 조회할 수 있다")
@Test
void should_responseAllComment_when_givenOfferingIdAndMemberId() {
RestAssured.given(spec).log().all()
.filter(document("get-all-comment-success", resource(snippets)))
.pathParam("offering-id", offering.getId())
.queryParam("member-id", member.getId())
.when().get("/comments/{offering-id}")
.then().log().all()
.statusCode(200);
}
}
}
Loading