[Springboot] Springboot+JPA를 통한 게시판 페이징 하기

728x90

 

오늘은 JPA를 활용하여 게시판을 페이징 해보도록 하겠습니다.


 

1) 서론

1-1) 페이징을 하는 이유 

여러분들은 페이징을 왜 해야하는가에 대해서 생각해본적이 있나요❓

컴퓨터 공학을 전공한 분이라면 페이징이라고 하면 두가지를 떠올릴 수 있습니다.

 

1) 게시판 같은, 웹 페이지에서 페이지가 넘어가는 페이징

2) 가상메모리 관리 기술인 페이징

 

두가지를 생각할 수 있습니다.

여기서 제가 설명할 것은 1번인 웹 페이지에서 페이지를 이동시키는 페이징을 진행 해보도록 할 것 입니다.

 

📢 결과적으로 제가 페이징을 하면서 느꼈던 것을 바탕으로 페이징을 필요성 대해서 적어보겠습니다.

  • 1) 사용자 경험 향상
    • 페이징을 하지 않으면 모든 글들이 한번에 나열되게됩니다. 글이 200개 300개 라면은 사용자가 그것을 보기에     불편할 것 이라고 생각합니다. 
  • 2) 로딩 시간 최적화 + DB 쿼리 최적화
    • 페이징을 하지 않으면 모든 글이 다 나열되게 됩니다. 그러면 DB에서 조회하는 시간이 그만큼 오래 걸립니다. 
    • 페이징을 하면은 정해둔 글의 갯수 만큼만 조회를 하기 때문에 그만큼 속도가 빠르고 성능 향상에 좋습니다.
  • 3) 서버 부하 감소
    • 페이징을 통해 한번에 많은 양의 폼 -> 컨트롤러 -> 서비스 로 이동하면서 처리를 하려면 그만큼 부하가 많이 생깁니다. 그러나 페이징을 하여 페이지 단위로 데이터를 전송하면은 서버 부하를 분산시킬 수 있고, 효율적이고 빠르게 데이터를 통신할 수 있습니다.

 

1-2) 어떻게 페이징을 하는가

페이징 기법에는 많은 방법이 있습니다. 

저는 SQL 쿼리를 쓰고 직접 메소드를 만들면서 했던 경험이 있습니다.

그때 경험은, 좀 복잡했고 코드가 길어진다는 단점이 있었습니다.

 

그러나 요번에 JPAPageable이라는 클래스를 사용해서 스프링부트 환경해서 편하게 해보았습니다.

기존에 했던 방법보다 코드도 훨씬 간결하고 파악도 하기 쉬워서 하는 방법만 알면 간단한 페이징을 할때 도움이 많이 될것이라고 생각합니다.  

바로 본론으로 들어가서 코드를 보겠습니다.

 

2) 본론

2-1) 페이징 처리 기법

JPA에 있는 메소드를 사용해서 처리를 했습니다. 

 

#1 DB와 통신

QuestionRepository.java

org.springframework.data.domain.Page
org.springframework.data.domain.PageRequest
org.springframework.data.domain.Pageable
package spring.project.repository;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import spring.project.dto.Question;

public interface QuestionRepository extends JpaRepository<Question,Integer> {
	Question findBySubject(String subject);
	Question findBySubjectAndContent(String subject, String content);
	List<Question> findBySubjectLike(String subject);
	Page<Question> findAll(Pageable pageable);
}

 

Pageable 객체를 입력으로 받아 Page<Question> 타입 객체를 리턴하는 findAll 메서드를 생성

 

#2 비즈니스 로직 처리

QuestionService.java

package spring.project.service;

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

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import spring.project.dto.Question;
import spring.project.repository.QuestionRepository;
import spring.project.toy.DataNotFoundException;

@RequiredArgsConstructor
@Service
public class QuestionService {

	private final QuestionRepository questionRepository;

	public List<Question> getList() {
		return this.questionRepository.findAll();
	}

	//num에 해당하는 question 데이터 조회
	public Question getQuestion(Integer id) {
		Optional<Question> question = this.questionRepository.findById(id);
		if(question.isPresent()) {
			return question.get();
		} else {
			throw new DataNotFoundException("question not found");
		}
	}

	public void create(String subject, String content) {
		Question q = new Question();
		q.setSubject(subject);
		q.setContent(content);
		q.setWirteday(LocalDateTime.now());
		this.questionRepository.save(q);
	}
	
	public Page<Question> getList(int page) {
		Pageable pageable = PageRequest.of(page,10);
		return this.questionRepository.findAll(pageable);
	}

}

객체지향 기법인 메소드 오버로딩을 통해서 getList메소드 하나를 더 만들어 줬습니다.

getList() 메소드는 이제 2가지의 역할을 할 수 있습니다.

getList는 int 타입의 페이지 번호를 입력받아 해당 페이지의 질문 목록을 리턴하는 메소드로 변경.

PageRequest.of(page,10) 은 page는 조회할 페이지 번호이고, 10은 한 페이지에 보여줄 게시물 갯수를 의미합니다.

위 과정을 통해 데이터 전체를 조회하지 않고 해당 페이지의 데이터만 조회하도록 쿼리가 변경됩니다.

 

#3 컨트롤러

@GetMapping("/list")
	public String list(Model model,
						@RequestParam(value="page",defaultValue = "0") int page) {
		Page<Question> paging = this.questionService.getList(page);
		model.addAttribute("paging",paging);
		return "question_list";
	}

기존 list를 출력하는 컨트롤러에서 이제 Paging 한 리스트를 출력하는 코드로 교체했습니다.

http://localhost:8080/question/list?page=0 처럼 GET 방식으로 요청된 URL에서 page값을 가져오기 위해 @RequestParam(value="page", defaultValue="0") int page 매개변수가 list메소드에 추가됨

URL에 페이지 파라미터 page가 전달되지 않은 경우를 대비해 기본 값을 0으로 설정했다.

정보 ❗ : 스프링부트의 페이징은 첫페이지 번호가 1이 아닌 0이다.

 

 

그리고 form에 전달할때 Page 객체인 paging을 model에 담아 전달하였다.

Page 객체에는 여러 메소드가 있다. 다음 메소드는 템플릿(=타임리프,html)에서 처리할때 사용한다

항목 설명

paging.isEmpty 페이지 존재 여부 (게시물이 있으면 false, 없으면 true)
paging.totalElements 전체 게시물 개수
paging.totalPages 전체 페이지 개수
paging.size 페이지당 보여줄 게시물 개수
paging.number 현재 페이지 번호
paging.hasPrevious 이전 페이지 존재 여부
paging.hasNext 다음 페이지 존재 여부

Java에서 페이징 로직은 끝났다 이제 실행시키면 게시물이 10개씩 뜨는걸 확인할 수 있습니다.

그러나 페이지를 이동할 수 있는 것이 없습니다.

그러기 위해서는 html파일에가서 만들어줘야 합니다.

<!-- 페이징처리 시작 -->
        <div th:if="${!paging.isEmpty()}">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
                    <a class="page-link"
                       th:href="@{|?page=${paging.number-1}|}">
                        <span>이전</span>
                    </a>
                </li>
                <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
                    th:classappend="${page == paging.number} ? 'active'"
                    class="page-item">
                    <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
                </li>
                <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=${paging.number+1}|}">
                        <span>다음</span>
                    </a>
                </li>
            </ul>
        </div>
        <!-- 페이징처리 끝 -->

 

이전 페이지가 없는 경우에는 "이전" 링크가 비활성화(disabled)되도록 하였다. (다음페이지의 경우도 마찬가지 방법으로 적용했다.) 그리고 페이지 리스트를 루프 돌면서 해당 페이지로 이동할 수 있는 링크를 생성하였다. 이때 루프 도중의 페이지가 현재 페이지와 같을 경우에는 active클래스를 적용하여 강조표시(선택표시)도 해 주었다.

위 템플릿에 사용된 주요 페이징 기능을 정리한 표 입니다.

 

페이징 기능 코드

이전 페이지가 없으면 비활성화 th:classappend="${!paging.hasPrevious} ? 'disabled'"
다음 페이지가 없으면 비활성화 th:classappend="${!paging.hasNext} ? 'disabled'"
이전 페이지 링크 th:href="@{
다음 페이지 링크 th:href="@{
페이지 리스트 루프 th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
현재 페이지와 같으면 active 적용 th:classappend="${page == paging.number} ? 'active'"

 

위 html 템플릿 엔진, 즉 타임리프를 사용하면은 쉽게 할 수 있습니다.

하지만 여기서 알아둬야 할 것이 위 조건만 하면은

게시물의 보여줄 페이지 개수 만큼 숫자가 다 나오게 됩니다.

위 문제를 해결하기 위해서, 조건을 줘서 다시 처리를 해야 합니다.

이전다음 사이에 페이징 숫자가 들어가기 때문에 위 가운데 코드에서 조건을 주어서

처리를 해야 합니다.

<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}" 
                th:if="${page >= paging.number-5 and page <= paging.number+5}"
                th:classappend="${page == paging.number} ? 'active'" 
                class="page-item">
                <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
 </li>

위 코드를 사용하면 페이지 리스트가 현재 페이지 기준으로 좌우 5개씩 보이도록 만들었다.

루프내에 표시되는 페이지가 현재 페이지를 의미하는 paging.number 보다 5만큼 작거나 큰 경우에만 표시되도록 한 것이다.

지금은 질문이 등록한 순서대로 데이터가 표시되는데, 이것을

가장 최근에 작성한 게시물이 가장 상단에 있게 코드를 짜보겠습니다.

sql을 사용하게 된다면

select * from order by writeday desc;

이런식으로 내림차순으로 바로 정렬 할 수 있지만, 우리는 JPA를 사용하니 아래 코드를 사용한다.

public Page<Question> getList(int page) {
		List<Sort.Order> sorts = new ArrayList<Sort.Order>();
		sorts.add(Sort.Order.desc("wirteday"));
		Pageable pageable = PageRequest.of(page,10, Sort.by(sorts));
		return this.questionRepository.findAll(pageable);
	}

게시물을 역순으로 조회하기 위해서는, PageRequest.of() 메소드의 세번째 파라미터로 Sort 객체를 전달해야한다.

Sort.Order 객체로 구성된 ArrayList에

add메소드를 통해서 List에 (Sort.Order.desc(”wirteday”) 추가한다

위 메소드의 뜻은 wirteday의 기준으로 내림차순으로 정렬한다는 뜻을 가진 메소드 입니다.

 

💡 만약 작성일시 외에 추가로 정렬조건이 필요할 경우에는 sorts 리스트에 추가하면 된다.

 

<!-- 페이징처리 시작 -->
        <div th:if="${!paging.isEmpty()}">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
                    <a class="page-link"
                       th:href="@{|?page=${paging.number-1}|}">
                        <span>이전</span>
                    </a>
                </li>
                <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
                    th:classappend="${page == paging.number} ? 'active'"
                    class="page-item">
                    <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
                </li>
                <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=${paging.number+1}|}">
                        <span>다음</span>
                    </a>
                </li>
            </ul>
        </div>
        <!-- 페이징처리 끝 -->

이전 페이지가 없는 경우에는 "이전" 링크가 비활성화(disabled)되도록 하였다. (다음페이지의 경우도 마찬가지 방법으로 적용했다.) 그리고 페이지 리스트를 루프 돌면서 해당 페이지로 이동할 수 있는 링크를 생성하였다. 이때 루프 도중의 페이지가 현재 페이지와 같을 경우에는 active클래스를 적용하여 강조표시(선택표시)도 해 주었다.

위 템플릿에 사용된 주요 페이징 기능을 정리한 표 입니다.

페이징 기능 코드

이전 페이지가 없으면 비활성화 th:classappend="${!paging.hasPrevious} ? 'disabled'"
다음 페이지가 없으면 비활성화 th:classappend="${!paging.hasNext} ? 'disabled'"
이전 페이지 링크 th:href="@{
다음 페이지 링크 th:href="@{
페이지 리스트 루프 th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
현재 페이지와 같으면 active 적용 th:classappend="${page == paging.number} ? 'active'"

위 html 템플릿 엔진, 즉 타임리프를 사용하면은 쉽게 할 수 있습니다.

하지만 여기서 알아둬야 할 것이 위 조건만 하면은

게시물의 보여줄 페이지 개수 만큼 숫자가 다 나오게 됩니다.

위 문제를 해결하기 위해서, 조건을 줘서 다시 처리를 해야 합니다.

이전다음 사이에 페이징 숫자가 들어가기 때문에 위 가운데 코드에서 조건을 주어서

처리를 해야 합니다.

<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}" 
                th:if="${page >= paging.number-5 and page <= paging.number+5}"
                th:classappend="${page == paging.number} ? 'active'" 
                class="page-item">
                <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
 </li>

위 코드를 사용하면 페이지 리스트가 현재 페이지 기준으로 좌우 5개씩 보이도록 만들었다.

루프내에 표시되는 페이지가 현재 페이지를 의미하는 paging.number 보다 5만큼 작거나 큰 경우에만 표시되도록 한 것이다.

지금은 질문이 등록한 순서대로 데이터가 표시되는데, 이것을

가장 최근에 작성한 게시물이 가장 상단에 있게 코드를 짜보겠습니다.

sql을 사용하게 된다면

select * from order by writeday desc;

이런식으로 내림차순으로 바로 정렬 할 수 있지만, 우리는 JPA를 사용하니 아래 코드를 사용한다.

public Page<Question> getList(int page) {
		List<Sort.Order> sorts = new ArrayList<Sort.Order>();
		sorts.add(Sort.Order.desc("wirteday"));
		Pageable pageable = PageRequest.of(page,10, Sort.by(sorts));
		return this.questionRepository.findAll(pageable);
	}

게시물을 역순으로 조회하기 위해서는, PageRequest.of() 메소드의 세번째 파라미터로 Sort 객체를 전달해야한다.

Sort.Order 객체로 구성된 ArrayList에

add메소드를 통해서 List에 (Sort.Order.desc(”wirteday”) 추가한다

위 메소드의 뜻은 wirteday의 기준으로 내림차순으로 정렬한다는 뜻을 가진 메소드 입니다.

💡 만약 작성일시 외에 추가로 정렬조건이 필요할 경우에는 sorts 리스트에 추가하면 된다.

3) 결론

3-1) 전체코드

question.java

package spring.project.dto;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Data;

@Data
@Entity
public class Question {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;

	@Column(length = 200)
	private String subject;

	@Column(columnDefinition = "TEXT")
	private String content;

	private LocalDateTime wirteday;

	//질문 하나에는 여러개의 답변이 작성 될 수 있다.
	//이때 질문 삭제시 그에 달린 모든 답변또한 삭제 -> sql cascade랑 같은 역할.
	@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
	private List<Answer> answerList;
}

QuestionService.java

package spring.project.service;

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

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import spring.project.dto.Question;
import spring.project.repository.QuestionRepository;
import spring.project.toy.DataNotFoundException;

@RequiredArgsConstructor
@Service
public class QuestionService {

	private final QuestionRepository questionRepository;

	public List<Question> getList() {
		return this.questionRepository.findAll();
	}

	//num에 해당하는 question 데이터 조회
	public Question getQuestion(Integer id) {
		Optional<Question> question = this.questionRepository.findById(id);
		if(question.isPresent()) {
			return question.get();
		} else {
			throw new DataNotFoundException("question not found");
		}
	}

	public void create(String subject, String content) {
		Question q = new Question();
		q.setSubject(subject);
		q.setContent(content);
		q.setWirteday(LocalDateTime.now());
		this.questionRepository.save(q);
	}

	public Page<Question> getList(int page) {
		List<Sort.Order> sorts = new ArrayList<Sort.Order>();
		sorts.add(Sort.Order.desc("wirteday"));
		Pageable pageable = PageRequest.of(page,10, Sort.by(sorts));
		return this.questionRepository.findAll(pageable);
	}

}

QuestionController.java

package spring.project.controller;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import spring.project.dto.AnswerForm;
import spring.project.dto.Question;
import spring.project.dto.QuestionForm;
import spring.project.service.QuestionService;

@RequiredArgsConstructor
@Controller
@RequestMapping("/question")
public class QuestionController {

	//Requiredargsconstructor 는 questionRepository 속성을 포함하는 생성자를 생성한다.
	//final이 붙은 속성을 포함하는 생성자를 자동으로 생성하는 역할을 한다.
	//이 어노테이션 때문에 questionRepository가 bean이 주입이 된다.

	private final QuestionService questionService;

	@GetMapping("/")
	public String root() {
		return "/question/list";
	}
	//redirect:<URL> - URL로 리다이렉트 (리다이렉트는 완전히 새로운 URL로 요청이 된다.)
	//forward:<URL> - URL로 포워드 (포워드는 기존 요청 값들이 유지된 상태로 URL이 전환된다.)

	@GetMapping("/list")
	public String list(Model model,
						@RequestParam(value="page",defaultValue = "0") int page) {
		Page<Question> paging = this.questionService.getList(page);
		model.addAttribute("paging",paging);
		return "question_list";
	}

	@GetMapping(value="/detail/{id}")
	public String detail(Model model, @PathVariable("id") Integer id, AnswerForm answerForm) {
		Question question = this.questionService.getQuestion(id);
		model.addAttribute("question",question);
		return "question_detail";
	}

	@GetMapping("/create")
	public String questionCreate(QuestionForm questionForm) {
		return "question_form";
	}
	//아래위 같아도 상관없는 이유 => 메소드 오버로딩
	@PostMapping("/create")
	public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
		/*
		* BindingResult 매개변수는 항상 @Valid 매개변수 바로 뒤에 위치해야 한다.
		* 만약 2개의 매개변수의 위치가 정확하지 않다면 @Valid만 적용이 되어 입력값 검증 실패 시 400 오류가 발생한다.
		* */

		if(bindingResult.hasErrors()) {
			return "question_form";
		}
		questionService.create(questionForm.getSubject(), questionForm.getContent());
		return "redirect:/question/list";
	}
}

question_list.html

<html layout:decorate="~{layout}">
    <div layout:fragment="content" class="container my-3">
        <table class="table table-bordered" style="margin-top:1.3%;">
            <thead class="table-dark">
                <tr>
                    <th width="50">번호</th>
                    <th width="150">제목</th>
                    <th width="150">작성일시</th>
                </tr>
            </thead>

        <tbody>
            <tr th:each="question, loop:${paging}">
                <td th:text="${loop.count}"></td>
                <td>
                    <a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
                </td>
                <td th:text="${#temporals.format(question.wirteday, 'yyyy-MM-dd HHMMSS')}"></td>
            </tr>
        </tbody>
           <!-- <a th:href="@{/question/create}" class="bnt btn-outline-dark">질문 등록</a>-->
            <button onclick="location.href='/question/create'" type="button" class="btn btn-outline-primary">질문 등록</button><br>
        </table>

        <!-- 페이징처리 시작 -->
        <div th:if="${!paging.isEmpty()}">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
                    <a class="page-link"
                       th:href="@{|?page=${paging.number-1}|}">
                        <span>이전</span>
                    </a>
                </li>
                <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
                    th:if="${page >= paging.number-5 and page <= paging.number+5}"
                    th:classappend="${page == paging.number} ? 'active'"
                    class="page-item">
                    <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
                </li>
                <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=${paging.number+1}|}">
                        <span>다음</span>
                    </a>
                </li>
            </ul>
        </div>
        <!-- 페이징처리 끝 -->

    </div>
</html>

layout.html

<!Doctype html>
<html lang="ko">
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
  <link rel="stylesheet" type="text/css" th:href="@{/style.css}">
  <title>Hello kyu</title>
</head>

<body>
<!--네비게이션 바-->
<nav th:replace="~{navbar :: navbarFragment}"></nav>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<script th:src="@{/bootstrap.min.js}"></script>
</body>

</html>

navbar.html

<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
  <div class="container-fluid">
    <a class="navbar-brand" href="/">Board</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <a class="nav-link" href="#">로그인</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">대쉬보드</a>
        </li>
      </ul>
    </div>
  </div>
</nav>

 

 

 JPA+Springboot+JPA 를 활용한 페이징을 마치겠습니다. 

틀린 부분있으면 지적해주세요. 감사합니다.

 

 

ref : https://wikidocs.net/162028

728x90