Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- @IdClass
- Filter
- 복합키
- REST란
- 스프링부트오류
- JPA
- 빈생성안됨
- queryDSL
- jpa회원가입
- uncheck Exception
- spring서버
- 스프링 부트 공식 문서
- Q 클래스
- ERD 작성
- 최종 프로젝트
- JoinColumn
- JPA주의사항
- 인텔리제이
- json gson 차이
- Unsatisfied dependency
- Spring Spring boot 차이
- Error creating bean with name
- REST API 규칙
- 스프링 부트 기능
- github
- jpa에러
- json
- jwt메서드
- git
- 1차캐시
Archives
- Today
- Total
Everyday Dev System
myBlog - Version 2.1 본문
Version 1.02023.06.15 - [내배캠 주요 학습/Spring 입문] - Spring Boot 블로그 서버 구현
github Repo
https://github.com/Chaeyounglim/myblog
<< 주요 추가사항 >>
로그인 및 회원가입을 구현하고,
게시글 작성 및 수정, 삭제 시에 JWT 토큰을 활용하여 작성자 여부를 판단하여 수행합니다.
1. DB & Entity Diagram 작성
2. 요구 사항에 따른 API 설계
https://docs.google.com/spreadsheets/d/1JZmNS89s8zFramK_9xyLxhH-iz1DUxvwZ1-F8a9hHng/edit#gid=0
Method | URL | Request Header |
Request | Response | Response Header | 기능 |
POST | /api/user/signup | - | { "username" : "chaeyoung" , "password" : "1234" } |
HttpServletResponse response - 메시지, 상태코드 response.setStatus(401); { "msg" : " " , "status" : " " } |
회원가입 | |
POST | /api/user/login | - | HttpServletRequest request { "username" : "chaeyoung" , "password" : "1234" } |
HttpServletResponse response - 메시지, 상태코드 response.setStatus(401); { "msg" : " " , "status" : " " } |
Authentication Token response.addHeader (JwtUtil.AUTHORIZATION_HEADER, token) |
로그인 |
GET | /api/posts | - | - | { { "id" : " " , "title" : " ", "user_name" : " ", "contents" : " ", "createAt" : " ", "modifiedAt" : " " }. { "id" : " " , "title" : " ", "user_name" : " ", "contents" : " ", "createAt" : " ", "modifiedAt" : " " } } |
전체 게시글 목록 조회 (작성 날짜 기준 내림차순) |
|
POST | /api/posts | Authentication Token | @AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody @Valid PostRequestDto requestDto { "title" : " ", "contents" : " " } |
{ "id" : " " , "title" : " ", "user_name" : " ", "contents" : " ", "createAt" : " ", "modifiedAt" : " " } |
게시글 작성 | |
GET | /api/posts/{id} | - | - | { "title" : " ", "user_name" : " ", "contents" : " ", "createAt" : " ", }. |
선택한 게시글 조회 | |
PUT | /api/posts/{id} | Authentication Token | @AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody @Valid PostRequestDto requestDto { "title" : " ", "contents" : " " } |
{ "id" : " " , "title" : " ", "user_name" : " ", "contents" : " ", "createAt" : " ", "modifiedAt" : " " } |
선택한 게시글 수정 | |
DELETE | /api/posts/{id} | Authentication Token | @AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody @Valid PostRequestDto requestDto |
HttpServletResponse response - 메시지, 상태코드 response.setStatus(401); { "msg" : " " , "status" : " " } |
선택한 게시글 삭제 |
요구사항
더보기
<< 추가 요구 사항 >>
- 회원 가입 API
- username, password를 Client에서 전달받기
- username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성되어야 한다.
- password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성되어야 한다.
- DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기
- 로그인 API
- username, password를 Client에서 전달받기
- DB에서 username을 사용하여 저장된 회원의 유무를 확인하고 있다면 password 비교하기
- 로그인 성공 시, 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급하고, 발급한 토큰을 Header에 추가하고 성공했다는 메시지, 상태코드 와 함께 Client에 반환하기
<< 수정된 요구 사항 >>
- 전체 게시글 목록 조회 API
- 제목, 작성자명(username), 작성 내용, 작성 날짜를 조회하기
- 작성 날짜 기준 내림차순으로 정렬하기
- 게시글 작성 API
- 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
- 제목, 작성 내용을 저장하고
- 저장된 게시글을 Client 로 반환하기(username은 로그인 된 사용자)
- 선택한 게시글 조회 API
- 선택한 게시글의 제목, 작성자명(username), 작성 날짜, 작성 내용을 조회하기 (검색 기능이 아닙니다. 간단한 게시글 조회만 구현해주세요.)
- 선택한 게시글 수정 API
수정을 요청할 때 수정할 데이터와 비밀번호 같이 보내서 서버에서 비밀번호 일치 여부 확인 한 후- 토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 수정 가능
- 제목, 작성 내용을 수정하고 수정된 게시글을 Client 로 반환하기
- 선택한 게시글 삭제 API
삭제를 요청할 때 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부 확인 한 후- 토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 삭제 가능
- 선택한 게시글을 삭제하고 Client 로 성공했다는 메시지, 상태코드 반환하기
3. Entity 작성
1. username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성되어야 한다.
2. password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성되어야 한다.
Entity code :
더보기
package com.sparta.myblog.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "user")
private List<Post> postList = new ArrayList<>();
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
public User(String username, String password, UserRoleEnum role) {
this.username = username;
this.password = password;
this.role = role;
}
}
package com.sparta.myblog.entity;
import com.sparta.myblog.dto.PostRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@Table(name = "post")
@NoArgsConstructor
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false, length = 500)
private String contents;
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL)
private List<Comment> commentList = new ArrayList<>();
public Post(PostRequestDto requestDto, User user) {
this.title = requestDto.getTitle();
this.contents = requestDto.getContents();
this.user = user;
}
public void addComment(Comment comment) {
this.commentList.add(comment);
}
public void update(PostRequestDto requestDto) {
this.title = requestDto.getTitle();
this.contents = requestDto.getContents();
}
}
package com.sparta.myblog.entity;
import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
package com.sparta.myblog.entity;
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
4. Dto 작성
회원가입시,
DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기.
로그인시,
로그인 성공 시, 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급하고,
발급한 토큰을 Header에 추가하고 성공했다는 메시지, 상태코드 와 함께 Client에 반환하기.
클라이언트로 부터 요청받을 때는 username , password 가 필요하며,
클라이언트에게 응답을 보낼 때는 따로 Dto 클래스 타입이 필요하지 않다.
대신, 서버가 클라이언트에 응답을 보낼 때에 Header에 저장되는 HttpServletResponse에
상태 코드와 메시지를 set 하는 절차가 필요하다.
Dto Code:
더보기
package com.sparta.myblog.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserRequestDto {
// login and sing up Request DTO
@Pattern(regexp = "^[a-z0-9]{4,10}$" , message = "정규식에 맞지 않습니다.")
@NotBlank
private String username;
@Pattern(regexp = "^[a-zA-Z0-9]{8,15}$", message = "정규식에 맞지 않습니다.")
@NotBlank
private String password;
}
5. 회원가입 기능 구현
<< 회원 가입 API >>
username, password를 Client에서 전달받기
username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성되어야 한다.
password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성되어야 한다.
DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기
1) Entity 작성
1. username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성되어야 한다.
2. password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성되어야 한다.
Entity code :
더보기
package com.sparta.myblog.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "user")
private List<Post> postList = new ArrayList<>();
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
public User(String username, String password, UserRoleEnum role) {
this.username = username;
this.password = password;
this.role = role;
}
}
package com.sparta.myblog.entity;
import com.sparta.myblog.dto.PostRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@Table(name = "post")
@NoArgsConstructor
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false, length = 500)
private String contents;
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL)
private List<Comment> commentList = new ArrayList<>();
public Post(PostRequestDto requestDto, User user) {
this.title = requestDto.getTitle();
this.contents = requestDto.getContents();
this.user = user;
}
public void addComment(Comment comment) {
this.commentList.add(comment);
}
public void update(PostRequestDto requestDto) {
this.title = requestDto.getTitle();
this.contents = requestDto.getContents();
}
}
package com.sparta.myblog.entity;
import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
package com.sparta.myblog.entity;
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
2) Dto 작성
회원가입시,
DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기.
로그인시,
로그인 성공 시, 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급하고,
발급한 토큰을 Header에 추가하고 성공했다는 메시지, 상태코드 와 함께 Client에 반환하기.
클라이언트로 부터 요청받을 때는 username , password 가 필요하며,
클라이언트에게 응답을 보낼 때는 따로 Dto 클래스 타입이 필요하지 않다.
대신, 서버가 클라이언트에 응답을 보낼 때에 Header에 저장되는 HttpServletResponse에
상태 코드와 메시지를 set 하는 절차가 필요하다.
Dto Code:
더보기
package com.sparta.myblog.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserRequestDto {
// login and sing up Request DTO
@Pattern(regexp = "^[a-z0-9]{4,10}$" , message = "정규식에 맞지 않습니다.")
@NotBlank
private String username;
@Pattern(regexp = "^[a-zA-Z0-9]{8,15}$", message = "정규식에 맞지 않습니다.")
@NotBlank
private String password;
}
3) Controller 클래스 구현
- 위에 해당 API 명세서를 참고하여 맵핑되는 URL을 적어주었다.
- UserService 클래스를 멤버변수로 포함하여 생성자 메서드에서 초기화된다.
- UserRequestDto 클래스에 명시해둔 예외처리를 실행하고 message를 출력하는 부분을 구현하였다.
- 예외처리가 모두 통과되어 없을 경우에, userService.signUp() 를 실행하고 회원가입을 진행한다.
package com.sparta.myblog.controller;
import com.sparta.myblog.dto.UserRequestDto;
import com.sparta.myblog.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// 회원가입
@PostMapping("/user/signup")
public void signUp(@Valid @RequestBody UserRequestDto userRequestDto, BindingResult bindingResult, HttpServletResponse res) throws IOException {
// 1. Validation 예외처리
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
if (fieldErrors.size() > 0) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
}
}else {
// 2. userService 에서 signup 하고
// 3. HttpServletResponse 에 (msg, status set 하기)
userService.signUp(userRequestDto,res);
}
}
@PostMapping("/user/login")
public void login(@RequestBody UserRequestDto userRequestDto) {
}
}
4) Service 클래스 구현
- Controller 클래스에 의해 호출된 메서드가 실행된다.
- UserRepository 클래스를 멤버변수로 포함하여 생성자 메서드에서 초기화된다.
- responseResult() 메서드를 통해 클라이언트에 응답 메시지와 상태 코드를 반환할 수 있다.
package com.sparta.myblog.service;
import com.sparta.myblog.dto.UserRequestDto;
import com.sparta.myblog.entity.User;
import com.sparta.myblog.entity.UserRoleEnum;
import com.sparta.myblog.repository.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
// 회원가입 메서드
public void signUp(UserRequestDto requestDto, HttpServletResponse res) throws IOException {
// 1. 요청 받은 회원가입 정보 변수에 저장 및 비밀번호 암호화
String username = requestDto.getUsername();
String password = passwordEncoder.encode(requestDto.getPassword());
// 2. 회원 중복 확인
// 2-1. DB에 해당 username 에 대한 row 가 있다면 checkUsername 변수에 저장.
Optional<User> checkUsername = userRepository.findByUsername(username);
if( checkUsername.isPresent() ) { // 2-2. 중복된 회원이 있을 경우
responseResult(res,400,"중복된 사용자가 있습니다.");
log.error("중복된 사용자가 존재합니다.");
}else { // 2-2. 중복된 회원이 없을 경우 가입 시도
// 3. 사용자 ROLE 부여
UserRoleEnum role = UserRoleEnum.USER;
// 4. 해당 정보를 생성자 메서드로 User 객체 생성 후 DB 에 저장
User user = new User(username, password, role);
userRepository.save(user);
// 5. Client 에 반환할 데이터 및 log 출력
responseResult(res, 200, "회원가입 성공");
log.info("회원가입에 성공하였습니다.");
}
}
// Client 에 HttpServletResponse 를 통해 반환할 msg, status 세팅 메서드
private void responseResult(HttpServletResponse response, int statusCode, String message) throws IOException {
String jsonResponse = "{\"status\": " + statusCode + ", \"message\": \"" + message + "\"}";
// Content-Type 및 문자 인코딩 설정
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// PrintWriter 를 사용하여 응답 데이터 전송
PrintWriter writer = response.getWriter();
writer.write(jsonResponse);
writer.flush();
}
}
5) Repository클래스 구현
- Spring JPA 에서 제공하는 (자동 생성하는) JpaRepository라는 인터페이스를 상속한다.
- Entity 타입과 Entity의 @Id 타입을 <> 안에 기재한다.
- 해당 클래스 내에서 Query String 명명 규칙에 맞게 메서드를 만들 수 있고, 이를 통해 DB에 접근한다.
- 이는 JPA 내부에서 제공하는 편리한 기능으로, 직접 SQL Query문을 작성하지 않도록 수고를 덜어준다.
package com.sparta.myblog.repository;
import com.sparta.myblog.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
// Query 문 : select * from users where username = ? ;
}
결과 :
서버 측,
클라이언트 측,
'내배캠 주요 학습 > Spring 숙련' 카테고리의 다른 글
[Spring Security] 권한 설정 및 표현식 (0) | 2023.06.29 |
---|---|
GenerationType 종류 (0) | 2023.06.28 |
MySelectShop - Version.1.1 - 페이징 및 정렬 기능 (0) | 2023.06.23 |
MySelectShop - Version 1.0 (0) | 2023.06.23 |
My Select Shop 프로젝트 초기 설정 (0) | 2023.06.22 |