주특기 심화 과제

박영준·2022년 12월 19일
0

Java

목록 보기
33/112

config

WebSecurityConfig

package com.sparta.springboards.config;

import com.sparta.springboards.jwt.JwtAuthFilter;
import com.sparta.springboards.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화

public class WebSecurityConfig {
    private final JwtUtil jwtUtil;

    @Bean
    //Spring Security 가 제공하는 적응형 단방향 함수인 bCrypt 를 사용하여 비밀번호를 암호화(Encoder) --> 적응형 단방향이 자동 적용
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //WebSecurityCustomizer 은 SecurityFilterChain 보다 우선적으로 걸리는 설정
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()      //ignoring(): 이러한 경로도 들어온 것들은 인증 처리하는 것을 무시하겠다
                .requestMatchers(PathRequest.toH2Console())     //h2-console 사용 및 resources 접근 허용 설정
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf().disable();

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정 → form login이 안되게 함
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //permitAll() 를 사용해서, 이런 URL 들을 인증하지 않고 실행 할 수 있게 함
        http.authorizeRequests().antMatchers("/api/auth/**").permitAll()
                //그 이외의 URL 요청들을 전부 다 authentication(인증 처리)하겠다
                .anyRequest().authenticated()
                // JWT 인증/인가를 사용하기 위한 설정
                // Custom Filter 등록하기
                //addFilterBefore: 어떤 Filter 이전에 추가하겠다 --> 우리가 만든 JwtAuthFilterUsernamePasswordAuthenticationFilter 이전에 실행할 수 있도록
                //1. JwtAuthFilter 를 통해 인증 객체를 만들고 --> 2. context 에 추가 --> 3.인증 완료 --> UsernamePasswordAuthenticationFilter 수행 --> 인증됐으므로 다음 Filter 로 이동 --> Controller 까지도 이동
                .and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

        //"거부"가 났을 때, 403 Forbidden 페이지(접근 제한 페이지) 이동 설정
        http.exceptionHandling().accessDeniedPage("/api/user/forbidden");

        return http.build();
    }
}

controller

BoardController

package com.sparta.springboards.controller;

import com.sparta.springboards.dto.BoardRequestDto;
import com.sparta.springboards.dto.BoardResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.security.UserDetailsImpl;
import com.sparta.springboards.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;

//Controller 클래스(여기서는 BoardController)의 각 하위 메서드에 @ResponseBody 를 붙이지 않아도(@Controller 와 달리), 문자열과 JSON 등을 전송 가능
//RestController 의 주용도는 Json 형태로 객체 데이터를 반환하는 것
//@Controller + @ResponseBody 를 결합한 어노테이션
@RestController
@RequiredArgsConstructor        //final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequestMapping("/api")

public class BoardController {

    //HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
    //boardService 를 가져다 쓰겠다 (의존성 주입)
    private final BoardService boardService;

//Client 기준에서 보내는, 받는 정보 라는 뜻 =  DB 기준에서 받는, 보내는 정보

//게시글 작성
    @PostMapping("/board")
    //미리 말해두기: BoardResponseDto 타입으로 반환 받을 건데, createBoard() 메소드를 사용해서, 그 안에는 BoardRequestDto 객체의 requestDto 정보, UserDetailsImpl 객체의 userDetails 정보가 담겨져있는 상태로 받을꺼다!
        //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌)
        //BoardRequestDto: 요청을 받을 때는 Request Body 부분에서 오는 데이터를 DTO 를 만들어서 받는다.(변환이 필요)
            //이유? client 와 controller 는 DTO 로 데이터를 교환하기 때문
        //@AuthenticationPrincipal: 인증 객체(Authentication)의 Principal 부분의 값을 가져온다
        //UserDetailsImpl userDetails: 인증 객체를 만들 때, Principal 부분에 userDetails 를 넣었기 때문에, userDetails 를 파라미터로 받아올 수 있었음
    public ResponseEntity<BoardResponseDto> createBoard(@RequestBody BoardRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //다시 돌려줘: boardService 에서 createBoard() 메소드를 사용해서, 그 안에는 requestDto, userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
            //return boardService. 를 쓸 수 있는 이유? private final BoardService boardService; 로 의존성 주입을 해줘서, boardService 와 연결해둔 상태이기 때문!
        return ResponseEntity.ok().body(boardService.createBoard(requestDto, userDetails.getUser()));
    }

//전체 게시글 목록 조회
    @GetMapping("/boards")
    //미리 말해두기: BoardResponseDto 타입의 데이터를 List 형식으로 반환 받을 건데, getListBoards() 메소드를 사용해서, 그 안에는 UserDetailsImpl 객체의 userDetails 정보, category 정보가 담겨져있는 상태로 받을꺼다!
        //@RequestParam: 요청 매개변수에 들어있는 기본 타입 데이터를 메서드 인자로 받아올 수 있다.
            //요청 매개변수: URL 요청의 localhost:8080?username=value&age=value 의 쿼리 매개변수와 HTML 태그의 Form 데이터가 해당
            //되도록 String 타입 활용
    public ResponseEntity<List<BoardResponseDto>> getListBoards(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam String category) {
        //다시 돌려줘: boardService 에서 getListBoards() 메소드를 사용해서, 그 안에는 userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
            //해당 메소드의 모든 데이터를 반환할 때는 () 괄호를 비워둔다?
        return ResponseEntity.ok().body(boardService.getListBoards(userDetails.getUser(),category));
    }

//선택한 게시글 조회
    @GetMapping("/board/{id}")
    //미리 말해두기
        //@PathVariable: URL 경로에 변수를 넣기
            //예시: localhost:8080/api/board/{12}
    public ResponseEntity<BoardResponseDto> getBoards(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //다시 돌려줘
        return ResponseEntity.ok().body(boardService.getBoard(id, userDetails.getUser()));
    }

//선택한 게시글 수정(변경)
    @PutMapping("/board/{id}")
    //미리 말해두기
    public ResponseEntity<BoardResponseDto> updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //다시 돌려줘
            //requestDto 넣는 이유? Client 가 수정한 내용(= 요청한 내용)을 받기 위해
        return ResponseEntity.ok().body(boardService.updateBoard(id, requestDto, userDetails.getUser()));
    }

//선택한 게시글 삭제
    @DeleteMapping("/board/{id}")
    //미리 말해두기
    public ResponseEntity<MsgResponseDto> deleteBoard(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //다시 돌려줘
        boardService.deleteBoard(id,userDetails.getUser());
        //다시 돌려줘
            //return new 를 사용해서 MsgResponseDto() 메소드를 호출하는 이유?
            //HttpStatus.OK.value(): 상태 코드 를 담아서 보낸다.
        return ResponseEntity.ok().body(new MsgResponseDto("삭제 성공", HttpStatus.OK.value()));
    }

//게시글 좋아요
    @PostMapping("/board/like/{boardId}")
    //미리 말해두기: MsgResponseDto 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, saveBoardLike() 메소드를 사용해서, 그 안에는 boardId 정보, userDetails 정보가 담겨져있는 상태로 받을꺼다!
        //ResponseEntity: HttpEntity 클래스를 상속받아 구현한 클래스. HttpStatus, HttpHeaders, HttpBody 를 포함. 결과값, 상태코드, 헤더값을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있음
    public ResponseEntity<MsgResponseDto> saveBoardLike(
            @PathVariable Long boardId,
            @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, boardService 에서 saveBoardLike() 메소드를 사용해서, 그 안에는 boardId, userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
            //ResponseEntity.ok(): 상태코드를 반환
        return ResponseEntity.ok().body(boardService.saveBoardLike(boardId, userDetails.getUser()));
    }
}

CommentController

package com.sparta.springboards.controller;

import com.sparta.springboards.dto.CommentRequestDto;
import com.sparta.springboards.dto.CommentResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.security.UserDetailsImpl;
import com.sparta.springboards.service.CommentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/comment")

public class CommentController {

    //의존성 주입
    //CommentService 와 연결
    private final CommentService commentService;

//댓글 작성
    @PostMapping("/{id}")
    //미리 말해두기: CommentResponseDto 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, createComment() 메소드를 사용해서, 그 안에는 id 정보, commentRequestDto 정보, userDetails 가 담겨져있는 상태로 받을꺼다!
    public ResponseEntity<CommentResponseDto> createComment(@PathVariable Long id, @RequestBody CommentRequestDto commentRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, commentService 에서 createComment() 메소드를 사용해서, 그 안에는 id, commentRequestDto, userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
        return ResponseEntity.ok().body(commentService.createComment(id, commentRequestDto, userDetails.getUser()));
    }

//댓글 수정(변경)
    @PutMapping("/{boardId}/{cmtId}")
    public ResponseEntity<CommentResponseDto> updateComment(@PathVariable Long boardId, @PathVariable Long cmtId, @RequestBody CommentRequestDto commentRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseEntity.ok().body(commentService.updateComment(boardId, cmtId, commentRequestDto, userDetails.getUser()));
    }

//댓글 삭제
    @DeleteMapping("/{boardId}/{cmtId}")
    public ResponseEntity<MsgResponseDto> deleteComment(@PathVariable Long boardId, @PathVariable Long cmtId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        commentService.deleteComment(boardId, cmtId, userDetails.getUser());
        return ResponseEntity.ok(new MsgResponseDto("삭제 성공", HttpStatus.OK.value()));
    }

//댓글 좋아요
    @PostMapping("/like/{cmtId}")
    public ResponseEntity<MsgResponseDto> commentLike(@PathVariable Long cmtId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseEntity.ok(commentService.CommentLike(cmtId, userDetails.getUser()));
    }
}

UserController

package com.sparta.springboards.controller;

import com.sparta.springboards.dto.LoginRequestDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.dto.SignupRequestDto;
import com.sparta.springboards.security.UserDetailsImpl;
import com.sparta.springboards.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@RestController     //Client 요청으로부터 view 를 반환. MVC 패턴의 Controller 클래스임을 명시
@RequiredArgsConstructor
@RequestMapping("/api/auth")

public class UserController {

    //UserService 와 연결
    private final UserService userService;

//회원가입 구현
    @PostMapping("/signup")
    //미리 말해두기: MsgResponseDto 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, signup() 메소드를 사용해서, 그 안에는 signupRequestDto 정보가 담겨져있는 상태로 받을꺼다!
        //@Valid: Controller 에서 유효성 검사를 할 곳에 붙임
            //유효성 검사: 데이터가 server 혹은 DB 로 옮겨지기 전, 개발자가 만든 조건에 부합하는지 확인, 검증하는 작업 (이미 가입된 ID 입니다. 비밀번호는 영문,숫자,특수문자가 혼합되어야 합니다 등...)
    public ResponseEntity<MsgResponseDto> signup(@Valid @RequestBody SignupRequestDto signupRequestDto) {
        //다시 돌려줘: userService 에서 signup() 메소드를 사용해서, 그 안에는 signupRequestDto 를 담아서, DB 로부터 return(반환) 받는다.
        userService.signup(signupRequestDto);
        //다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, MsgResponseDto 에서 (String msg, int statusCode)를 담아서, DB 로부터 return(반환) 받는다.
        return ResponseEntity.ok(new MsgResponseDto("회원가입 완료", HttpStatus.OK.value()));
    }

//로그인 구현
    @PostMapping("/login")
        //HttpServletRequest response : 누가 로그인 했는지 알기위한 토큰을 담고 있음 ?
    public ResponseEntity<MsgResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
        //다시 돌려줘: userService 에서 login() 메소드를 사용해서, 그 안에는 loginRequestDto 정보, response 정보를 담아서, DB 로부터 return(반환) 받는다.
        userService.login(loginRequestDto, response);
        //다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, MsgResponseDto 에서 (String msg, int statusCode)를 담아서, DB 로부터 return(반환) 받는다.
        return ResponseEntity.ok(new MsgResponseDto("로그인 완료",HttpStatus.OK.value()));
    }

//회원 탈퇴
    @DeleteMapping("/signOut")
    //미리 말해두기:
    public ResponseEntity<MsgResponseDto> signOut(@RequestBody LoginRequestDto loginRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
        userService.signOut(loginRequestDto,userDetails.getUser());
        return ResponseEntity.ok(new MsgResponseDto("회원 탈퇴 완료", HttpStatus.OK.value()));
    }
}
//@ResponseBody
    //http 요청 body 를 자바 객체로 전달받을 수 있다. = String 값 자체를 반환하고 싶을때 쓰면된다.
    //@Controller 의 경우, body 를 자바객체로 받기 위해서는 @ResponseBody 를 반드시 명시해주어야한다.
    //BoardController 에서는 @RestController 를 썼기 때문에, @ResponseBody 를 따로 쓰지 않았음

dto

BoardRequestDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
    //접근자, 설정자: private 설정된 멤버변수에 대하여 외부 클래스에서 접근이 불가하기 때문에, 메소드 형태로 해당 클래스에 만들어 놓은것
@Getter     //접근자: 매개변수는 X. 반환유형은 해당변수랑 같은 타입
@Setter     //설정자: 반환유형은 X. 해당변수와 같은 타입의 데이터를 매개변수로 받아서 해당변수에 대입

@NoArgsConstructor      //파라미터가 없는 기본생성자를 생성
//@AllArgsConstructor     //해당 객체 내에 있는 모든 변수들을 인수로 받는 생성자를 만들어냄 --> 모든 필드값을 파라미터로 받는 생성자를 굳이 생성해주지 않아도 됨

public class BoardRequestDto {

    //필드
    private String title;
    private String contents;
    private String category;
}

BoardResponseDto

package com.sparta.springboards.dto;

import com.sparta.springboards.entity.Board;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor

public class BoardResponseDto {

    //필드
    private Long id;
    private String title;
    private String category;
    private String contents;
    private String username;
    private int boardLikeCount;
    private boolean boardLikeCheck;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    //생성자
    public BoardResponseDto(Board board) {      //매개변수를 가지는 생성자
        //board 매개변수로 들어온 데이터를 getId(), getTitle()... 에 담아서, this.id, this.title... 필드에 저장 (Client 에게로 보내기 위해)
        this.id = board.getId();
        this.title = board.getTitle();
        this.category = board.getCategory();
        this.contents = board.getContents();
        this.username = board.getUsername();
        this.createdAt = board.getCreatedAt();
        this.modifiedAt = board.getModifiedAt();
    }

    private List<CommentResponseDto> commentList = new ArrayList<>();

    public BoardResponseDto(Board board, List<CommentResponseDto> commentList, boolean boardLikeCheck) {
        //board, commentList, boardLikeCheck 매개변수로 들어온 데이터를 board 매개변수의 getId(), getTitle()... 에 담아서, this.id, this.title... 필드에 저장 (Client 에게로 보내기 위해)
        this.id = board.getId();
        this.title = board.getTitle();
        this.category = board.getCategory();
        this.contents = board.getContents();
        this.username = board.getUsername();
        this.boardLikeCount = board.getBoardLikeList().size();      //size(): Collection Object(ArrayList, Set etc) 에 사용
        this.boardLikeCheck = boardLikeCheck;
        this.createdAt = board.getCreatedAt();
        this.modifiedAt = board.getModifiedAt();
        this.commentList = commentList;
    }
}

CommentRequestDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter

public class CommentRequestDto {
    private String comment;
}

CommentResponseDto

package com.sparta.springboards.dto;

import com.sparta.springboards.entity.Comment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
@NoArgsConstructor

//댓글 표시
public class CommentResponseDto {
    private Long id;
    private String username;
    private String comment;
    private Integer CommentLike;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    public CommentResponseDto (Comment comment, int cnt) {
        this.id = comment.getId();
        this.username = comment.getUsername();
        this.comment = comment.getComment();
        this.CommentLike = cnt;
        this.createdAt = comment.getCreatedAt();
        this.modifiedAt = comment.getModifiedAt();
    }
}

LoginRequestDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter

public class LoginRequestDto {
    //필드
    private String username;
    private String password;
}

MsgResponseDto

package com.sparta.springboards.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder        //해당 클래스에 자동으로 빌더를 추가

public class MsgResponseDto {
    //필드
    private String msg;          //BoardResponseDto 에 있던 필드와 생성자를 별도로 ResponseDto 를 만들어서 분리해줄 수도 있다!
    private int statusCode;             //어떤 경우에?  기능에 따라서 반환하려는 데이터가 다른 경우!
}

SecurityExceptionDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor

public class SecurityExceptionDto {
    private int statusCode;
    private String msg;

    //Token 에 대한 오류가 발생했을 때, Client 에게 넘겨줄 Exception 한 결과값
    public SecurityExceptionDto(int statusCode, String msg) {
        this.statusCode = statusCode;
        this.msg = msg;
    }
}

SignupRequestDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Setter
@Getter
public class SignupRequestDto {

//필드
    @Size(min = 4,max = 10,message ="아이디는 4에서 10자 사이 입니다.")         //문자열, 배열등의 크기 설정
    @Pattern(regexp = "[a-z0-9]*$",message = "아이디 형식이 일치하지 않습니다.")      //정규식 설정    --> Dto 에 설정해주는 것이 좋다
    private String username;

    @Size(min = 8,max = 15,message ="비밀번호는 8에서 15자 사이 입니다.")
    @Pattern(regexp = "[a-zA-Z0-9`~!@#$%^&*()_=+|{};:,.<>/?]*$",message = "비밀번호 형식이 일치하지 않습니다.")
    private String password;
    private boolean admin = false;       //admin 인지 아닌지 확인
    private String adminToken = "";
}

정규식은 entity 보단 dto에 적자.

entity

Board

package com.sparta.springboards.entity;

import com.sparta.springboards.dto.BoardRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter

//JPA 에서 사용할 엔티티 이름 지정
//DB 테이블 역할을 함
//기본 생성자 필수
@Entity
@Table(name = "Board")      //name 속성: JPA 에서 사용할 엔티티 이름 지정. 따로 지정하지 않을 시 기본값으로 클래스 이름을 그대로 Entity 이름으로 지정
@NoArgsConstructor

public class Board extends Timestamped {

//필드
    //PK(Primary Key: DB 테이블의 유일한 값)임을 지정해줌. 특정 속성을 기본키로 설정
    //@Id 어노테이션만 적게될 경우 기본키값을 직접 부여해줘야 함 --> 그래서 @GeneratedValue 를 사용
    @Id
    //@GeneratedValue(strategy = GenerationType.IDENTITY) 또는 SEQUENCE) 또는 TABLE) 또는 AUTO)
        //각각의 기능: 기본 키 생성을 DB에 위임 (Mysql), DB 시퀀스를 사용해서 기본 키 할당 (ORACLE), 키 생성 테이블 사용 (모든 DB 사용 가능), 선택된 DB에 따라 자동으로 전략 선택
            //AUTO: DB에 따라 전략을 JPA 가 자동으로 선택되므로, DB를 변경해도 코드를 수정할 필요 없다는 장점
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    //comment : board 관계 --> 다대일 양방향 관계
        //cascade = CascadeType.REMOVE: 부모 엔티티를 삭제할 때, 연관된 자식 데이터에 대한 DELETE 쿼리를 실행 (자식 엔티티를 삭제하는 쿼리가 먼저 실행되고, 그 다음 부모 엔티티를 삭제하는 쿼리가 실행됨)
            //즉, board 를 삭제할 때, comment 도 삭제한다. (comment 가 먼저 삭제되고, board 가 삭제됨)
            //cascade: 영속성 전이
                //영속성: 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성. 영속성을 갖지 않으면 데이터는 메모리에서만 존재. 프로그램이 종료되면 해당 데이터는 모두 사라짐.
                    //그래서 데이터를 파일이나 DB에 영구 저장함으로써, 데이터에 영속성을 부여.
    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
    //@OrderBy("필드명 [ASC | DESC] ") 형식
    @OrderBy("createdAt DESC")          //--> repository 에서 사용하자.
    private List<Comment> comments = new ArrayList<>();

    //boardLikeList : board 관계 --> 다대일 양방향 관계
    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
    private List<BoardLike> boardLikeList = new ArrayList<>();

    //column: DB 에서 가져온다
    @Column(nullable = false)       //nullable: null 허용 여부
    private String title;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String contents;

    @Column
    private String category;

    //boardList : user 관계 --> 다대일 관계
    //FK = 연관관계의 주인(N 쪽이 주인)
    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

//생성자
//게시글 작성
    //requestDto, user 매개변수로 들어온 데이터를 requestDto, user 매개변수의 getId(), getUsername()... 에 담아서, this.title, this.username... 필드에 저장 (this 이므로, 여기 필드)
        //Board: 생성자이므로, 클래스명과 동일함
    public Board(BoardRequestDto requestDto, User user) {
        this.title = requestDto.getTitle();
        this.username = user.getUsername();
        this.contents = requestDto.getContents();
        this.category = requestDto.getCategory();
        this.user = user;
    }

//메소드
//선택한 게시글 수정(변경)
    //update() 메소드를 사용해서, 그 안에는 boardrequestDto 매개변수로 들어온 데이터를 boardrequestDto 매개변수의 getTitle(), getContents() 에 담아서, this.title, this.contents 필드에 저장 (this 이므로, 여기 필드)
    public void update(BoardRequestDto boardrequestDto) {
        this.title = boardrequestDto.getTitle();
        this.category = boardrequestDto.getCategory();
        this.contents = boardrequestDto.getContents();
    }
}

BoardLike

package com.sparta.springboards.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@Entity
@NoArgsConstructor
public class BoardLike {

//필드
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    //boardLikeList : board 관계 --> 다대일 양방향 관계
    @ManyToOne
    @JoinColumn(name = "BOARD_ID", nullable = false)        //like : board ⇒ 다대일 필요한가?
    private Board board;                                      // Post 엔티티가 Like 엔티티를 List 로 가져야할 명분이 있는가? 게시글이 자신이 받을 좋아요의 갯수를 알아야 할 필요가 있을까?
                                                                // 필요하다면, 양방향 매핑
    //boardLike : user 관계 --> 다대일 단방향 관계
    //FK = 연관관계의 주인(N 쪽이 주인)
    @ManyToOne
    @JoinColumn(name = "USER_ID", nullable = false)     //테이블에서 name 속성을 따로 적어주지 않는 경우, name 은 해당 객체명이 된다.(여기선 user)
    private User user;

//생성자
    public BoardLike(Board board, User user) {
        this.board = board;
        this.user = user;
    }
}

Comment

package com.sparta.springboards.entity;

import com.sparta.springboards.dto.CommentRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;

@Entity
@Getter
@NoArgsConstructor
public class Comment extends Timestamped {

//필드
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    //comment : board 관계 --> 다대일 양방향 관계
    //FK = 연관관계의 주인(N 쪽이 주인)
    @ManyToOne
    @JoinColumn(name = "board_id", nullable = false)
    private Board board;

    //commentList : user 관계 --> 다대일 양방향 관계
    //FK = 연관관계의 주인(N 쪽이 주인)
    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String comment;

//생성자
    public Comment(CommentRequestDto commentRequestDto, Board board, User user) {
        this.comment = commentRequestDto.getComment();
        this.username = user.getUsername();
        this.board = board;
        this.user = user;
    }

//메소드
    public void update(CommentRequestDto commentRequestDto) {
        this.comment = commentRequestDto.getComment();
    }
}

CommentLike

package com.sparta.springboards.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@AllArgsConstructor
@Getter
@Entity
@Builder
@NoArgsConstructor
public class CommentLike {
    
//필드    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long CommentLikeId;

    //commentLike : comment  관계 --> 다대일 단방향 관계
    @ManyToOne
    @JoinColumn(name = "comment_id")        //테이블에서 name 속성을 따로 적어주지 않는 경우, name 은 해당 객체명이 된다.
    private Comment comment;

    //commentLike : user  관계 --> 다대일 단방향 관계
    @ManyToOne
    @JoinColumn(name = "comment_like_User_id")      //테이블에서 name 속성을 따로 적어주지 않는 경우, name 은 해당 객체명이 된다.
    private User user;
}

Timestamped

package com.sparta.springboards.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter

//객체의 입장에서 공통 매핑 정보가 필요할 때 사용
//공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고, 속성만 상속 받아서 사용하고 싶을 때 --> Board 가 Timestamped 를 상속받고있다!
@MappedSuperclass

//@EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능(시간에 대해서 자동으로 값을 넣어주는 기능)을 포함
    //Entity 가 삽입, 삭제, 수정, 조회 등...의 작업을 하기 전/후에 어떤 작업을 하기 위해 이벤트 처리를 위해서
    //예시: 데이터 삽입 전 또는 수정 전에 createdAt 과 modifiedAt 의 시간을 조작
@EntityListeners(AuditingEntityListener.class)

public class Timestamped {

//필드
//createdAt, modifiedAt 컬럼 2개를 가진다
    //생성일자
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    //마지막 수정일자
    @LastModifiedDate
    @Column
    private LocalDateTime modifiedAt;
}

User

package com.sparta.springboards.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor
@Entity(name = "users")

public class User {

//필드
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    //boardList : user 관계 --> 다대일 양방향 관계
    @OneToMany(mappedBy = "user")
    private List<Board> boardList = new ArrayList<>();

    //commentList : user 관계 --> 다대일 양방향 관계
    @OneToMany(mappedBy = "user")
    private List<Comment> commentList = new ArrayList<>();

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    //Enum 의 선언된 상수의 이름을 String 클래스 타입으로 변환하여 DB에 넣는다.(즉, DB 클래스 타입은 String 이다.)
        //UserRoleEnum 에서 열거혐(enum) 으로 필드가 선언되어있음을 확인 가능함!
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

//생성자
    public User(String username, String password, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }
}

UserRoleEnum

package com.sparta.springboards.entity;

//주의! 열거혐(enum)으로 만들어야 함
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";
    }
}

exception

CustomException

package com.sparta.springboards.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor

//RuntimeException: JVM 의 정상 작동 中 발생할 수 있는 예외의 슈퍼클래스
//CustomException: 전역으로 사용된다.
//RuntimeException 을 상속받아서 Unchecked Exception 으로 활용
    //RuntimeException 을 상속하지 않은 클래스는 Checked Exception
    //RuntimeException 을 상속한 클래스는 Unchecked Exception 으로 분류
public class CustomException extends RuntimeException{
    private final ErrorCode errorCode;
}

ErrorCode

package com.sparta.springboards.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor

public enum ErrorCode {

/* Custom Error Code */

    // 토큰 관련 ErrorCode
    INVALID_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, "권한 정보가 없는 토큰입니다"),
    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 유효하지 않습니다."),


    // 유저 관련 ErrorCode
    AUTHORIZATION(HttpStatus.BAD_REQUEST, "작성자만 수정/삭제할 수 있습니다."),
    DUPLICATED_USERNAME(HttpStatus.BAD_REQUEST, "중복된 username 입니다"),
    INVALID_FORMAT(HttpStatus.BAD_REQUEST, "username과 password의 형식이 올바르지 않습니다."),
    NOT_MATCH_INFORMATION(HttpStatus.BAD_REQUEST, "회원정보가 일치하지 않습니다."),
    NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "회원을 찾을 수 없습니다."),


    // 게시글 관련 ErrorCode
    NOT_FOUND_BOARD(HttpStatus.BAD_REQUEST, "게시글을 찾을 수 없습니다."),
    NOT_EXIST_CATEGORY(HttpStatus.BAD_REQUEST, "카테고리가 존재하지 않습니다."),


    // 댓글 관련 ErrorCode
    NOT_FOUND_COMMENT(HttpStatus.BAD_REQUEST, "댓글을 찾을 수 없습니다.");

    private final HttpStatus httpStatus;
    private final String message;
}

ErrorResponse

package com.sparta.springboards.exception;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.ResponseEntity;

import java.time.LocalDateTime;

@Getter
@Builder    //모든 요소를 받는 package-private 생성자가 자동으로 생성

//@Builder 를 사용한 경우, @AllArgsConstructor 가 암묵적으로 적용된 상태임 (즉, 생성자를 굳이 만들어주지 않아도 되는 상태)
    //그러나, 우리가 생성자를 직접 만들어주게 된 경우, @AllArgsConstructor 도 넣어줘야함 ?
@AllArgsConstructor

public class ErrorResponse {
    
//필드    
    private final LocalDateTime timestamp = LocalDateTime.now();    //로컬 컴퓨터의 현재 날짜와 시간 출력
    private final int status;
    private final String error;
    private final String code;
    private final String message;

//생성자
    public ErrorResponse(ErrorCode errorCode) {
        this.status = errorCode.getHttpStatus().value();    //.value() 쓰는 이유?  에러코드가 int 타입이다.
        this.code = errorCode.name();
        this.error = errorCode.getHttpStatus().name();    //.name() 쓰는 이유?  에러코드가 String 타입이다.
        this.message = errorCode.getMessage();
    }

    //ErrorResponse 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, toResponseEntity() 메소드를 사용해서, 그 안에는 errorCode 정보가 담겨져있는 상태로 받을꺼다!
    public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
        return ResponseEntity                                       //builder 사용법?
                .status(errorCode.getHttpStatus())
                .body(ErrorResponse.builder()
                        .status(errorCode.getHttpStatus().value())
                        .error(errorCode.getHttpStatus().name())
                        .code(errorCode.name())
                        .message(errorCode.getMessage())
                        .build());
    }
}

GlobalExceptionHandler

package com.sparta.springboards.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

//로깅에 대한 추상 레이어를 제공하는 인터페이스의 모음
    //장점? 추후에 필요로 의해 로깅 라이브러리를 변경할 때, 코드의 변경 없이 가능
    //로깅: 로그(log, 정보를 제공하는 일련의 기록)를 생성하도록 시스템을 작성하는 활동
        //log 필요 이유?  로그를 작성해두면, 어떤 동작 중인지, 어느 부분에 에러가 났는지 파악 가능
    //추상 레이어 ?
@Slf4j
@RestControllerAdvice       //전역적으로 에러를 처리

//ResponseEntityExceptionHandler: Spring 에서는 스프링 예외를 미리 처리해둔 추상 클래스를 제공해줌. 이 추상 클래스가 ResponseEntityExceptionHandler
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    //@ExceptionHandler: @Controller, @RestController 가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리. 특정 Exception 을 지정해서 별도로 처리해줌
        //사용법: 1. @ExceptionHandler({ 인자로 캐치하고 싶은 예외클래스1, 인자로 캐치하고 싶은 예외클래스2 }) 이런식으로 두 개 이상 등록도 가능
    @ExceptionHandler(value = {CustomException.class})
    //CustomException: 기본적으로 제공되는 Exception 외에 사용
    protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
        //log: 로깅 메시지를 전달
        log.error("handleDataException throw Exception : {}", e.getErrorCode());
        return ErrorResponse.toResponseEntity(e.getErrorCode());
    }

    //Bean Validation 예외 발생시
    @Override
    //handleMethodArgumentNotValid: Controller 에서 @Valid 를 사용했을 때, MethodArgumentNotValidException 예외가 발생
        //문제 원인? 위의 @ExceptionHandler 에서 이미 MethodArgumentNotValidException 이 구현되어있는데, GlobalExceptionHandler 에서 동일한 예외 처리를 하게 되면, Ambiguous(모호성) 문제가 발생
        //해결법? handleMethodArgumentNotValid 를 오버라이딩 하기
    //object: 모든 클래스들의 최상위 클래스 --> 따라서, 모든 클래스는 Object 클래스에서 상속을 받음
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers,
                                                                  HttpStatus status,
                                                                  WebRequest request) {
        ErrorCode errorCode = ErrorCode.INVALID_FORMAT;
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(new ErrorResponse(errorCode));
    }
}

jwt

JwtAuthFilter

package com.sparta.springboards.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.springboards.dto.SecurityExceptionDto;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@RequiredArgsConstructor

public class JwtAuthFilter extends OncePerRequestFilter {

    //의존성 주입
    private final JwtUtil jwtUtil;

    @Override
        //request: 토큰을 가져온다
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //jwtUtil 에서 resolveToken() 메소드를 사용해서, 그 안에는 request 정보를 담아서, token 에 저장
        String token = jwtUtil.resolveToken(request);

        //token 이 null 이 아닌 경우(토큰이 이미 있는 경우 = 로그인, 회원가입이 돼있는 경우),
            //로그인, 회원가입은 인증이 필요 없음 --> 그래서 Token 이 Header 에 들어가 있지 않음 --> 이런 처리(if) 해주지 않으면, 토큰을 검증하는 부분에서 exception 발생해버림
                //인증이 필요없는 URL 는 if 문이 수행되지 않고, 다음 Filter 로 이동
        if (token != null) {
            //jwtUtil 에서, validateToken() 메소드를 실행해서 false 가 된다면 (= JWT 검증 부분)
            if (!jwtUtil.validateToken(token)) {
                //jwtExceptionHandler() 메소드를 실행하는데, 그 안에는 response, "Token Error", HttpStatus.UNAUTHORIZED.value() 정보를 담아둔다.
                    //jwtExceptionHandler() 메소드는 아래에.
                jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
                return;
            }
            //token 이 null 이 아닌 경우(= Token 에 문제가 없다면)(바깥 if 문), jwtUtil 에서 token 정보를 담은 getUserInfoFromToken() 메소드를 실행해라. (= JWT 에서 사용자 정보 가져오기 부분)
            Claims info = jwtUtil.getUserInfoFromToken(token);
            //그리고, info 매개변수에 getSubject() 정보를 담은 setAuthentication() 메소드를 실행하라.
            setAuthentication(info.getSubject());
        }
        //filterChain 매개변수에, doFilter() 메소드를 사용해서, request,response 정보를 담아라.
        filterChain.doFilter(request,response);
    }

// 인증 객체 생성 및 등록
        //token 이 null 이 아닌 경우(위의 if 문에서 이어짐)
    public void setAuthentication(String username) {
        //SecurityContextHolder: authentication 을 담고 있는 Holder
        SecurityContext context = SecurityContextHolder.createEmptyContext();   //SecurityContext context 안에 인증한 객체가 들어가게 됨
        Authentication authentication = jwtUtil.createAuthentication(username);     // 인증 객체 생성 부분
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

        //위의 if(token != null) 인 경우 中 jwtUtil 에서, validateToken() 메소드를 실행해서 false 가 되는 경우
            // 여기에서 Token 에 대한 오류가 발생했을 때, Exception 한 결과값을 Client 에게 넘긴다
    public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
        response.setStatus(statusCode);
        response.setContentType("application/json");

        //try-catch 문 (try 블럭 내에는 예외 발생 가능성 있는 문장이 들어간다.)
            //1. 예외가 try 블럭 내에서 발생하지 않은 경우 --> catch 블럭을 거치지 않고 try 문 자체를 빠져나감
            //2. 예외가 try 블럭 내에서 발생한 경우 --> 이를 처리할 수 있는 catch 블럭이 있는지 찾음
        try {
            //ObjectMapper 를 통해서 변환한다
            String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
            //write: 문자 기반 출력 스트림의 최상위 추상 클래스
            response.getWriter().write(json);
        //catch(Exception e): 어떤 종류의 예외가 발생하더라도 이 catch 블럭에 의해서 처리가 될 수 있음
            //보통 마지막 catch 블럭에 Exception e를 선언
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

JwtUtil

package com.sparta.springboards.jwt;

import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.security.UserDetailsServiceImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j

//빈 등록을 위해 사용하는데, @Component 는 Bean Configuration 파일에 Bean 을 따로 등록하지 않아도 사용 가능하게 만들어줌
@Component
@RequiredArgsConstructor

public class JwtUtil {  //빈이 등록됐다는 '나뭇잎 모양' 확인 가능

    //의존성 주입
    private final UserDetailsServiceImpl userDetailsService;

//토큰 생성에 필요한 값
    //Authorization: Bearer <JWT> 에서 Header 에 들어가는 KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";

    // 사용자 권한 값의 KEY: 사용자 권한도 Token 안에 넣는데, 이를 가져올 때 사용하는 KEY 값
    public static final String AUTHORIZATION_KEY = "auth";

    // Token 식별자: Token 을 만들 때, 앞에 들어가는 부분
    private static final String BEARER_PREFIX = "Bearer ";

    // 토큰 만료시간: 밀리세컨드 기준. 60 * 1000L는 1분. 60 * 60 * 1000L는 1시간
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    //@Value("${프로퍼티 키값}") : application.properties 에 정의한 내용을 가져와서 사용 가능
        //사용 이유? 수정과 관리가 용이하기 때문
    @Value("${jwt.secret.key}")
    private String secretKey;   //@Value() 안에 application.properties 에 넣어둔 KEY 값(jwt.secret.key=7ZWt7ZW0O...pA=)을 넣으면, 가져올 수 있음
    private Key key;    //Key 객체: Token 을 만들 때 넣어줄 KEY 값
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;


    //@PostConstruct: 의존성 주입이 이루어진 후, 초기화를 수행하는 메서드
        //사용 이유?
            //1. 생성자가 호출되었을 때, 빈은 초기화되지 않았음(의존성 주입이 이루어지지 않았음)
            //2. 어플리케이션이 실행될 때, bean 이 오직 한 번만 수행되게 해서 bean 이 여러 번 초기화되는 걸 방지
    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);   //Base64로 인코딩되어 있는 것을, 값을 가져와서(getDecoder()) 디코드하고(decode(secretKey)), byte 배열로 반환
        key = Keys.hmacShaKeyFor(bytes);
    }

//Header 에서 Token 가져오기
    public String resolveToken(HttpServletRequest request) {
        //HttpServletRequest request 객체 안에 들어간 Token 값을 가져옴
        //() 안에는 어떤 KEY 를 가져올지 정할 수 있음(여기선 AUTHORIZATION_HEADER 안에 있는 KEY 의 값을 가져옴)
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        //그리고, 가져온 코드가 있는지, BEARER_PREFIX 로 시작하는지 확인
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            //substring() 메소드를 사용해서, 앞의 일곱 글자를 지워줌 --> 앞의 일곱 글자는 Token 과 관련 없는 String 값이므로
            return bearerToken.substring(7);
        }
        return null;
    }

//JWT 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +      //Token 앞에 들어가는 부분임
                //실제로 만들어지는 부분
                Jwts.builder()
                        //setSubject 라는 공간 안에 username 를 넣는다
                        .setSubject(username)
                        //claim 이라는 공간 안에 AUTHORIZATION_KEY 사용자의 권한을 넣고(이 권한을 가져올 때는 지정해둔 auth KEY 값을 사용해서 넣음)
                        .claim(AUTHORIZATION_KEY, role)
                        //이 Token 을 언제까지 유효하게 가져갈건지. date: 위의 Date date = new Date() 에서 가져온 부분
                        //getTime(): 현재 시간
                        //+ TOKEN_TIME: 우리가 지정해 둔 시간(TOKEN_TIME = 60 * 60 * 1000L)을 더한다 = 즉, 지금 기준으로 언제까지 가능한지 결정해줌
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        //Token 이 언제 만들어졌는지 넣는 부분
                        .setIssuedAt(date)
                        //secret key 를 사용해서 만든 key 객체와
                        //어떤 암호화 알고리즘을 사용해서 암호화할것인지 지정(SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256)
                        .signWith(key, signatureAlgorithm)
                        //반환
                        .compact();
    }

//JWT 검증
    public boolean validateToken(String token) {
        try {
            //Jwts: 위에서 JWT 생성 시 사용했던 것
            //parserBuilder(): 검증 방법
            //setSigningKey(key): Token 을 만들 때 사용한 KEY
            //parseClaimsJws(token): 어떤 Token 을 검증할 것인지
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);  //이 코드만으로 내부적으로 검증 가능
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }


//JWT 에서 사용자 정보 가져오기
    // 토큰에서 사용자 정보 가져오기 --> 위에서 validateToken() 으로 토큰을 검증했기에 이 코드를 사용할 수 있는 것
    public Claims getUserInfoFromToken(String token) {
        //getBody(): Token?? 안에 들어있는 정보를 가져옴
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

// 인증 객체 생성
    public Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);  //userDetailsService 에서 User 가 있는지 없는지 확인
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

repository

BoardLikeRepository

package com.sparta.springboards.repository;

import com.sparta.springboards.entity.BoardLike;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardLikeRepository extends JpaRepository<BoardLike, Long> {

    //Optional<T>: null 값일 수도 있는 변수를 감싸주는 Wrapper class
        //제너릭으로 값을 타입을 지정
        //NullPointerException 이 발생하지 않게 함
            //NPE 는 언제 발생? 실제 값이 아닌 null 을 가지고 있는 객체/변수를 호출 시
    //조회용 함수
        //단일건을 조회 --> findBy
        //여러건을 조회 --> findAllBy
    //Optional<BoardLike> findByBoardIdAndUserId(Long boarId, Long userId);
    void deleteByBoardIdAndUserId(Long boardId, Long userId);
    boolean existsByBoardIdAndUserId(Long boardId, Long userId);
}

BoardRepository

package com.sparta.springboards.repository;

import com.sparta.springboards.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

//게시글 작성
//<연결할 엔티티(테이블) 클래스 이름, ID 필드 타입>
    //Board 에서 id 필드의 타입이 Long 이기 때문
public interface BoardRepository extends JpaRepository<Board, Long> {

//전체 게시글 목록 조회
    List<Board> findAllByCategoryOrderByCreatedAtDesc(String category);
    List<Board> findAllByOrderByCreatedAtDesc();

//게시글 수정, 삭제
    //findByIdAndUserId(Long id, Long userId): Board 의 id 와 userId 가 일치하는 Board 를 가져온다
        //이유? 로그인한 해당 사용자가 작성한 게시글만 수정, 삭제되기 때문(본인 인증)
        //게시글 수정, 삭제에서 optional 사용 이유?  id 와 userid 를 비교해서 일치하지 않으면, 예외 처리를 하기때문
    Optional<Board> findByIdAndUserId(Long id, Long userId);
}

CommentLikeRepository

package com.sparta.springboards.repository;

import com.sparta.springboards.entity.CommentLike;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface CommentLikeRepository extends JpaRepository<CommentLike, Long> {
    Optional<CommentLike> findByCommentIdAndUserId(Long cmtId, Long userid);
    void deleteByCommentIdAndUserId(Long cmtId, Long userid);
    int countAllByCommentId(Long cmtId);
}

CommentRepository

package com.sparta.springboards.repository;

import com.sparta.springboards.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface CommentRepository extends JpaRepository<Comment, Long> {
    Optional<Comment> findByIdAndUserId(Long cmtId, Long UserId);
}

UserRepository

package com.sparta.springboards.repository;

import com.sparta.springboards.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);
    void deleteByUsername(String username);
}

security

UserDetailsImpl

package com.sparta.springboards.security;

import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;

//회원 상세정보 (UserServiceImpl) 를 통해 "권한 (Authority)" 설정 가능
public class UserDetailsImpl implements UserDetails {

//인증이 완료된 사용자 추가
    private final User user;
    private final String username;

    public UserDetailsImpl(User user, String username) {
        this.user = user;
        this.username = username;
    }

//인증완료된 User 를 가져오는 Getter
    public User getUser() {
        return user;
    }

//사용자의 권한 GrantedAuthority 로 추상화 및 반환
    @Override
        //Collection 이란?
        //? 의 의미?
        //GrantedAuthority: 현재 사용자가 가지고 있는 권한
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    //사용자의 ID, PWD Getter
    @Override                       //오버라이드 vs 오버로드 차이
    public String getUsername() {
        return this.username;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}

UserDetailsServiceImpl

package com.sparta.springboards.security;

import com.sparta.springboards.entity.User;
import com.sparta.springboards.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    //throws: 예외 떠넘기기
        //리턴타입 매개변수명 (매개변수 선언1, ...) throws 예외클래스1, ... {   }
            //예외클래스1,... : 상위 예외 클래스
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

        return new UserDetailsImpl(user, user.getUsername());
    }
}

service

BoardService

package com.sparta.springboards.service;

import com.sparta.springboards.dto.BoardRequestDto;
import com.sparta.springboards.dto.BoardResponseDto;
import com.sparta.springboards.dto.CommentResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.entity.*;
import com.sparta.springboards.exception.CustomException;
import com.sparta.springboards.repository.BoardLikeRepository;
import com.sparta.springboards.repository.BoardRepository;
import com.sparta.springboards.repository.CommentLikeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

import static com.sparta.springboards.exception.ErrorCode.*;

@Service
@RequiredArgsConstructor

public class BoardService {

    //DB 접근을 위한 의존성 주입
    private final BoardRepository boardRepository;
    private final BoardLikeRepository boardLikeRepository;
    private final CommentLikeRepository commentLikeRepository;

//게시글 작성
    //@Transactional : 클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션(데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위(더 이상 쪼개질 수 없는 최소의 연산))이 되도록 보장
        //사용 이유?
            //1. 해당 메서드를 실행하는 도중 메서드 값을 수정/삭제하려는 시도가 들어와도, 값의 신뢰성이 보장
            //2. 연산 도중 오류가 발생해도, rollback 해서 DB에 해당 결과가 반영되지 않도록 함
        //예시: 결제는 다른 사람과 독립적으로 이루어지며, 과정 중에 다른 연산이 끼어들 수 없다. 오류가 생긴 경우 연산을 취소하고 원래대로 되돌린다. 성공할 경우 결과를 반영한다
    @Transactional
    //미리 말해두기: BoardResponseDto 타입으로 반환 받을 건데, createBoard() 메소드를 사용해서, 그 안에는 BoardRequestDto 객체의 requestDto 정보, User 객체의 user 정보가 담겨져있는 상태로 받을꺼다!
    public BoardResponseDto createBoard(BoardRequestDto requestDto, User user) {
        //만약, requestDto 에서 category 가 "TIL" 또는 "Spring" 또는 "Java" 이라면,
        if(requestDto.getCategory().equals("TIL") || requestDto.getCategory().equals("Spring") || requestDto.getCategory().equals("Java")) {
            //requestDto, user 정보를 Board 객체에 담아서 boardRepository 에 board 라는 매개변수로 저장한다.
            Board board = boardRepository.save(new Board(requestDto, user));
            //다시 돌려줘: BoardResponseDto() 메소드에 board 정보를 담아서, DB 로부터 return(반환) 받는다.
            return new BoardResponseDto(board);
        //아니라면, 예외처리
        } else {
            throw new CustomException(NOT_EXIST_CATEGORY);
        }
    }


//전체 게시글 목록 조회
        //(readOnly = true): JPA 를 사용할 경우, 변경감지 작업을 수행하지 않아 성능상의 이점
    @Transactional(readOnly = true)
    //BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
    public List<BoardResponseDto> getListBoards(User user, String category) {

        //만약, category 가 "TIL" 또는 "Spring" 또는 "Java" 이라면,
        if (category.equals("TIL") || category.equals("Spring") || category.equals("Java")) {
            //category 를 findAllByCategoryOrderByCreatedAtDesc 정렬해서, boardList 에 담는다.
            List<Board> boardList = boardRepository.findAllByCategoryOrderByCreatedAtDesc(category);
            
            //for 문을 돌린 결과를 담을 List 를 미리 만들어둔다.
            List<BoardResponseDto> boardResponseDto = new ArrayList<>();
            //for 문을 이용하여, boardList 에 담긴 데이터들을 객체 Board 로 모두 옮긴다
            for (Board board : boardList) {
                //for 문을 돌린 결과를 담을 List 를 미리 만들어둔다.
                List<CommentResponseDto> commentList = new ArrayList<>();
                //for 문을 이용하여, board.getComments() 에 담긴 데이터들을 객체 Comment 로 모두 옮긴다
                for (Comment comment : board.getComments()) {
                    //commentList 에 comment 정보, comment.getId() 정보를 담아서 추가(add)한다.
                    commentList.add(new CommentResponseDto(comment, commentLikeRepository.countAllByCommentId(comment.getId())));
                }
                //boardResponseDto 에 board 정보, commentList 정보, checkBoardLike 정보를 담아서 추가(add)한다.
                    //checkBoardLike: 해당 회원의 해당 게시글 좋아요 여부. 게시글 좋아요는 해당 게시글의 아이디 정보(board.getId()), 유저 정보(user) 를 담고있다.
                boardResponseDto.add(new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user))));
            }
            return boardResponseDto;
        } else {
            //boardRepository 와 연결해서, 모든 데이터들을 내림차순으로, List 타입으로 객체 Board 에 저장된 데이터들을 boardList 안에 담는다
            List<Board> boardList =  boardRepository.findAllByOrderByCreatedAtDesc();

            List<BoardResponseDto> boardResponseDto = new ArrayList<>();
            for (Board board : boardList) {
                List<CommentResponseDto> commentList = new ArrayList<>();
                for (Comment comment : board.getComments()) {
                    commentList.add(new CommentResponseDto(comment, commentLikeRepository.countAllByCommentId(comment.getId())));
                }
                boardResponseDto.add(new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user))));
            }
            return boardResponseDto;
        }
    }

//선택한 게시글 조회
    @Transactional(readOnly = true)
    public BoardResponseDto getBoard(Long id, User user) {
        //boardRepository 와 연결해서, 게시글의 id 를 찾는다
            //boardRepository 와 연결? DB 에 접근하여, 이미 저장돼있는 id 가 있는지 확인하기 위함
        Board board = boardRepository.findById(id).orElseThrow (
                () -> new CustomException(NOT_FOUND_USER)
        );

        List<CommentResponseDto> commentList = new ArrayList<>();
        for (Comment comment : board.getComments()) {
            commentList.add(new CommentResponseDto(comment,commentLikeRepository.countAllByCommentId(comment.getId())));
        }
        return new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user)));
    }

//선택한 게시글 수정(변경)
    @Transactional
    public BoardResponseDto updateBoard(Long id, BoardRequestDto requestDto, User user) {
        Board board;
        //user 의 권한이 ADMIN 와 같다면,
        if(user.getRole().equals(UserRoleEnum.ADMIN)) {
            //boardRepository 와 연결해서, 게시글의 id 를 찾는다
            board = boardRepository.findById(id).orElseThrow(
                    //게시글이 DB 에 없다면
                    () -> new CustomException(NOT_FOUND_BOARD)
            );
        //user 의 권한이 ADMIN 이 아니라면,
        } else {
            //boardRepository 와 연결해서, 게시글의 id 와 유저의 id 를 비교한다.(같은 id 인지 확인) --> 아이디가 같은 유저만 수정 가능하도록
            board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    //게시글의 id 와 유저의 id 가 다르다면
                    () -> new CustomException(AUTHORIZATION)
            );
        }

        board.update(requestDto);

        List<CommentResponseDto> commentList = new ArrayList<>();
        for (Comment comment : board.getComments()) {
            commentList.add(new CommentResponseDto(comment, commentLikeRepository.countAllByCommentId(comment.getId())));
        }
        if(requestDto.getCategory().equals("TIL") || requestDto.getCategory().equals("Spring") || requestDto.getCategory().equals("Java")){
            return new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user)));
        }else{
            throw new CustomException(NOT_EXIST_CATEGORY);
        }
    }

//선택한 게시글 삭제
    @Transactional
    public void deleteBoard (Long id, User user) {
        Board board;
        if(user.getRole().equals(UserRoleEnum.ADMIN)) {
            board = boardRepository.findById(id).orElseThrow(
                    () -> new CustomException(NOT_FOUND_BOARD)
            );
        } else {
            board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new CustomException(AUTHORIZATION)
            );
        }

        //boardRepository 로 board 정보의 삭제를 위해 delete() 메소드를 실행한다.
        boardRepository.delete(board);
    }

// 게시글 좋아요 확인
    @Transactional(readOnly = true)
    public boolean checkBoardLike(Long boardId, User user) {
        // DB 에서 해당 유저의 게시글 좋아요 여부 확인
            //exists : 데이터가 존재하는지 확인. countBy 보다 성능측면에서 우수. existsBy 의 반환 타입은 boolean. countBy 는 반환 타입이 Long
                //이유? 검색 도중 중복되는 게 하나라도 존재하면, 그 즉시 쿼리를 종료하기때문에, 모든 개수를 세는 count 보다 압도적으로 좋다
                    //쿼리: DB 에게 정보를 요청하는 행위. 보통 DB 에서 특정 주제어, 어귀를 찾기위해 사용됨
        return boardLikeRepository.existsByBoardIdAndUserId(boardId, user.getId());
    }

    // 게시글 좋아요 생성 및 삭제
    @Transactional
    public MsgResponseDto saveBoardLike(Long boardId, User user) {
        // 1. 요청한 글이 DB 에 존재하는지 확인
        Board board = boardRepository.findById(boardId).orElseThrow(
                () -> new CustomException(NOT_FOUND_BOARD)
        );

        // 2. 해당 유저의 게시글 좋아요 여부를 확인해서 false 라면
        if (!checkBoardLike(boardId, user)) {
            // 3. 즉시 해당 유저의 게시글 좋아요를 DB 에 저장
                //saveAndFlush(): 즉시 DB 에 변경사항을 적용
                //save(): 바로 DB 에 저장되지 않고, 영속성 컨텍스트에 저장되었다가, flush() 또는 commit() 수행 시 DB에 저장
            boardLikeRepository.saveAndFlush(new BoardLike(board, user));
            return new MsgResponseDto("좋아요 완료", HttpStatus.OK.value());
        // 4. 게시글 좋아요 여부가 true 라면, 해당 유저의 게시글 좋아요를 DB 에서 삭제
        } else {
            boardLikeRepository.deleteByBoardIdAndUserId(boardId, user.getId());
            return new MsgResponseDto("좋아요 취소", HttpStatus.OK.value());
        }
    }
}

CommentService

package com.sparta.springboards.service;

import com.sparta.springboards.dto.CommentRequestDto;
import com.sparta.springboards.dto.CommentResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.entity.*;
import com.sparta.springboards.entity.Board;
import com.sparta.springboards.entity.Comment;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.exception.CustomException;
import com.sparta.springboards.repository.BoardRepository;
import com.sparta.springboards.repository.CommentLikeRepository;
import com.sparta.springboards.repository.CommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.sparta.springboards.exception.ErrorCode.*;

@Service
@RequiredArgsConstructor

public class CommentService {
    private final CommentRepository commentRepository;
    private final BoardRepository boardRepository;
    private final CommentLikeRepository commentLikeRepository;

//댓글 작성
    @Transactional
    public CommentResponseDto createComment(Long id, CommentRequestDto commentRequestDto, User user) {
        Board board = boardRepository.findById(id).orElseThrow(
                () -> new CustomException(NOT_FOUND_BOARD)
        );

        Comment comment = commentRepository.save(new Comment(commentRequestDto, board, user));
        //댓글 생성시, 좋아요를 0으로 초기화시켜주기 위함
        int cnt = 0;
        return new CommentResponseDto(comment,cnt);
    }

//댓글 수정
    @Transactional
    public CommentResponseDto updateComment(Long boardId, Long cmtId, CommentRequestDto commentRequestDto, User user) {
        //DB 에 게시글 저장 확인
        Board board = boardRepository.findById(boardId).orElseThrow (
                () -> new CustomException(NOT_FOUND_BOARD)
        );

        Comment comment;

        if (user.getRole().equals(UserRoleEnum.ADMIN)) {
            comment = commentRepository.findById(cmtId).orElseThrow(
                    () -> new CustomException(NOT_FOUND_COMMENT)
            );

        } else {
            comment = commentRepository.findByIdAndUserId(cmtId, user.getId()).orElseThrow(
                    () -> new CustomException(AUTHORIZATION)
            );
        }

        comment.update(commentRequestDto);

        return new CommentResponseDto(comment ,commentLikeRepository.countAllByCommentId(comment.getId()));
    }

//댓글 삭제
    @Transactional
    public CommentResponseDto deleteComment(Long boardId, Long cmtId, User user) {
        //DB 에 게시글 저장 확인
        Board board = boardRepository.findById(boardId).orElseThrow (
                () -> new CustomException(NOT_FOUND_BOARD)
        );
        Comment comment;
        //ADMIN 권한을 가진 user 이라면
        if (user.getRole().equals(UserRoleEnum.ADMIN)) {
            //DB 에서 댓글을 찾아봄
            comment = commentRepository.findById(cmtId).orElseThrow(
                    () -> new CustomException(NOT_FOUND_COMMENT)
            );
        //ADMIN 권한을 가진 user 가 아니라면
        } else {
            //DB 에서 댓글을 찾아봄
            comment = commentRepository.findByIdAndUserId(cmtId, user.getId()).orElseThrow(
                    () -> new CustomException(AUTHORIZATION)
            );
        }
        //해당 댓글 삭제
        commentRepository.deleteById(cmtId);

        return new CommentResponseDto(comment,commentLikeRepository.countAllByCommentId(comment.getId()));
    }

//댓글 좋아요
    @Transactional
    public MsgResponseDto CommentLike(Long cmtId, User user) {
        //DB 에서 댓글을 찾아봄
        Comment comment = commentRepository.findById(cmtId).orElseThrow(
                () -> new CustomException(NOT_FOUND_COMMENT)
        );
        //DB 에 해당 usr 가 '댓글 좋아요' 누른 적이 없다면, '댓글 좋아요' 를 추가하기
        if (commentLikeRepository.findByCommentIdAndUserId(cmtId, user.getId()).isEmpty()){
            CommentLike commentLike = CommentLike.builder()
                    .comment(comment)
                    .user(user)
                    .build();
            commentLikeRepository.save(commentLike);
            return new MsgResponseDto("좋아요 완료", HttpStatus.OK.value());
        //DB 에 해당 usr 가 '댓글 좋아요' 누른 적이 있다면, '댓글 좋아요' 를 제거하기
        }else {
            commentLikeRepository.deleteByCommentIdAndUserId(comment.getId(), user.getId());
            return new MsgResponseDto("좋아요 취소", HttpStatus.OK.value());
        }
    }
}

UserService

package com.sparta.springboards.service;

import com.sparta.springboards.dto.LoginRequestDto;
import com.sparta.springboards.dto.SignupRequestDto;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.exception.CustomException;
import com.sparta.springboards.jwt.JwtUtil;
import com.sparta.springboards.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.servlet.http.HttpServletResponse;
import java.util.Optional;

import static com.sparta.springboards.exception.ErrorCode.*;

@Service

//다른 계층에서 파라미터를 검증하기 위해서
@Validated          //추가

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

public class UserService {

//회원가입 구현
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;  //의존성 주입(DI) --> jwtUtil.class 에서 @Component 로 빈을 등록했기때문에 가능
    private final PasswordEncoder passwordEncoder;
    private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

    @Transactional
    //SignupRequestDto 에서 가져온 username, password 를 확인
    public void signup(SignupRequestDto signupRequestDto) {
        String username = signupRequestDto.getUsername();
        String password = passwordEncoder.encode(signupRequestDto.getPassword());   //저장하기 전에 password 를 Encoder 한다

    // 회원 중복 확인
        //userRepository 에서 username 으로 실제 유저가 있는지 없는지 확인
        Optional<User> found = userRepository.findByUsername(username);
        //중복된 유저가 존재한다면,
        if (found.isPresent()) {
            throw new CustomException(DUPLICATED_USERNAME);
        }

    // 사용자 ROLE(권한) 확인
        UserRoleEnum role = UserRoleEnum.USER;
        //SignupRequestDto 에서 admin 이 true 라면(admin 으로 로그인을 시도하려고 하는구나),
        if (signupRequestDto.isAdmin()) {
            //들어온 Token 값과 위의 검증을 위한 Token 값(위쪽에 AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC)이 일치하는지 확인
            if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
                //일치하지 않으면(매개변수가 의도치 않는 상황 유발시), 해당 메시지를 보낸다
                throw new CustomException(INVALID_AUTH_TOKEN);
            }
            //일치하면, user 를 admin 타입으로 바꾼다
            role = UserRoleEnum.ADMIN;
        }

        //가져온 username, password, role(UserRoleEnum)를 넣어서 저장(save)
        User user = new User(username, password, role);
        userRepository.save(user);
    }

//로그인 구현
    @Transactional(readOnly = true)
    //로그인이 되면, LoginRequestDto 로 username, password 가 넘어온다
    public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
        String username = loginRequestDto.getUsername();
        String password = loginRequestDto.getPassword();

    //사용자 확인
        //username 을 통해 확인해서, 있다면 User 객체에 담긴다
        User user = userRepository.findByUsername(username).orElseThrow(
                () -> new CustomException(NOT_FOUND_USER)
        );
    //비밀번호 확인
        //User 객체에 들어있던 Password 와 가지고 온 Password(String password = loginRequestDto.getPassword()에 있는) 가 일치하는지 확인
        if(!passwordEncoder.matches(password, user.getPassword())) {
            throw new CustomException(NOT_MATCH_INFORMATION);
        }

        //response 에 addHeader() 를 사용해서, Header 쪽에 값을 넣는다. (위의 User 객체에서 유저 정보를 가져왔기 떄문에 사용 가능한 것)
            //AUTHORIZATION_HEADER: KEY 값
            //jwtUtil: 이것을 사용하기 위해, 위에서 의존성 주입을 해줘야 함
            //createToken(user.getUsername(), user.getRole()): Username(이름), Role(권한)을 넣어서 토큰을 만든다
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername(), user.getRole()));
    }

//회원 탈퇴
    @Transactional
    public void signOut(LoginRequestDto loginRequestDto, User user){
        //Username 의 문자열의 길이가 0 이라면,
            //isEmpty(): 문자열의 길이가 0인 경우에, true 를 리턴. 비어있지 않으면 false 를 리턴
        if (userRepository.findByUsername(loginRequestDto.getUsername()).isEmpty()) {
            throw new CustomException(NOT_FOUND_USER);
        //Username 의 문자열의 길이가 0 이 아니라면,
        } else {
            //password 의 일치여부를 비교해서, 일치하지 않다면
            if(!passwordEncoder.matches(loginRequestDto.getPassword(), user.getPassword())) {
                throw new CustomException(NOT_FOUND_USER);
            //password 의 일치여부를 비교해서, 일치하다면
            } else {
                //loginRequestDto 에서 입력한 Username 을 deleteByUsername() 메소드를 써서, userRepository 로 보낸다.
                userRepository.deleteByUsername(loginRequestDto.getUsername());
            }
        }
    }
}

SpringboardsApplication

package com.sparta.springboards;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class SpringboardsApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringboardsApplication.class, args);
	}
}

application.properties

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=

spring.thymeleaf.cache=false

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.6'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.sparta'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	implementation group: 'org.json', name: 'json', version: '20220924'

	compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

	implementation 'org.springframework.boot:spring-boot-starter-validation'    //의존성을 위해 추가

}

tasks.named('test') {
	useJUnitPlatform()		//JUnit: 자바 프로그래밍 언어용 단위 테스트 프레임워크 --> JUnit 을 이용한 단위 테스트 가능
}
profile
개발자로 거듭나기!

0개의 댓글