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] 멤버 관련 API 구현 #34

Merged
merged 12 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.debatetimer.controller.member;

import com.debatetimer.controller.member.dto.MemberCreateRequest;
import com.debatetimer.controller.member.dto.MemberCreateResponse;
import com.debatetimer.controller.member.dto.TableResponses;
import com.debatetimer.service.member.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class MemberController implements MemberControllerSwagger{

private final MemberService memberService;

@Override
@GetMapping("/api/table")
public TableResponses getTables(@RequestParam Long memberId) {
return memberService.getTables(memberId);
}

@Override
@PostMapping("/api/member")
@ResponseStatus(HttpStatus.CREATED)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 컨벤션에서 ResponseStatus와 관련된 이야기가 나오지 않아서 그런데

비토와 커찬 모두에게 어떤 방향을 더 선호하는지 묻습니다.

  1. ResponseStatus 쓰기
  2. ResponseEntity 쓰기

저는 responsebody 이외에 응답에 대한 header와 같은 설정을 해줄 수 있다는 점에서 확장성을 고려해 ResponseEntity를 사용해오긴 했습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 header 설정이 필요할 땜만 ResponseEntity 를 사용하는 편입니다. 주로 헤더는 인증 용도로 사용되서 잘 사용할 일도 적을 것 같구요

public MemberCreateResponse createMember(@RequestBody MemberCreateRequest request) {
return memberService.createMember(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.debatetimer.controller.member;

import com.debatetimer.controller.member.dto.MemberCreateRequest;
import com.debatetimer.controller.member.dto.MemberCreateResponse;
import com.debatetimer.controller.member.dto.TableResponses;
import com.debatetimer.swagger.annotation.ErrorCode400;
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.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "Member API")
public interface MemberControllerSwagger {

@Operation(
summary = "멤버의 토론 시간표 조회",
responses = {
@ApiResponse(
responseCode = "200",
description = "멤버의 토론 시간표 조회 성공",
content = @Content(schema = @Schema(implementation = TableResponses.class))
)
}
)
@ErrorCode400
TableResponses getTables(Long memberId);

@Operation(
summary = "멤버 생성",
requestBody = @RequestBody(
content = @Content(schema = @Schema(implementation = MemberCreateRequest.class))
),
responses = {
@ApiResponse(
responseCode = "201",
description = "멤버 생성 성공",
content = @Content(schema = @Schema(implementation = MemberCreateResponse.class))
)
}
)
@ErrorCode400
MemberCreateResponse createMember(MemberCreateRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.debatetimer.controller.member.dto;

import com.debatetimer.domain.member.Member;
import io.swagger.v3.oas.annotations.media.Schema;

public record MemberCreateRequest(

@Schema(description = "멤버 닉네임", example = "콜리")
String nickname
) {

public Member toMember() {
return new Member(nickname);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.debatetimer.controller.member.dto;

import com.debatetimer.domain.member.Member;
import io.swagger.v3.oas.annotations.media.Schema;

public record MemberCreateResponse(

@Schema(description = "멤버 아이디", example = "1")
long id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이곳은 클래스 최상단이 아닌 일종의 생성자 파라미터를 입력하는 곳인데 개행을 없애는건 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 것 같네요! 반영하도록 하겠습니다~!


@Schema(description = "멤버 닉네임", example = "콜리")
String nickname
) {

public MemberCreateResponse(Member member) {
this(member.getId(), member.getNickname());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.debatetimer.controller.member.dto;

import com.debatetimer.domain.parliamentary_debate.ParliamentaryTable;
import io.swagger.v3.oas.annotations.media.Schema;

public record TableResponse(

@Schema(description = "테이블 이름", example = "테이블1")
String name,

@Schema(description = "토론 타입", example = "PARLIAMENTARY")
TableType type,

@Schema(description = "소요 시간 (초 단위)", example = "1800")
int duration
) {

public TableResponse(ParliamentaryTable parliamentaryTable) {
this(parliamentaryTable.getName(), TableType.PARLIAMENTARY, parliamentaryTable.getDuration());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.debatetimer.controller.member.dto;

import com.debatetimer.domain.parliamentary_debate.ParliamentaryTable;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record TableResponses(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayResponse의 경우 제네릭으로 선언하여 사용하는 건 어떨까요?

Suggested change
public record TableResponses(
public record ArrayResponse<T>(
List<T> responses
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api에 responses 대신에 tables로 되어 있어서 TableResponses로 만들었습니다.

ArrayResponse의 경우 제네릭으로 선언하여 사용하는 건 어떨까요?

모든 배열 응답 값이 responses로 의무적으로 통일하는 것도 썩... 좋진 않은 것 같습니다. 콜리의 생각은 어떤가요?


@ArraySchema(schema = @Schema(description = "테이블들", implementation = TableResponse.class))
List<TableResponse> tables
) {

public static TableResponses from(List<ParliamentaryTable> parliamentaryTables) {
return new TableResponses(toTableResponses(parliamentaryTables));
}

private static List<TableResponse> toTableResponses(List<ParliamentaryTable> parliamentaryTables) {
return parliamentaryTables.stream()
.map(TableResponse::new)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.debatetimer.controller.member.dto;

public enum TableType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨벤션에 따라 한 줄 개행 부탁드립니다 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 반영하겠습니다.


PARLIAMENTARY,
;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.debatetimer.domain;
package com.debatetimer.domain.member;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.debatetimer.domain.parliamentary_debate;

import com.debatetimer.domain.Member;
import com.debatetimer.domain.member.Member;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
Expand All @@ -18,7 +18,7 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ParliamentaryTable {

private static final String NAME_REGEX = "^[a-zA-Z가-힣]+$";
private static final String NAME_REGEX = "^[a-zA-Z가-힣\\s]+$";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디테일 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테이블 이름으로 띄어쓰기만 가능하게 할 껀가요, 모든 개행문자가 다 가능하도록 할 껀가요? 지금은 모든 개행문자가 다 가능할 것 같아요.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"^[a-zA-Z가-힣 ]+$" 으로 수정했습니다~

public static final int NAME_MAX_LENGTH = 20;

@Id
Expand Down Expand Up @@ -48,7 +48,7 @@ public ParliamentaryTable(Member member, String name, String agenda, int duratio
}

private void validate(String name, int duration) {
if (name.isEmpty() || name.length() > NAME_MAX_LENGTH) {
if (name.isBlank() || name.length() > NAME_MAX_LENGTH) {
throw new IllegalArgumentException("테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(NAME_MAX_LENGTH));
}
if (!name.matches(NAME_REGEX)) {
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.debatetimer.repository.member;

import com.debatetimer.domain.member.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {

default Member getById(Long id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[순수한 질문]

Optional 가 아니라 default 메서드를 통해 예외 처리를 해준 이유가 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보통 findById().orElseThrow()는 이 서비스 저 서비스에서 자주 사용되서, 레포지토리에서 default method로 만들어 사용하는 편입니다.

return findById(id).orElseThrow(() -> new IllegalArgumentException("해당 회원이 존재하지 않습니다"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.debatetimer.repository.parliamentary_debate;

import com.debatetimer.domain.member.Member;
import com.debatetimer.domain.parliamentary_debate.ParliamentaryTable;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ParliamentaryTableRepository extends JpaRepository<ParliamentaryTable, Long> {

List<ParliamentaryTable> findAllByMember(Member member);
}
Empty file.
34 changes: 34 additions & 0 deletions src/main/java/com/debatetimer/service/member/MemberService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.debatetimer.service.member;

import com.debatetimer.controller.member.dto.MemberCreateRequest;
import com.debatetimer.controller.member.dto.MemberCreateResponse;
import com.debatetimer.controller.member.dto.TableResponses;
import com.debatetimer.domain.member.Member;
import com.debatetimer.domain.parliamentary_debate.ParliamentaryTable;
import com.debatetimer.repository.member.MemberRepository;
import com.debatetimer.repository.parliamentary_debate.ParliamentaryTableRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
private final ParliamentaryTableRepository parliamentaryTableRepository;

@Transactional(readOnly = true)
public TableResponses getTables(Long memberId) {
Member member = memberRepository.getById(memberId);
List<ParliamentaryTable> parliamentaryTable = parliamentaryTableRepository.findAllByMember(member);
return TableResponses.from(parliamentaryTable);
}

@Transactional
public MemberCreateResponse createMember(MemberCreateRequest request) {
Member member = memberRepository.save(request.toMember());
return new MemberCreateResponse(member);
}
}
9 changes: 9 additions & 0 deletions src/test/java/com/debatetimer/BaseControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.debatetimer;

import com.debatetimer.repository.member.MemberRepository;
import com.debatetimer.repository.parliamentary_debate.ParliamentaryTableRepository;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

Expand All @@ -17,4 +20,10 @@ public abstract class BaseControllerTest {
void setPort() {
RestAssured.port = port;
}

@Autowired
protected MemberRepository memberRepository;

@Autowired
protected ParliamentaryTableRepository parliamentaryTableRepository;
}
9 changes: 9 additions & 0 deletions src/test/java/com/debatetimer/BaseServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package com.debatetimer;

import com.debatetimer.repository.member.MemberRepository;
import com.debatetimer.repository.parliamentary_debate.ParliamentaryTableRepository;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@ExtendWith(DataBaseCleaner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public abstract class BaseServiceTest {

@Autowired
protected MemberRepository memberRepository;

@Autowired
protected ParliamentaryTableRepository parliamentaryTableRepository;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.debatetimer.controller.member;

import static org.assertj.core.api.Assertions.assertThat;

import com.debatetimer.BaseControllerTest;
import com.debatetimer.controller.member.dto.MemberCreateRequest;
import com.debatetimer.controller.member.dto.MemberCreateResponse;
import com.debatetimer.controller.member.dto.TableResponses;
import com.debatetimer.domain.member.Member;
import com.debatetimer.domain.parliamentary_debate.ParliamentaryTable;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class MemberControllerTest extends BaseControllerTest {

@Nested
class CreateMember {

@Test
void 회원을_생성한다() {
MemberCreateRequest request = new MemberCreateRequest("커찬");

MemberCreateResponse response = RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(request)
.when().post("/api/member")
.then().log().all()
.statusCode(201)
.extract().as(MemberCreateResponse.class);

assertThat(response.nickname()).isEqualTo(request.nickname());
}
}

@Nested
class getTables {

@Test
void 회원의_전체_토론_시간표를_조회한다() {
Member member = memberRepository.save(new Member("커찬"));
parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800));
parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900));

TableResponses response = RestAssured.given().log().all()
.contentType(ContentType.JSON)
.queryParam("memberId", member.getId())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 저는 url에 직접 명시해주었는데 queryparameter를 지정해주는 이런 방법이 있었군요 👍

.when().get("/api/table")
.then().log().all()
.statusCode(200)
.extract().as(TableResponses.class);

assertThat(response.tables()).hasSize(2);
}
}
}
1 change: 1 addition & 0 deletions src/test/java/com/debatetimer/domain/MemberTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.debatetimer.domain;

import com.debatetimer.domain.member.Member;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
Expand Down
Loading
Loading