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

3차 세미나 실습 과제 #13

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
@SpringBootApplication
public class PracticeApplication {

public static void main(String[] args) {
SpringApplication.run(PracticeApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(PracticeApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.sopt.practice.common;

import jakarta.persistence.EntityNotFoundException;
import org.sopt.practice.common.dto.ErrorMessage;
import org.sopt.practice.common.dto.ErrorResponse;
import org.sopt.practice.exception.NotFoundException;
import org.sopt.practice.exception.CustomizedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand All @@ -22,8 +22,13 @@ protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Me
}

@ExceptionHandler(NotFoundException.class)
protected ResponseEntity<ErrorResponse> handleEntityNotFoundException (NotFoundException e){
protected ResponseEntity<ErrorResponse> handleEntityNotFoundException(NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(ErrorMessage.MEMBER_NOT_FOUND_BY_ID_EXCEPTION));
}

@ExceptionHandler(CustomizedException.class)
public ResponseEntity<ErrorResponse> handleUnauthorizedAccessException(CustomizedException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(ErrorMessage.UNAUTHORIZED_ACCESS));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
public enum ErrorMessage {
MEMBER_NOT_FOUND_BY_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 사용자가 존재하지 않습니다."),
BLOG_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 블로그가 존재하지 않습니다."),
UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED.value(), "접근 권한이 없습니다."),
POST_NOT_FOUND_BY_POST_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "글이 존재하지 않습니다."),
POST_NOT_FOUND_BY_BLOG_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "해당 블로그에 작성한 글이 존재하지 않습니다."),
;
private final int status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public record ErrorResponse(
int status,
String message
) {
public static ErrorResponse of(int status, String message){
public static ErrorResponse of(int status, String message) {
return new ErrorResponse(status, message);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
@Getter
@AllArgsConstructor
public enum SuccessMessage {
BLOG_CREATE_SUCCESS(HttpStatus.CREATED.value(),"블로그 생성이 완료되었습니다."),
BLOG_CREATE_SUCCESS(HttpStatus.CREATED.value(), "블로그 생성이 완료되었습니다."),
POST_CREATE_SUCCESS(HttpStatus.CREATED.value(), "글 작성이 완료되었습니다."),
POST_ALL_FIND_SUCCESS(HttpStatus.OK.value(), "작성된 글을 모두 불러왔습니다."),
POST_FIND_SUCCESS(HttpStatus.OK.value(), "요청한 글을 불러왔습니다."),

;
private final int status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.sopt.practice.common.dto;

public record SuccessStatusResponse(
int status, String message
int status, String message, Object data
) {
public static SuccessStatusResponse of(SuccessMessage successMessage){
return new SuccessStatusResponse(successMessage.getStatus(), successMessage.getMessage());
return new SuccessStatusResponse(successMessage.getStatus(), successMessage.getMessage(), null);
}
public static SuccessStatusResponse of(int status, String message, Object data) {
Copy link

Choose a reason for hiding this comment

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

위에 작성하신 것처럼 SuccessMessage로 캡슐화해서 넘겨주는 것은 어떨까요?

return new SuccessStatusResponse(status, message, data);
}
Comment on lines +9 to +11
Copy link

Choose a reason for hiding this comment

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

Object대신 제네릭을 사용해보시는 것을 추천드립니다!
타입 안전성이 보장되고 타입이 명시적으로 들어나기에 가독성에 도움이 된다 생각합니다


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import lombok.RequiredArgsConstructor;
import org.sopt.practice.common.dto.SuccessMessage;
import org.sopt.practice.common.dto.SuccessStatusResponse;
import org.sopt.practice.dto.BlogCreateRequest;
import org.sopt.practice.dto.BlogTitleUpdateRequest;
import org.sopt.practice.dto.blog.BlogCreateRequest;
import org.sopt.practice.dto.blog.BlogTitleUpdateRequest;
import org.sopt.practice.service.BlogService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -21,16 +21,16 @@ public class BlogController {
//성공했을 때 값을 반환하기 위한 SuccessStatusResponse
@PostMapping("/blog")
public ResponseEntity<SuccessStatusResponse> creatBlog(@RequestHeader Long memberId,
@RequestBody BlogCreateRequest blogCreateRequest){
return ResponseEntity.status(HttpStatus.CREATED).header("Location",blogService.create(memberId, blogCreateRequest))
@RequestBody BlogCreateRequest blogCreateRequest) {
return ResponseEntity.status(HttpStatus.CREATED).header("Location", blogService.create(memberId, blogCreateRequest))
.body(SuccessStatusResponse.of(SuccessMessage.BLOG_CREATE_SUCCESS));
}

@PatchMapping("/blog/{blogId}/title")
public ResponseEntity updateBlogTitle(
@PathVariable Long blogId,
@Valid @RequestBody BlogTitleUpdateRequest blogTitleUpdateRequest
){
) {
blogService.updateTitle(blogId, blogTitleUpdateRequest);
return ResponseEntity.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.sopt.practice.controller;

import lombok.RequiredArgsConstructor;
import org.sopt.practice.dto.MemberCreateDto;
import org.sopt.practice.dto.MemberFindDto;
import org.sopt.practice.dto.member.MemberCreateDto;
import org.sopt.practice.dto.member.MemberFindDto;
import org.sopt.practice.service.MemberService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -17,20 +17,19 @@ public class MemberController {
private final MemberService memberService;

@PostMapping
public ResponseEntity postMember(@RequestBody MemberCreateDto memberCreateDto)
{
public ResponseEntity postMember(@RequestBody MemberCreateDto memberCreateDto) {
return ResponseEntity.created(URI.create(memberService.createMember(memberCreateDto))).build();
}

//ResponseEntity는 generic타입을 넣을 수 있음

@GetMapping("/{memberId}")
public ResponseEntity<MemberFindDto> findMemberById(@PathVariable Long memberId){
return ResponseEntity.ok(memberService.findMemberById(memberId));
public ResponseEntity<MemberFindDto> findMemberById(@PathVariable Long memberId) {
return ResponseEntity.ok(memberService.findMemberById(memberId));
}

@DeleteMapping("/{memberId}")
public ResponseEntity deleteMemberById(@PathVariable Long memberId){
public ResponseEntity deleteMemberById(@PathVariable Long memberId) {
memberService.deleteMemberById(memberId);
return ResponseEntity.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.sopt.practice.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.sopt.practice.common.dto.SuccessMessage;
import org.sopt.practice.common.dto.SuccessStatusResponse;
import org.sopt.practice.domain.Post;
import org.sopt.practice.dto.post.PostCreateRequest;
import org.sopt.practice.dto.post.PostResponse;
import org.sopt.practice.service.PostService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class PostController {

private final PostService postService;

@PostMapping("/post")
Copy link

Choose a reason for hiding this comment

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

rest api에서는 단수형보다 복수형이 더 선호된다고 알고 있습니다!

그리고 글 작성시에 /api/v1/post로 보내시고 있는데
rest api�는 리소스의 식별이 url에 의해 명확하게 이루어 져야 한다 생각해서
저는 /v1/blogs/{blogId}/posts를 엔드포인트로 사용하고 있습니다
blogid를 pathvariable이 아닌 헤더로 보내주면 api가 어떤 블로그에 대해 글을 작성하는지 명확해지지 않는다고 생각하는데 세빈님의 생각은 어떠신지 궁금합니다

public ResponseEntity<SuccessStatusResponse> writePost(@RequestHeader Long blogId, @RequestHeader Long memberId,
@Valid @RequestBody PostCreateRequest postCreateRequest) {
return ResponseEntity.status(HttpStatus.CREATED).header("Location", postService.writePost(blogId, memberId, postCreateRequest))
.body(SuccessStatusResponse.of(SuccessMessage.POST_CREATE_SUCCESS));
Comment on lines +28 to +29
Copy link

Choose a reason for hiding this comment

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

status와 헤더를 직접 입력하는 대신에 .created를 사용해보아도 좋을 것 같아요

}

@GetMapping("/post")
public ResponseEntity<SuccessStatusResponse> findAllPostsByBlogId(@RequestHeader Long blogId, @RequestHeader Long memberId) {
List<Post> posts = postService.findAllPostsByBlogId(blogId, memberId);
List<PostResponse> responses = posts.stream()
.map(post -> new PostResponse(post.getId(), post.getTitle(), post.getContent()))
.collect(Collectors.toList());
Comment on lines +35 to +37
Copy link

Choose a reason for hiding this comment

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

dto로의 변환 작업은 service layer에서 수행하는 것이 어떨까요?
controller에서는 요청데이터의 유효성 검사, 서비스 메소드 호출, 응답상태, 데이터 전송정도만 하고
service에서 비즈니스로직, dto로의 변환을 해주는 것이 계층간의 역할분리에 도움이 된다 생각합니당

Copy link
Contributor

Choose a reason for hiding this comment

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

더해서 PostResponse에서 생성자가 아닌 정적 팩토리 메서드 패턴으로 매개변수로 Post 자체를 받으면
.map(PostResponse::of) 를 사용할 수 있는데, 한 번 고민해봐도 좋을 것 같아요 :)

return ResponseEntity.ok(SuccessStatusResponse.of(
SuccessMessage.POST_ALL_FIND_SUCCESS.getStatus(),
SuccessMessage.POST_ALL_FIND_SUCCESS.getMessage(),
responses
));
}

@GetMapping("/post/{postId}")
public ResponseEntity<SuccessStatusResponse> getPostById(@RequestHeader Long blogId, @RequestHeader Long memberId,
@PathVariable Long postId) {
Post post = postService.findPostById(blogId, memberId, postId);
PostResponse postResponse = new PostResponse(post.getId(), post.getTitle(), post.getContent());
Copy link

Choose a reason for hiding this comment

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

여기도 마찬가지로 service에서 변환해주는 것이 좋을 것 같습니다.

return ResponseEntity.ok(new SuccessStatusResponse(
SuccessMessage.POST_FIND_SUCCESS.getStatus(),
SuccessMessage.POST_FIND_SUCCESS.getMessage(),
postResponse
));
Comment on lines +50 to +54
Copy link
Contributor

Choose a reason for hiding this comment

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

Response에도 정적 팩터리 메서드 패턴을 사용해도 좋을 것 같아요!

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

//엔티티 클래스
@Entity
@Getter
@NoArgsConstructor
public class Blog extends BaseTimeEntity{
public class Blog extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -21,20 +24,24 @@ public class Blog extends BaseTimeEntity{

private String description;

private Blog(Member member, String title, String description){
private Blog(Member member, String title, String description) {
this.member = member;
this.title = title;
this.description= description;
this.description = description;
}

public static Blog create(Member member, String title, String description){
public static Blog create(Member member, String title, String description) {
return new Blog(member, title, description);
}

public void updateTitle(
String title
) {
this.title = title;
}

@OneToMany(mappedBy = "blog", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();


}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String name;

@Enumerated(EnumType.STRING) //enum class의 string 값을 가져옴
private Part part;
Expand All @@ -22,13 +22,13 @@ public class Member {
//접근 제어자 private인 이유 : 객체의 무분별한 생성을 막기 위해서

@Builder
public Member(String name, Part part, int age){
public Member(String name, Part part, int age) {
this.name = name;
this.part = part;
this.age = age;
}

public static Member create(String name, Part part, int age){
public static Member create(String name, Part part, int age) {
return Member.builder()
.name(name)
.part(part)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@
import lombok.Getter;
import lombok.NoArgsConstructor;


@Entity
@Getter
@NoArgsConstructor
public class Post extends BaseTimeEntity{

@NoArgsConstructor
public class Post extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
private Blog blog;


private String title;

private String content;

@ManyToOne(fetch =FetchType.LAZY)
private Blog blog;
public static Post create(Blog blog, String title, String content) {
return new Post(blog, title, content);
}

public Post(Blog blog, String title, String content) {
this.blog = blog;

this.title = title;
this.content = content;
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.sopt.practice.dto;
package org.sopt.practice.dto.blog;

public record BlogCreateRequest(String title, String description) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.practice.dto.blog;

import jakarta.validation.constraints.Size;

public record BlogTitleUpdateRequest(
@Size(max = 5, message = "블로그 제목이 최대 글자 수(5자)를 초과했습니다.")
String title
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.sopt.practice.dto.member;

import org.sopt.practice.domain.Part;

public record MemberCreateDto(String name, Part part, int age) {

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package org.sopt.practice.dto;
package org.sopt.practice.dto.member;

import org.sopt.practice.domain.Member;
import org.sopt.practice.domain.Part;

//정보만을 가져오는 !!
public record MemberFindDto(
String name,
Part part,
int age
){
String name,
Part part,
int age
) {
public static MemberFindDto of(Member member) //한 멤버 객체에서 멤버를 찾는다 라는 의미의 of 함수
{
return new MemberFindDto(member.getName(), member.getPart(), member.getAge());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.sopt.practice.dto.post;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record PostCreateRequest(
@NotBlank(message = "제목을 입력해주세요.")
Copy link
Member

Choose a reason for hiding this comment

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

@notblank 어노테이션은 처음 보는데 입력이 공백으로 처리되는 걸 막아주는 건가요? 궁금해서 남깁니당

@Size(max = 100, message = "제목은 100자 이하로 작성해주세요.")
String title,
@NotBlank(message = "내용을 입력해주세요.")
@Size(max = 1000, message = "내용은 1000자 이하로 작성해주세요.")
String content) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sopt.practice.dto.post;

public record PostResponse(Long id, String title, String content) {
}
Loading