Skip to content

Commit d287ff2

Browse files
committed
Refine pagination and expand seed data
1 parent 3b6b01f commit d287ff2

4 files changed

Lines changed: 195 additions & 55 deletions

File tree

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,69 @@
1-
package com.hihat.blog.controller;
2-
3-
import com.hihat.blog.domain.Article;
4-
import com.hihat.blog.dto.ArticleListViewResponse;
5-
import com.hihat.blog.dto.ArticleViewResponse;
6-
import com.hihat.blog.service.BlogService;
7-
import com.hihat.blog.util.PageableImpl;
8-
import lombok.RequiredArgsConstructor;
9-
import org.springframework.data.domain.Page;
10-
import org.springframework.data.domain.Pageable;
11-
import org.springframework.stereotype.Controller;
12-
import org.springframework.ui.Model;
13-
import org.springframework.web.bind.annotation.GetMapping;
14-
import org.springframework.web.bind.annotation.ModelAttribute;
15-
import org.springframework.web.bind.annotation.PathVariable;
16-
import org.springframework.web.bind.annotation.RequestParam;
17-
18-
import java.util.List;
19-
20-
@Controller
21-
@RequiredArgsConstructor
22-
public class BlogViewController {
23-
24-
private final BlogService blogService;
25-
26-
@GetMapping(value = {"/articles"})
27-
public String getArticles(@RequestParam(required = false) String type,
28-
@RequestParam(required = false) Integer page,
29-
@RequestParam(required = false) Integer size,
30-
Model model) {
31-
if (type == null) {
32-
type = "이론정리";
33-
}
34-
Pageable pageable = new PageableImpl(page, size);
35-
Page<Article> pagingObj = blogService.findAllByTypeAndPaging(type, pageable);
1+
package com.hihat.blog.controller;
2+
3+
import com.hihat.blog.domain.Article;
4+
import com.hihat.blog.dto.ArticleListViewResponse;
5+
import com.hihat.blog.dto.ArticleViewResponse;
6+
import com.hihat.blog.service.BlogService;
7+
import com.hihat.blog.util.PageableImpl;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Page;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.stereotype.Controller;
12+
import org.springframework.ui.Model;
13+
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.ModelAttribute;
15+
import org.springframework.web.bind.annotation.PathVariable;
16+
import org.springframework.web.bind.annotation.RequestParam;
17+
18+
import java.util.List;
19+
20+
@Controller
21+
@RequiredArgsConstructor
22+
public class BlogViewController {
23+
24+
private final BlogService blogService;
25+
26+
@GetMapping(value = {"/articles"})
27+
public String getArticles(@RequestParam(required = false) String type,
28+
@RequestParam(required = false) Integer page,
29+
@RequestParam(required = false) Integer size,
30+
Model model) {
31+
if (type == null) {
32+
type = "이론정리";
33+
}
34+
Pageable pageable = new PageableImpl(page, size);
35+
Page<Article> pagingObj = blogService.findAllByTypeAndPaging(type, pageable);
3636
List<ArticleListViewResponse> articles = pagingObj.getContent()
3737
.stream()
3838
.map(ArticleListViewResponse::new)
3939
.toList();
4040
model.addAttribute("articles", articles);
41-
model.addAttribute("totalPages", pagingObj.getTotalPages());
41+
int totalPages = pagingObj.getTotalPages();
42+
int currentPage = totalPages == 0 ? 1 : Math.min(Math.max(pageable.getPageNumber() + 1, 1), totalPages);
43+
int prevPage = Math.max(0, currentPage - 2);
44+
int nextPage = totalPages == 0 ? 0 : Math.min(totalPages - 1, currentPage);
45+
46+
model.addAttribute("totalPages", totalPages);
47+
model.addAttribute("currentPage", currentPage);
48+
model.addAttribute("hasPrev", currentPage > 1);
49+
model.addAttribute("hasNext", currentPage < totalPages);
50+
model.addAttribute("prevPage", prevPage);
51+
model.addAttribute("nextPage", nextPage);
52+
model.addAttribute("lastPage", Math.max(0, totalPages - 1));
53+
model.addAttribute("paginationItems", buildPaginationItems(currentPage, totalPages));
4254
model.addAttribute("pageable", pageable);
4355
model.addAttribute("type", type);
4456
return "articleList";
4557
}
46-
47-
@GetMapping("/articles/{id}")
48-
public String getArticle(@PathVariable Long id, Model model) {
49-
Article article = blogService.findById(id);
50-
model.addAttribute("article", article);
51-
return "article";
52-
}
53-
54-
@GetMapping("/new-article")
58+
59+
@GetMapping("/articles/{id}")
60+
public String getArticle(@PathVariable Long id, Model model) {
61+
Article article = blogService.findById(id);
62+
model.addAttribute("article", article);
63+
return "article";
64+
}
65+
66+
@GetMapping("/new-article")
5567
public String newArticle(@RequestParam(required = false) Long id, Model model) {
5668
if (id == null) {
5769
model.addAttribute("article", new ArticleViewResponse());
@@ -60,4 +72,35 @@ public String newArticle(@RequestParam(required = false) Long id, Model model) {
6072
}
6173
return "newArticle";
6274
}
75+
76+
private List<Integer> buildPaginationItems(int currentPage, int totalPages) {
77+
if (totalPages <= 0) {
78+
return List.of();
79+
}
80+
if (totalPages <= 7) {
81+
return java.util.stream.IntStream.rangeClosed(1, totalPages).boxed().toList();
82+
}
83+
int start = Math.max(2, currentPage - 1);
84+
int end = Math.min(totalPages - 1, currentPage + 1);
85+
if (currentPage <= 3) {
86+
start = 2;
87+
end = 4;
88+
} else if (currentPage >= totalPages - 2) {
89+
start = totalPages - 3;
90+
end = totalPages - 1;
91+
}
92+
java.util.List<Integer> items = new java.util.ArrayList<>();
93+
items.add(1);
94+
if (start > 2) {
95+
items.add(-1);
96+
}
97+
for (int page = start; page <= end; page++) {
98+
items.add(page);
99+
}
100+
if (end < totalPages - 1) {
101+
items.add(-1);
102+
}
103+
items.add(totalPages);
104+
return items;
105+
}
63106
}

src/main/resources/data.sql

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,34 @@ INSERT INTO article (title, content, author, type, created_at, updated_at) VALUE
99
NOW(),
1010
NOW()
1111
);
12+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - BFS 레벨 탐색', '<p>큐를 활용해 레벨 순으로 방문합니다.</p><p>최단 거리 문제에 자주 쓰입니다.</p>', '관리자', '이론정리', NOW(), NOW());
13+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 14502 연구소', '<p>벽 3개를 세우고 안전 영역을 최대화합니다.</p><p>브루트포스 + BFS 조합을 연습하기 좋습니다.</p>', '관리자', '문제풀이', NOW(), NOW());
14+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 이분 탐색', '<p>정렬된 배열에서 구간을 절반씩 줄여 찾습니다.</p><pre><code>while (lo &lt;= hi) {\n int mid = (lo + hi) / 2;\n}\n</code></pre>', '관리자', '이론정리', NOW(), NOW());
15+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 2178 미로 탐색', '<p>격자 최단 거리는 BFS로 풉니다.</p><p>방문 배열을 반드시 확인합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
16+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 세그먼트 트리', '<p>구간 합과 최댓값을 빠르게 질의합니다.</p><ul><li>구간 분할</li><li>트리 구축</li><li>업데이트</li></ul>', '관리자', '이론정리', NOW(), NOW());
17+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 1916 최소비용 구하기', '<p>다익스트라로 최단 경로를 계산합니다.</p><p>우선순위 큐 사용이 핵심입니다.</p>', '관리자', '문제풀이', NOW(), NOW());
18+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 최소 스패닝 트리', '<p>모든 노드를 최소 비용으로 연결합니다.</p><p>크루스칼과 프림을 비교합니다.</p>', '관리자', '이론정리', NOW(), NOW());
19+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 1197 최소 스패닝 트리', '<p>간선을 정렬하고 유니온 파인드를 사용합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
20+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 위상 정렬', '<p>진입 차수가 0인 노드를 순서대로 처리합니다.</p><p>사이클 존재 여부도 함께 확인합니다.</p>', '관리자', '이론정리', NOW(), NOW());
21+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 2623 음악프로그램', '<p>위상 정렬 결과를 출력합니다.</p><p>사이클이면 0을 출력합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
22+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 다익스트라', '<p>양의 가중치 그래프에서 최단 경로를 구합니다.</p><p>방문 처리 시점을 주의합니다.</p>', '관리자', '이론정리', NOW(), NOW());
23+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 4485 녹색 옷', '<p>격자형 그래프에서 다익스트라를 적용합니다.</p><p>비용 누적을 체크합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
24+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 플로이드 워셜', '<p>모든 쌍 최단 거리를 구하는 알고리즘입니다.</p><p>O(N^3) 복잡도를 이해합니다.</p>', '관리자', '이론정리', NOW(), NOW());
25+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 11404 플로이드', '<p>도시 간 최소 비용을 플로이드 워셜로 계산합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
26+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 유니온 파인드', '<p>서로소 집합 자료구조를 설명합니다.</p><p>경로 압축과 랭크 최적화를 포함합니다.</p>', '관리자', '이론정리', NOW(), NOW());
27+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 1717 집합', '<p>유니온 파인드로 합집합과 질의를 처리합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
28+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - DP 기본', '<p>작은 문제로 쪼개서 큰 문제를 해결합니다.</p><p>메모이제이션과 테이블 구성법을 정리합니다.</p>', '관리자', '이론정리', NOW(), NOW());
29+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 11053 LIS', '<p>가장 긴 증가하는 부분 수열을 구합니다.</p><p>O(N log N) 풀이로 정리합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
30+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 그리디', '<p>매 순간 최선의 선택을 합니다.</p><p>정당성 증명을 함께 기록합니다.</p>', '관리자', '이론정리', NOW(), NOW());
31+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 1931 회의실', '<p>끝나는 시간이 빠른 순서로 선택합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
32+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 누적 합', '<p>구간 합을 빠르게 계산하기 위한 전처리입니다.</p><p>1차원과 2차원을 비교합니다.</p>', '관리자', '이론정리', NOW(), NOW());
33+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 11659 구간 합', '<p>누적 합 배열로 질의를 처리합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
34+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 투 포인터', '<p>두 개의 포인터를 움직이며 구간 조건을 만족합니다.</p><p>정렬 여부를 확인합니다.</p>', '관리자', '이론정리', NOW(), NOW());
35+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 1806 부분합', '<p>최소 길이 부분합을 투 포인터로 해결합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
36+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 슬라이딩 윈도우', '<p>고정 길이 구간을 이동하면서 계산합니다.</p><p>카운트 업데이트가 핵심입니다.</p>', '관리자', '이론정리', NOW(), NOW());
37+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 12891 DNA 비밀번호', '<p>문자 빈도를 유지하면서 유효 조건을 검사합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
38+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 스택과 큐', '<p>기본 자료구조의 동작을 정리합니다.</p><p>괄호 검사 예제를 포함합니다.</p>', '관리자', '이론정리', NOW(), NOW());
39+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 10828 스택', '<p>스택의 기본 연산을 구현합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
40+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('이론 정리 - 트라이', '<p>문자열 검색을 위한 트리 구조입니다.</p><p>접두사 탐색에 유리합니다.</p>', '관리자', '이론정리', NOW(), NOW());
41+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('문제 풀이 - 5052 전화번호', '<p>트라이로 접두사 충돌을 검사합니다.</p>', '관리자', '문제풀이', NOW(), NOW());
1242
INSERT INTO users (email, password, nickname) VALUES ('beomseok.dev@gmail.com', '', 'TESTER');

src/main/resources/static/assets/css/common.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,18 @@ a:hover {
729729
margin-top: 1.6rem;
730730
}
731731

732+
.pagination-slim {
733+
gap: 0.4rem;
734+
flex-wrap: wrap;
735+
}
736+
732737
.page-link {
733738
border-radius: 10px;
734739
border: 1px solid var(--border);
735740
color: var(--muted);
741+
padding: 0.35rem 0.75rem;
742+
min-width: 2.4rem;
743+
text-align: center;
736744
}
737745

738746
.page-link.active,
@@ -748,6 +756,41 @@ a:hover {
748756
opacity: 0.5;
749757
}
750758

759+
.page-ellipsis .page-link {
760+
background: transparent;
761+
border-style: dashed;
762+
color: var(--muted);
763+
pointer-events: none;
764+
}
765+
766+
.page-status {
767+
font-weight: 600;
768+
color: var(--text);
769+
background: var(--surface-soft);
770+
}
771+
772+
.page-item--mobile {
773+
display: none;
774+
}
775+
776+
.page-item--desktop {
777+
display: inline-flex;
778+
}
779+
780+
@media (max-width: 640px) {
781+
.pagination-slim {
782+
gap: 0.3rem;
783+
}
784+
785+
.page-item--desktop {
786+
display: none;
787+
}
788+
789+
.page-item--mobile {
790+
display: inline-flex;
791+
}
792+
}
793+
751794
.site-footer {
752795
padding: 2rem 0 3rem;
753796
border-top: 1px solid var(--border);

src/main/resources/templates/articleList.html

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,47 @@ <h1 class="page-title" th:text="${type}">알고리즘 노트</h1>
3535
<p>첫 글을 작성하거나 다른 카테고리를 선택해 주세요.</p>
3636
</div>
3737

38-
<nav aria-label="Page navigation" class="page-nav" th:if="${totalPages > 0}">
39-
<ul class="pagination justify-content-center">
38+
<nav aria-label="Page navigation" class="page-nav" th:if="${totalPages > 1}">
39+
<ul class="pagination pagination-slim justify-content-center">
40+
<li class="page-item page-item--desktop">
41+
<a th:class="|page-link ${hasPrev ? '' : 'disabled'}|"
42+
th:href="@{/articles(type=${type}, page=0, size=${pageable.getPageSize()})}"
43+
aria-label="First">
44+
처음
45+
</a>
46+
</li>
4047
<li class="page-item">
41-
<a th:class="|page-link ${pageable.getPageNumber() > 0 ? '' : 'disabled'}|"
42-
th:href="@{/articles(type=${type}, page=${pageable.getPageNumber() - 1}, size=${pageable.getPageSize()})}"
48+
<a th:class="|page-link ${hasPrev ? '' : 'disabled'}|"
49+
th:href="@{/articles(type=${type}, page=${prevPage}, size=${pageable.getPageSize()})}"
4350
aria-label="Previous">
44-
<span aria-hidden="true">&laquo;</span>
51+
이전
4552
</a>
4653
</li>
47-
<th:block th:if="${totalPages > 0}" th:each="page : ${#numbers.sequence(1, totalPages)}">
48-
<li class="page-item"><a th:class="|page-link ${page eq pageable.getPageNumber() + 1 ? 'active' : ''}|" th:href="@{/articles(type=${type}, page=${page - 1}, size=${pageable.getPageSize()})}" th:text="${page}">page</a></li>
54+
<li class="page-item page-item--mobile">
55+
<span class="page-link page-status" th:text="|${currentPage} / ${totalPages}|">1 / 1</span>
56+
</li>
57+
<th:block th:each="page : ${paginationItems}">
58+
<li class="page-item page-item--desktop" th:if="${page > 0}">
59+
<a th:class="|page-link ${page == currentPage ? 'active' : ''}|"
60+
th:href="@{/articles(type=${type}, page=${page - 1}, size=${pageable.getPageSize()})}"
61+
th:text="${page}">page</a>
62+
</li>
63+
<li class="page-item page-ellipsis page-item--desktop" th:if="${page < 0}">
64+
<span class="page-link" aria-hidden="true">...</span>
65+
</li>
4966
</th:block>
5067
<li class="page-item">
51-
<a th:class="|page-link ${pageable.getPageNumber() < totalPages - 1 ? '' : 'disabled'}|"
52-
th:href="@{/articles(type=${type}, page=${pageable.getPageNumber() + 1}, size=${pageable.getPageSize()})}"
68+
<a th:class="|page-link ${hasNext ? '' : 'disabled'}|"
69+
th:href="@{/articles(type=${type}, page=${nextPage}, size=${pageable.getPageSize()})}"
5370
aria-label="Next">
54-
<span aria-hidden="true">&raquo;</span>
71+
다음
72+
</a>
73+
</li>
74+
<li class="page-item page-item--desktop">
75+
<a th:class="|page-link ${hasNext ? '' : 'disabled'}|"
76+
th:href="@{/articles(type=${type}, page=${lastPage}, size=${pageable.getPageSize()})}"
77+
aria-label="Last">
78+
5579
</a>
5680
</li>
5781
</ul>

0 commit comments

Comments
 (0)