90일차 - JPA, React (동적 쿼리, React 라우팅)

Yohan·2024년 7월 2일
0

코딩기록

목록 보기
132/156
post-custom-banner

Querydsl 사용하려면 세팅 필수

package com.spring.jpastudy.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

// QueryDsl 세팅
@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean // 외부라이브러리를 스프링 컨테이너에 관리시키는 설정
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

동적 쿼리

  • BooleanBuilder 객체 필요

    @Test
    @DisplayName("동적 쿼리를 사용한 간단한 아이돌 조회")
    void dynamicTestOne() {
        //given

        String name = "김채원";
        String genderParam = "여";
		//  int minAge = 20;
        Integer minAge = 20; // int는 null체크를 못하니, INTEGER로
        Integer maxAge = 25;

        // 동적 쿼리를 위한 BooleanBuilder
        BooleanBuilder booleanBuilder = new BooleanBuilder();

        if (name != null) {
            booleanBuilder.and(idol.idolName.eq(name));
        }

        if (genderParam != null) {
            booleanBuilder.and(idol.gender.eq(genderParam));
        }
        
        // between 조건
        if (minAge != null) {
            booleanBuilder.and(idol.age.goe(minAge));
        }
        // between 조건
        if (maxAge != null) {
            booleanBuilder.and(idol.age.loe(maxAge));
        }

        //when
        List<Idol> result = factory
                .selectFrom(idol)
                .where(booleanBuilder)
                .fetch();
        //then
        assertFalse(result.isEmpty());
    }

동적 정렬

  • OrderSpecifier 객체 필요

    @Test
    @DisplayName("동적 정렬을 사용한 아이돌 조회")
    void dynamicTest2() {
        //given
        String sortBy = "age"; //나이, 이름, 그룹명
        boolean ascending = false; // 오름차(true), 내림차(false)
        //when

        OrderSpecifier<?> specifier = null;
        // 동적 정렬 조건 생성
        switch (sortBy) {
            case "age":
                specifier = ascending ? idol.age.asc() : idol.age.desc();
                break;
            case "idolName":
                specifier = ascending ? idol.idolName.asc() : idol.idolName.desc();
                break;
            case "groupName":
                specifier = ascending ? idol.group.groupName.asc() : idol.group.groupName.desc();
                break;
        }

        List<Idol> result = factory
                .selectFrom(idol)
                .orderBy(specifier)
                .fetch();
        //then
        assertFalse(result.isEmpty());
    }

jpa를 통해 api 만들어보기

커스텀impl

  • queryDsl을 사용하려면 config 파일에서 등록이 되어있어야 한다.
  • repository
package com.spring.jpastudy.event.repository;


public interface EventRepository
        extends JpaRepository<Event, Long>, EventRepositoryCustom {


}
  • EventRepositoryCustom.interface
package com.spring.jpastudy.event.repository;

public interface EventRepositoryCustom {

    List<Event> findEvents(String sort);

    // join ..

    // groupby ..
}
  • EventRepositoryCustomImpl.java
  • 실질적인 로직은 여기서 구현
package com.spring.jpastudy.event.repository;

import static com.spring.jpastudy.event.entity.QEvent.*;

@Repository
@RequiredArgsConstructor
@Slf4j
public class EventRepositoryCustomImpl implements EventRepositoryCustom {

    private final JPAQueryFactory factory;

    @Override
    public List<Event> findEvents(String sort) {
        return factory
                .selectFrom(event)
                .orderBy(specifier(sort))
                .fetch()
                ;
    }

    // 정렬 조건을 처리하는 메서드
    private OrderSpecifier<?> specifier(String sort) {
        switch (sort) {
            case "date":
                return event.date.desc();
            case "title":
                return event.title.asc();
            default:
                return null;
        }
    }
}
  • 엔터티
package com.spring.jpastudy.event.entity;

@Getter
@ToString
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name="tbl_event")
public class Event {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ev_id")
    private Long id;

    @Column(name = "ev_title", nullable = false, length = 50)
    private String title; // 이벤트 제목

    @Column(name = "ev_desc")
    private String description; // 이벤트 설명

    @Column(name = "ev_image_path")
    private String image; // 이벤트 메인 이미지 경로

    @Column(name = "ev_start_date")
    private LocalDate date; // 이벤트 행사 시작 날짜

    @CreationTimestamp
    private LocalDateTime createdAt; // 이벤트 등록 날짜

}
  • EventService
package com.spring.jpastudy.event.service;


@Service
@RequiredArgsConstructor
@Slf4j
@Transactional // 반드시 붙여야 함
public class EventService {

    private final EventRepository eventRepository;

    // 전체조회 서비스
    public List<Event> getEvents(String sort) {
        return eventRepository.findEvents(sort);
    }

    // 이벤트 등록
    public List<Event> saveEvent(EventSaveDto dto) {
        Event savedEvent = eventRepository.save(dto.toEntity());
        log.info("saved event: {}", savedEvent);
        return getEvents("date");
    }
}
  • EventController (restApi)
package com.spring.jpastudy.event.controller;

import java.util.List;

@RestController
@RequestMapping("/events")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin
public class EventController {

    private final EventService eventService;

    // 전체 조회 요청
    @GetMapping
    public ResponseEntity<?> getList(String sort) {
        List<Event> events = eventService.getEvents(sort);
        return ResponseEntity.ok().body(events);
    }

    // 등록 요청
    @PostMapping
    public ResponseEntity<?> register(@RequestBody EventSaveDto dto) {
        List<Event> events = eventService.saveEvent(dto);
        return ResponseEntity.ok().body(events);
    }
}

React Router

  • 리액트는 html파일이 하나여서, 링크 이동이 불가능
    -> router를 사용해서 새로고침 없이 링크의 전환을 할 수 있다.
    npm install react-router-dom 으로 라이브러리 설치
  • app.js
import React from 'react';
import Home from './components/RouteExample/pages/home';
import Products from './components/RouteExample/pages/Products';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
                          //  RouterProvider는 컴포넌트.

const router = createBrowserRouter([
    // /로 들어오면 뭘 랜더링 할거냐: home
    { path: '/', element: <Home /> },
    // /products로 들어오면 뭘 렌더링할거냐: products
    { path: '/products', element: <Products /> },
]);

const App = () => {

    return (
        <div>
            <RouterProvider router={router} />
        </div>
    );
};

export default App;
  • home.js (components)
  • a태그를 사용해서 링크의 전환을 하면 새로고침이 일어난다.
    그러면 모든 상태값이 초기화 될 수 있어서 Link태그를 사용.
import React from 'react';
import { Link } from "react-router-dom" // 새로고침을 막기 위한 컴포넌트

const Home = () => {

    console.log("home!")

    return (
        <>
            <h1>My Home Page</h1>
            <p>
                {/*<a href='/products'>Products</a>페이지로 이동하기*/}
                <Link to='/products'>Products</Link>페이지로 이동하기
            </p>
        </>
    );
};

export default Home;
  • products.js (components)
import React from 'react';
import { Link } from "react-router-dom" // 새로고침을 막기 위한 컴포넌트

const Products = () => {

    console.log("products!")
    
    return (
        <>
            <h1>My Products Page</h1>
            <p>
                {/*<a href='/'>Home</a>페이지로 이동하기*/}
                <Link to='/'>Home</Link>페이지로 이동하기
            </p>
        </>
)
    ;
};

export default Products;

중첩 router

  • 헤더, 푸터 등 정적인 컴포넌트는 어디 페이지에 가도 고정이 되어야 함
  • 중첩라우터를 활용
  • app.js
import React from 'react';
import Home from './components/RouteExample/pages/home';
import Products from './components/RouteExample/pages/Products';
import RootLayout from './components/RouteExample/layout/RootLayout';
import ErrorPage from './components/RouteExample/pages/ErrorPage'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
                          //  RouterProvider는 컴포넌트.

const router = createBrowserRouter([

    // 중첩 라우터
    {
        path: '/base',
        element: <RootLayout />,
        errorElement: <ErrorPage />, // 에러가 났을 때 보여줄 페이지
        // children이 outlet
        children: [
            { path: '', element: <Home /> }, // 상대경로.
            { path: 'products', element: <Products /> }, // ./가 알아서 붙는다
        ]
    },

]);

const App = () => {

    return (
        <div>
            <RouterProvider router={router} />
        </div>
    );
};

export default App;
  • RootLayout.js
  • 실질적 페이지. 여기서 header, footer, 동적 컴포넌트들을 배치
import React from 'react';
import MainNavigation from './MainNavigation';
import { Outlet } from 'react-router-dom'

const RootLayout = () => {
    return (
        <>
            <MainNavigation/>
            {/* RootLayout의 children들이 Outlet으로 렌더링됨 */}
            <main>
                {/*바뀔 부분만 outlet으로 관리*/}
                <Outlet/>
            </main>
            {/*  footer 등등 정적인것 배치 가능  */}
        </>
    );
};

export default RootLayout;
  • styles.active가 붙으면 현재 있는 페이지에 활성화 표시되는 css가 적용
  • Link태그 말고 NavLink를 import해와서 사용하는데, NavLink를 사용하게되면 className에 함수를 넣을 수 있다.
  • isActive는 내장되어 있는 객체를 디스트럭처링 해서 가져온 것
  • MainNavigation.js
import React from 'react';
import { NavLink } from 'react-router-dom'
import styles from './MainNavigation.module.scss'

const MainNavigation = () => {

    const activeFn = ({ isActive }) => {
        // NavLink 컴포넌트에 className프롭스에 함수를 전달하면
        // 첫번째 파라미터에 어떤 객체 정보를 준다.
        return isActive ? styles.active : undefined;
    }

    return (
        <header className={styles.header}>
            <nav>
                <ul className={styles.list}>
                    <li>
                        {/* NavLink를 쓰면 className에 함수를 넣을 수 있다.  */}
                        <NavLink to={''} className={activeFn} end>Home</NavLink>
                    </li>
                    <li>
                                    {/* 상대경로 */}
                        <NavLink to={'products'}  className={activeFn}>Products</NavLink>
                    </li>
                </ul>
            </nav>
        </header>
    );
};

export default MainNavigation;

상대경로

import React from 'react';
import { Link } from "react-router-dom" // 새로고침을 막기 위한 컴포넌트

const Products = () => {

    console.log("products!")
    
    return (
        <>
            <h1>My Products Page</h1>
            <p>
                    {/*   ..은 상위로 가라 (상대경로)   */}
                <Link to=".." end>Home</Link>페이지로 이동하기
            </p>
        </>
)
    ;
};

export default Products;
import React from 'react';
import { Link } from "react-router-dom" // 새로고침을 막기 위한 컴포넌트

const Home = () => {

    console.log("home!")

    return (
        <>
            <h1>My Home Page</h1>
            <p>
                {/*<a href='/products'>Products</a>페이지로 이동하기*/}
                <Link to='products'>Products</Link>페이지로 이동하기
            </p>
        </>
    );
};

export default Home;

동적 라우팅, 파라미터값 읽기

  • app.js에 products/:prodId ... (상세보기) 경로 추가
children: [
            { index: true, element: <Home /> }, // 상대경로.
            { path: 'products', element: <Products /> }, // ./가 알아서 붙는다
            { path: 'products/:prodId/page/:pageNo', element: <ProductDetail />}
        ]
  • ProductDetail.js
  • react-router-dom에서 useParams import.
import React from 'react';
import { useParams } from 'react-router-dom'

const ProductDetail = () => {

    // 주소에 전달된 파라미터 읽기
    // localhost:3000/base/products/p2/page/10
    const params = useParams();
    console.log(params)

    return (
        <>
            <h1>제품 상세보기 화면</h1>
            <p>
                제품ID: {params.prodId}, 페이지번호: {params.pageNo}
            </p>
        </>
    );
};

export default ProductDetail;
  • Products.js
const Products = () => {

    console.log("products!")
    
    return (
        <>
            <h1>My Products Page</h1>
            <ul>
                {
                    DUMMY_PRODUCTS.map(prod => (
                        <li key={prod.id}>
                            <Link to={`{${prod.id}/page/10}`} >{prod.name}</Link>
                        </li>
                    ))
                }
            </ul>
        </>
)
    ;
};
profile
백엔드 개발자
post-custom-banner

0개의 댓글