본문 바로가기
Spring

Spring Project Board 2 (Repository, DTO, PageController) - 타임리프 SQL 출력

by code2772 2023. 1. 8.

[ 목차 ]

    728x90
    반응형
    MockMvc
    웹 어플리케이션을 어플리케이션 서버에 배포하지 않고 테스트용 MVC 환경을 만들어 요청 및 전송, 응답기능을
    제공하는 유틸리티 클래스
    
    Query DSL
    - JPA를 좀 더 효율적으로 사용할 수 있는 라이브러리
    - 오픈소스 프로젝트로 JPQL을 Java 코드로 작성할 수 있도록 함
    - 정적 타입을 이용해서 SQL과 같은 쿼리를 생성해줌
    
    @ConfigurationPropertiesScan
    - @EnableConfigurationProperties를 이용해 설정 프로퍼티 클래스를 사용하는 경우 클래스가
        많아지면 코드가 무거워지고 복잡해짐

     

    ✔ Repository

    @RepositoryRestResource//내가 설정한 REST 기능을 부여함(기본적인 API 자동 생성)
    public interface ArticleRepository extends
    
            JpaRepository<Article, Long>,
            QuerydslPredicateExecutor<Article>, // like검색이 아닌 전체단어 일치 검색
    
    // 쿼리에 적용하고자 하는 엔티티, 기본기능 중 완벽하게 동일한 것만 구분 가능(like는 아님)
            QuerydslBinderCustomizer<QArticle> { // like검색을 위해 오버라이드 필요함
    
           //pageable : 한번에 가저올 수를 의미한다.
           Page<Article> findByTitleContaining(String title, Pageable pageable);
           //findByTitleContaining : 정화한 타이틀을 찾는것이 아닌 일부만으로 찾을 수 있다.
           Page<Article> findByContentContaining(String title, Pageable pageable);
           Page<Article> findByUserAccount_UserIdContaining(String userId, Pageable pageable);
           // UserAccount와 같은 객체를 찾을 경우에는 _ Userid를 이용하여 찾는다.
           Page<Article> findByUserAccount_NickNameContaining(String nickname, Pageable pageable);
           Page<Article> findByHashtag(String hashtag, Pageable pageable);
    
    
    
           @Override
           default void customize(QuerydslBindings bindings, QArticle root){   //추상메소드 안에 적는 법 =default
                  bindings.excludeUnlistedProperties(true);
                  bindings.including(root.title, root.content, root.hashtag  ,root.createdAt, root.createdBy);
                  bindings.bind(root.title).first(StringExpression::containsIgnoreCase);  //like 검색이 된다.
                  bindings.bind(root.content).first(StringExpression::containsIgnoreCase);
                  bindings.bind(root.hashtag).first(StringExpression::containsIgnoreCase);
                  bindings.bind(root.createdAt).first(DateTimeExpression::eq);
                  bindings.bind(root.createdBy).first(StringExpression::containsIgnoreCase);
    
           }
    }

     

    findByTitleContaining : 타이틀 명칭을 찾을 때 토시하나 정확하게 입력하는 것이 아닌 해당 단어가 포함돼어 있으면 찾아주는 기능 - Containing

    // 해당 관련된 엔티티가 아닌 다른 엔티티에서 찾는다면 findByUserAccount_userIdContaining 과 같이 언더바(_)를 사용한다.

     

    ✔ Dto

    
    public record ArticleDto(
            Long id,
            UserAccountDto userAccountDto,
            String title,
            String content,
            String hashtag,
            String createdBy,
            LocalDateTime createdAt,
            String modifiedBy,
            LocalDateTime modifiedAt
    )
    {
        public static ArticleDto of(  Long id,  UserAccountDto userAccountDto
                                    ,String title, String content,
                                      String hashtag, String createdBy,
                                      LocalDateTime createdAt, String modifiedBy, LocalDateTime modifiedAt) {
    
            return new ArticleDto( id, userAccountDto, title,  content,
                    hashtag,  createdBy,  createdAt, modifiedBy, modifiedAt);
        }
        public static  ArticleDto from(Article entity){
            return new ArticleDto(
                    entity.getId(),
                    UserAccountDto.from(entity.getUserAccount()),//   getUserAccountd에서 가저와서  UserAccountDto 적용
                    entity.getTitle(),
                    entity.getContent(),
                    entity.getHashtag(),
                    entity.getCreatedBy(),
                    entity.getCreatedAt(),
                    entity.getModifiedBy(),
                    entity.getModifiedAt()
                    );
        }
    
        public Article toEntity(){
            return  Article.of(
                    userAccountDto.toEntity(),
                    title,
                    content,
                    hashtag
            );
    
        }
    }
    
    레코드(record)
    - 클래스의 특별한 종류
    - DTO를 편리하게 생성하기 위한 클래스
    - 자바 16부터 공식 기능이 되었음
    - 열거타입과 마찬가지로 자바 클래스의 종류
    - 다른 클래스를 상속받을 수 없음
    - abstract로 선언할 수 없고 암시적으로 final로 선언됨
    - 본문(body)에는 정적필드, 정적 메서드, 정적 이니셜라이저, 생성자, 인스턴스 메서드
    증첩타입(클래스, 인터페이스, 열거형등)을 선언할 수 있음

    ✔ PageController

    @RequiredArgsConstructor// 생성자 추가시 작성
    @RequestMapping("articles")
    @Controller
    public class ArticleController {
    
        private final ArticleService articleService;// 생성자
    
        @GetMapping
        public String articles(
                @RequestParam(required =false)SearchType searchType,
                @RequestParam(required =false)String searchValue,
                @PageableDefault(size = 10, sort = "createdAt",direction = Sort.Direction.DESC)Pageable pageable, ModelMap map
                ){
    
            map.addAttribute("articles", articleService
                    .searchArticles(searchType,searchValue,pageable).map(ArticleResponse::from));
            return "articles/index"; // articles의 index 전달
        }
    
        @GetMapping("/{articleId}")
        public String article(@PathVariable Long articleId, ModelMap map){
            ArticleWithCommentResponse article = ArticleWithCommentResponse.from(articleService.getArticle(articleId));
            map.addAttribute("article", article);
            map.addAttribute("articleComments", List.of());
            return "articles/detail";
        }
    }

    ✔ Config

    package com.koreait.projectboard.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    
    // 직접 빈 설정파일에 빈을 등록하는 방법
    @Configuration//컨피그로 적용을 하기 위해 사용, 이를 사용하기 위해서는 메인에서 @ConfigurationPropertiesScan 필요
    // 위 페이지는 사용자가 직접 만드는 페이지가 아니기 때문에 괜찮음
    // @Bean 을 수동으로 등록하고 있는 모습이다.
    public class SecurityConfig {
        @Bean // 하단 부 빈 주입하는 방법(SecurityConfig 설정 주입) -> 해당 사용자는 접근할 수 없는 보안 기능
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            return http
                    .authorizeRequests(auth -> auth.anyRequest().permitAll())
                    .formLogin().and()
                    .build();
        }
    }
    
    
    // @Bean : 오래된 방식의 java 오브젝트, 속성으로는 class, id, scope, constructor-arg 가 있으며 ID 컨테이너에 의해 생성 및 관리된다.
    // 해당 클래스를 직접 만드는 것이 아닌 가져다 쓰는 클래스인 경우에는 @Bean을 등록해 줘야한다. (+ 공유 기능, + 유지보수 용이)
    // 자바에서는 new 연산자를 사용하여 객체를 생성하였다면, Spring 에서는 이를 Bean이라고 한다.
    // Bean은 Spring의 ApplicationContext 가 담고있는 객체라고 생각하면 편하겠다.
    
    // 자동 등록 방법
    //@ComponentScan 어노테이션은 어느 지점부터 컴포넌트를 찾으라고 알려주는 역할
    // @Component 어노테이션이 있는 클래스들을 찾아서 자동으로 빈 등록 (자동 등록)

     

    ✔ Thymleaf

    ❗ HTML

    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="Ryuzy">
        <title>게시판 페이지</title>
    
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
        <link href="/css/search-bar.css" rel="stylesheet">
    </head>
    
    <body>
    
    <header id="header">
    헤더 삽입
    <hr>
    </header>
    <main class="container">
    
        <div class="row">
            <div class="card card-margin search-form">
                <div class="card-body p-0">
                    <form id="card search-form">
                        <div class="row">
                            <div class="col-12">
                                <div class="row no-gutters">
                                    <div class="col-lg-3 col-md-3 col-sm-12 p-0">
                                        <label for="search-type" hidden>검색 유형</label>
                                        <select class="form-control" id="search-type">
                                            <option>제목</option>
                                            <option>본문</option>
                                            <option>id</option>
                                            <option>닉네임</option>
                                            <option>해시태그</option>
                                        </select>
                                    </div>
                                    <div class="col-lg-8 col-md-6 col-sm-12 p-0">
                                        <label for="search-value" hidden>검색어</label>
                                        <input type="text" placeholder="검색어..." class="form-control" id="search-value" name="search-value">
                                    </div>
                                    <div class="col-lg-1 col-md-3 col-sm-12 p-0">
                                        <button type="submit" class="btn btn-base">
                                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
                                                <circle cx="11" cy="11" r="8"></circle>
                                                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
                                            </svg>
                                        </button>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    
        <table class="table" id="article-table">
            <thead>
            <tr>
                <th class="title col-6">제목</th>
                <th class="hashtag col-2">해시태그</th>
                <th class="user-id col">작성자</th>
                <th class="created-at col">작성일</th>
            </tr>
            </thead>
            <tbody>
            <tr>
                <td class ="title"><a>첫글</a></td>
                <td class = "hashtag">#java</td>
                <td calss = "user-id">김사과</td>
                <td class = "created-at"><time>2023-01-03</time></td>
            </tr>
            <tr>
                <td>두번째글</td>
                <td>#spring</td>
                <td>김사과</td>
                <td>2023-01-03</td>
            </tr>
            <tr>
                <td>세번째글</td>
                <td>#java</td>
                <td>김사과</td>
                <td>2023-01-03</td>
            </tr>
            </tbody>
        </table>
        <nav aria-label="Page navigation example">
            <ul class="pagination justify-content-center">
                <li class="page-item"><a class="page-link" href="#">Previous</a></li>
                <li class="page-item"><a class="page-link" href="#">1</a></li>
                <li class="page-item"><a class="page-link" href="#">2</a></li>
                <li class="page-item"><a class="page-link" href="#">3</a></li>
                <li class="page-item"><a class="page-link" href="#">Next</a></li>
            </ul>
        </nav>
    </main>
    
    <footer id="footer">
        푸터 삽입
        <hr>
    </footer>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
    </body>
    </html>

    ❗ xml

    <?xml version="1.0"?>
    <thlogic>
        <attr sel="#header" th:replace="header :: header"/>
        <attr sel="#footer" th:replace="footer :: footer"/>
    
        <attr sel="#article-table">
            <attr sel="tbody" th:remove="all-but-first">
                <attr sel="tr[0]" th:each="article : ${articles}">
                    <attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/'+${article.id}}"/>
                    <attr sel="td.hashtag" th:text="${article.hashtag}"/>
                    <attr sel="td.user-id" th:text="${article.nickname}"/>
                    <attr sel="td.created-at/time" th:datetime="${article.createdAt}" th:text="${#temporals.format(article.createdAt, 'yyyy-MM-dd')}"/>
    <!--temporals 시간포멧 설정 하는 기능-->
                </attr>
            </attr>
        </attr>
    </thlogic>

    ❗ 출력화면 - SQL 데이터를 활용하여 출력

    반응형