전체 스택 Reddit 클론 - 스프링 부트, React, Electron 애플리케이션 - 섹션 9

전체 스택 Reddit 클론 - 스프링 부트, React, Electron 애플리케이션 - 섹션 9


소개하다.


Spring Boot을 사용하여 Reddit 클론을 생성하는 섹션 9에 오신 것을 환영합니다.
우리는 이 부분에 무엇을 건설합니까?
  • 페이지별 지원
  • 페이지 나누기를 지원하기 위해 백엔드를 업데이트하여 데이터베이스가 확장되기 시작할 때 클라이언트의 마운트 시간을 줄일 것
  • JWT 실효
  • JWT 갱신
  • 에서 댓글을 만들고 읽는 데 사용할 창설과 읽기 단점을 추가했습니다!!

    중요한 부분

  • 후면 소스: https://github.com/MaxiCB/vox-nobis/tree/master/backend
  • 프런트엔드 소스: https://github.com/MaxiCB/vox-nobis/tree/master/client
  • 실시간 URL: 진행 중
  • 섹션 1: 저장소 업데이트🗄


    페이지 나누기와 정렬 지원을 위해 모든 저장소를 업데이트하는 것을 소개합니다.내부 통신.니 이름.백엔드.다음 클래스를 업데이트할 것입니다.
  • Comment Respository: 기존의 논리를 변환하고 목록으로 되돌아오는 findAllByPost 방법을 추가할 것입니다. 왜냐하면 이 방법에 의존하여 Post Service의 댓글 총량을 돌려보내기 때문입니다.
  • package com.maxicb.backend.repository;
    
    import com.maxicb.backend.model.Comment;
    import com.maxicb.backend.model.Post;
    import com.maxicb.backend.model.User;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.repository.PagingAndSortingRepository;
    
    import java.util.List;
    
    public interface CommentRepository extends PagingAndSortingRepository<Comment, Long> {
        Page<Comment> findByPost(Post post, Pageable pageable);
        List<Comment> findAllByPost(Post post);
        Page<Comment> findAllByUser(User user, Pageable pageable);
    }
    
  • PostRepository:
  • package com.maxicb.backend.repository;
    
    import com.maxicb.backend.model.Post;
    import com.maxicb.backend.model.Subreddit;
    import com.maxicb.backend.model.User;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.repository.CrudRepository;
    import org.springframework.data.repository.PagingAndSortingRepository;
    
    import java.util.List;
    
    public interface PostRepository extends PagingAndSortingRepository<Post, Long> {
        Page<Post> findAllBySubreddit(Subreddit subreddit, Pageable pageable);
        Page<Post> findByUser(User user, Pageable pageable);
    }
    
  • SubredditRepository:
  • package com.maxicb.backend.repository;
    
    import com.maxicb.backend.model.Subreddit;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.repository.PagingAndSortingRepository;
    
    import java.util.Optional;
    
    public interface SubredditRepository extends PagingAndSortingRepository<Subreddit, Long> {
        Optional<Subreddit> findByName(String subredditName);
        Optional<Page<Subreddit>> findByNameLike(String subredditName, Pageable pageable);
    }
    

    섹션 2: 서비스 업데이트🌎


    현재 우리는 이미 우리의 저장소를 갱신하였으며, 이러한 변화를 반영하기 위해 우리의 서비스를 갱신해야 한다.내부 통신.니 이름.백엔드.서비스는 다음 과정을 업데이트합니다.이 절에 전체 클래스를 표시하지 않고 업데이트할 특정한 방법만 표시한다는 것을 기억하십시오.
  • Comment Service: GetCommentsForPost와 GetCommentsForUser 메서드를 업데이트하여 페이지 나누기를 올바르게 처리합니다.
  •     public Page<CommentResponse> getCommentsForPost(Long id, Integer page) {
            Post post = postRepository.findById(id)
                    .orElseThrow(() -> new PostNotFoundException("Post not found with id: " + id));
            return commentRepository.findByPost(post, PageRequest.of(page, 100)).map(this::mapToResponse);
        }
    
        public Page<CommentResponse> getCommentsForUser(Long id, Integer page) {
            User user = userRepository.findById(id)
                    .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
            return commentRepository.findAllByUser(user, PageRequest.of(page, 100)).map(this::mapToResponse);
        }
    
  • PostService: 맵ToResponse와 getAllPosts와 getPostsBySubreddit와 getPostsByUsername 방법을 업데이트하여 페이지를 나누고 DTO에 비치는 기존 논리를 보존합니다
  •     private PostResponse mapToResponse(Post post) {
            return PostResponse.builder()
                    .postId(post.getPostId())
                    .postTitle(post.getPostTitle())
                    .url(post.getUrl())
                    .description(post.getDescription())
                    .userName(post.getUser().getUsername())
                    .subredditName(post.getSubreddit().getName())
                    .voteCount(post.getVoteCount())
                    .commentCount(commentRepository.findAllByPost(post).size())
                    .duration(TimeAgo.using(post.getCreationDate().toEpochMilli()))
                    .upVote(checkVoteType(post, VoteType.UPVOTE))
                    .downVote(checkVoteType(post, VoteType.DOWNVOTE))
                    .build();
        }
    
        public Page<PostResponse> getAllPost(Integer page) {
            return postRepository.findAll(PageRequest.of(page, 100)).map(this::mapToResponse);
        }
    
        public Page<PostResponse> getPostsBySubreddit(Integer page, Long id) {
            Subreddit subreddit = subredditRepository.findById(id)
                    .orElseThrow(() -> new SubredditNotFoundException("Subreddit not found with id: " + id));
            return postRepository
                    .findAllBySubreddit(subreddit, PageRequest.of(page, 100))
                    .map(this::mapToResponse);
        }
    
        public Page<PostResponse> getPostsByUsername(String username, Integer page) {
            User user = userRepository.findByUsername(username)
                    .orElseThrow(() -> new UserNotFoundException("User not found with username: " + username));
            return postRepository
                    .findByUser(user, PageRequest.of(page, 100))
                    .map(this::mapToResponse);
        }
    
  • SubredditService: getAll 메서드를 업데이트합니다.
  •     @Transactional(readOnly = true)
        public Page<SubredditDTO> getAll(Integer page) {
            return subredditRepository.findAll(PageRequest.of(page, 100))
                    .map(this::mapToDTO);
        }
    

    섹션 3: 컨트롤러 업데이트


    현재 서비스와 저장소를 업데이트했습니다. 클라이언트가 페이지를 사용할 수 있도록 컨트롤러를 업데이트해야 합니다.내부 통신.니 이름.백엔드.컨트롤러는 다음과 같은 종류를 업데이트할 것입니다.이 절에 전체 클래스를 표시하지 않고 업데이트할 특정한 방법만 표시한다는 것을 기억하십시오.
  • CommentController: 페이지 나누기를 올바르게 처리하기 위해 getCommentsByPost와 getCommentsByUser를 업데이트합니다.
  •     @GetMapping("/post/{id}")
        public ResponseEntity<Page<CommentResponse>> getCommentsByPost(@PathVariable("id") Long id, @RequestParam Optional<Integer> page) {
            return new ResponseEntity<>(commentService.getCommentsForPost(id, page.orElse(0)), HttpStatus.OK);
        }
    
        @GetMapping("/user/{id}")
        public ResponseEntity<Page<CommentResponse>> getCommentsByUser(@PathVariable("id") Long id,@RequestParam Optional<Integer> page) {
            return new ResponseEntity<>(commentService.getCommentsForUser(id, page.orElse(0)), HttpStatus.OK);
        }
    
  • PostController: 생성된 게시물을 클라이언트에게 보낼 수 있도록ddPost 방법을 먼저 업데이트하고 getAllPost와 getPostsBySubreddit와 GetPostsBySerName 방법으로 페이지 나누기를 실현합니다
  •     @PostMapping
        public ResponseEntity<PostResponse> addPost(@RequestBody PostRequest postRequest) {
            return new ResponseEntity<>(postService.save(postRequest), HttpStatus.CREATED);
        }
    
        @GetMapping
        public ResponseEntity<Page<PostResponse>> getAllPost(@RequestParam Optional<Integer> page) {
            return new ResponseEntity<>(postService.getAllPost(page.orElse(0)), HttpStatus.OK);
        }
    
        @GetMapping("/sub/{id}")
        public ResponseEntity<Page<PostResponse>> getPostsBySubreddit(@PathVariable Long id, @RequestParam Optional<Integer> page) {
            return new ResponseEntity<>(postService.getPostsBySubreddit(page.orElse(0), id), HttpStatus.OK);
        }
    
        @GetMapping("/user/{name}")
        public ResponseEntity<Page<PostResponse>> getPostsByUsername(@PathVariable("name") String username, @RequestParam Optional<Integer> page) {
            return new ResponseEntity<>(postService.getPostsByUsername(username, page.orElse(0)), HttpStatus.OK);
        }
    
  • Subreddit Controller: 응답 전송 및 페이지 나누기 지원을 위한 모든 방법을 업데이트합니다.
  •     @GetMapping("/{page}")
        public ResponseEntity<Page<SubredditDTO>> getAllSubreddits (@PathVariable("page") Integer page) {
            return new ResponseEntity<>(subredditService.getAll(page), HttpStatus.OK);
        }
    
        @GetMapping("/sub/{id}")
        public ResponseEntity<SubredditDTO> getSubreddit(@PathVariable("id") Long id) {
            return new ResponseEntity<>(subredditService.getSubreddit(id), HttpStatus.OK);
        }
    
        @PostMapping
        public ResponseEntity<SubredditDTO> addSubreddit(@RequestBody @Valid SubredditDTO subredditDTO) throws Exception{
            try {
                return new ResponseEntity<>(subredditService.save(subredditDTO), HttpStatus.OK);
            } catch (Exception e) {
                throw new Exception("Error Creating Subreddit");
            }
        }
    

    현재, 우리 응용 프로그램은 모든 자원의 페이지를 완전히 지원합니다. 이 자원들은 증가할 수 있고, 전방 응용 프로그램의 불러오는 시간이 느려질 수 있습니다.


    섹션 5: 토큰류 리셋⏳


    현재 우리는 우리의 RefreshToken 클래스를 만들어야 한다. 이 클래스는 ID, token과 그와 관련된creationDate를 만들어서 일정 시간 후에 표시를 무효화시킬 수 있도록 해야 한다.
  • 영패 리셋:
  • package com.maxicb.backend.model;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import java.time.Instant;
    
    @Data
    @Entity
    @AllArgsConstructor
    @NoArgsConstructor
    public class RefreshToken {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String token;
        private Instant creationDate;
    }
    

    섹션 5: 토큰 서비스 및 DTO 업데이트🌎


    이제 RefreshToken이 생겼으니 우리는 모든 것을 준비하고 우리의 신분 검증 시스템을 업데이트하기 시작할 것이다.프로젝트 내부에서 다음과 같은 종류를 추가하고 업데이트할 것입니다.
  • RefreshTokenRepository:
  • package com.maxicb.backend.repository;
    
    import com.maxicb.backend.model.RefreshToken;
    import org.springframework.data.repository.PagingAndSortingRepository;
    
    import java.util.Optional;
    
    public interface RefreshTokenRepository extends PagingAndSortingRepository<RefreshToken, Long> {
        Optional<RefreshToken> findByToken(String token);
    
        void deleteByToken(String token);
    }
    
  • RefreshTokenService: 이 서비스는 저희가 영패를 생성하고 검증하며 영패를 삭제할 수 있도록 합니다.
  • package com.maxicb.backend.service;
    
    import com.maxicb.backend.exception.VoxNobisException;
    import com.maxicb.backend.model.RefreshToken;
    import com.maxicb.backend.repository.RefreshTokenRepository;
    import lombok.AllArgsConstructor;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.time.Instant;
    import java.util.UUID;
    
    @Service
    @AllArgsConstructor
    @Transactional
    public class RefreshTokenService {
        private RefreshTokenRepository refreshTokenRepository;
    
        RefreshToken generateRefreshToken () {
            RefreshToken refreshToken = new RefreshToken();
            refreshToken.setToken(UUID.randomUUID().toString());
            refreshToken.setCreationDate(Instant.now());
            return refreshTokenRepository.save(refreshToken);
        }
    
        void validateToken(String token) {
            refreshTokenRepository.findByToken(token)
                    .orElseThrow(() -> new VoxNobisException("Invalid Refresh Token"));
        }
    
        public void deleteRefreshToken(String token) {
            refreshTokenRepository.deleteByToken(token);
        }
    }
    
  • 업데이트된 AuthResponse: 새로 생성된 영패를 포함하기 위해 AuthResponse를 업데이트합니다.
  • import lombok.AllArgsConstructor;
    import lombok.Data;
    
    import java.time.Instant;
    
    @Data
    @AllArgsConstructor
    public class AuthResponse {
            private String authenticationToken;
            private String refreshToken;
            private Instant expiresAt;
            private String username;
    }
    
  • RefreshTokenRequest: 이 DTO는 영패가 시스템에서 만료되기 전에 클라이언트로부터 영패 갱신 요청을 처리합니다
  • package com.maxicb.backend.dto;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import javax.validation.constraints.NotBlank;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class RefreshTokenRequest {
        @NotBlank
        private String refreshToken;
        private String username;
    }
    

    섹션 6: JWTprovider 업데이트🔏


    이제 모든 것이 준비되었으니 JWT 시스템 업데이트를 시작하겠습니다.내부 통신.니 이름.백엔드.서비스는 다음 과정을 업데이트합니다.이 절에 전체 클래스를 표시하지 않고 업데이트할 특정한 방법만 표시한다는 것을 기억하십시오.
  • JWTprovider: issuedAt 날짜를 포함하여 JWT 구현을 업데이트하고 새 영패를 만들 때 만료 날짜를 설정합니다.
  • @Service
    public class JWTProvider {
        private KeyStore keystore;
        @Value("${jwt.expiration.time}")
        private Long jwtExpirationMillis;
    
        ...
        ....
        public String generateToken(Authentication authentication) {
            org.springframework.security.core.userdetails.User princ = (User) authentication.getPrincipal();
            return Jwts.builder()
                    .setSubject(princ.getUsername())
                    .setIssuedAt(from(Instant.now()))
                    .signWith(getPrivKey())
                    .setExpiration(from(Instant.now().plusMillis(jwtExpirationMillis)))
                    .compact();
        }
    
        public String generateTokenWithUsername(String username) {
            return Jwts.builder()
                    .setSubject(username)
                    .setIssuedAt(from(Instant.now()))
                    .signWith(getPrivKey())
                    .setExpiration(from(Instant.now().plusMillis(jwtExpirationMillis)))
                    .compact();
        }
        ....
        ...
        public Long getJwtExpirationMillis() {
            return jwtExpirationMillis;
        }
    

    섹션 7: 업데이트된 인증💂‍♀️


    현재 우리는 페이지 나누기를 실현했고, 우리는 우리의 신분 검증 시스템을 업데이트하기 시작할 것이다.Google 프로젝트에서 다음과 같은 종류를 업데이트할 것입니다.이 절에 전체 클래스를 표시하지 않고 업데이트할 특정한 방법만 표시한다는 것을 기억하십시오.
  • AuthService: AuthService를 업데이트하여 업데이트 토큰을 보내고 기존 토큰을 업데이트하는 논리를 추가할 것입니다.
  • public AuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest) {
            refreshTokenService.validateToken(refreshTokenRequest.getRefreshToken());
            String token = jwtProvider.generateTokenWithUsername(refreshTokenRequest.getUsername());
            return new AuthResponse(token, refreshTokenService.generateRefreshToken().getToken(), Instant.now().plusMillis(jwtProvider.getJwtExpirationMillis()), refreshTokenRequest.getUsername());
        }
    
    public AuthResponse login (LoginRequest loginRequest) {
            Authentication authenticate = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            loginRequest.getUsername(), loginRequest.getPassword()));
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            String authToken = jwtProvider.generateToken(authenticate);
            String refreshToken = refreshTokenService.generateRefreshToken().getToken();
            return new AuthResponse(authToken, refreshToken, Instant.now().plusMillis(jwtProvider.getJwtExpirationMillis()), loginRequest.getUsername());
        }
    
  • AuthController: 클라이언트가 새로 추가한 논리를 사용할 수 있도록 새로운 노드를 구현합니다.
  • @PostMapping("/refresh/token")
        public AuthResponse refreshToken(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) {
            return authService.refreshToken(refreshTokenRequest);
        }
    
        @PostMapping("/logout")
        public ResponseEntity<String> logout(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) {
            refreshTokenService.deleteRefreshToken(refreshTokenRequest.getRefreshToken());
            return ResponseEntity.status(HttpStatus.OK).body("Refresh Token Deleted");
        }
    

    섹션 8: 사용자 정의 예외🚫

  • VoxNobiseException: 일반적인 사용자 정의 이상을 만들 것입니다. 프로그램을 확장할 때 전체 프로그램에서 다시 사용할 수 있습니다.
  • package com.maxicb.backend.exception;
    
    public class VoxNobisException extends RuntimeException {
        public VoxNobisException(String message) {super(message);}
    }
    

    섹션 9: 업데이트된 응용 프로그램.등록 정보


    우리는 프로그램이 영패를 만들 때 사용하고자 하는 만료 시간을 추가하고 그것들의 만료 날짜를 설정해야 한다.나는 그것을 15분으로 설정하기로 선택했지만, 미래에 지속 시간이 증가할 것이다.
    # JWT Properties
    jwt.expiration.time=900000
    

    섹션 10: Swagger UI 구현📃


    이제 MVP 백엔드의 끝에 이르렀으며 Swagger UI를 추가합니다.이전에 Swagger를 사용한 적이 없는 경우 API에 대한 문서를 자동으로 생성하는 것이 좋습니다.당신은 더 많은 것을 알 수 있습니다here!
  • pom.xml: 프로젝트pom에 흔들림 의존항을 포함해야 합니다.xml 파일.
  •         <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>2.9.2</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>2.9.2</version>
            </dependency>
    
  • 시장 유치 설정: 내부com.니 이름.백엔드.다음 클래스를 만들 것입니다.
  • import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
        @Bean
        public Docket voxNobisAPI() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                    .apiInfo(getAPIInfo());
        }
    
        private ApiInfo getAPIInfo(){
            return new ApiInfoBuilder()
                    .title("Vox-Nobis API")
                    .version("1.0")
                    .description("API for Vox-Nobis reddit clone")
                    .build();
        }
    }
    
  • 백엔드 응용 프로그램: 내장된com.니 이름.백엔드에서 우리는 우리의 흔들림 설정을 주입할 것이다.
  • @SpringBootApplication
    @EnableAsync
    @Import(SwaggerConfig.class)
    public class BackendApplication {
        ...
    }
    
  • 보안: 지금 프로그램을 실행하고 http://localhost:8080/swagger-ui.html#/로 이동하려고 하면 403에서 금지된 오류가 발생할 수 있습니다.내부 통신.니 이름.백엔드.설정은 기존의 매칭기 아래에 다음 매칭기를 추가해서 권한이 부여되지 않은 접근을 허용하는 보안 설정을 업데이트해야 합니다.
  • .antMatchers(HttpMethod.GET, "/api/subreddit")
    .permitAll()
    .antMatchers("/v2/api-docs",
                "/configuration/ui",
                "/swagger-resources/**",
                "/configuration/security",
                "/swagger-ui.html",
                "/webjars/**")
    .permitAll()
    

    결론🔍

  • 모든 구성이 올바른지 확인하기 위해 어플리케이션을 실행하고 콘솔에 오류가 없는지 확인합니다.콘솔 하단에서 아래와 유사한 출력을 볼 수 있을 것 같습니다
  • 만약에 컨트롤러에 오류가 없다면 당신은 정확한 데이터로http://localhost:8080/api/auth/loginpost 요청을 보내서 새로운 논리를 테스트할 수 있습니다. 로그인에 성공하면refreshToken과 사용자 이름을 즉시 받아야 합니다!
  • 내비게이션http://localhost:8080/swagger-ui.html#/을 통해 우리가 만든 모든 단점의 문서와 필요한 정보를 보고 돌아올 수 있습니다.
  • 본문에 페이지와 영패의 만료 시간을 추가했습니다.
  • 다음


    열 번째 부분이 발표될 때, 우리는 응용 프로그램의 앞부분에서 작업을 시작할 것이다.

    좋은 웹페이지 즐겨찾기