Spring

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

code2772 2023. 1. 8. 10:20
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 데이터를 활용하여 출력

반응형