Skip to content

Commit 342d4b5

Browse files
committed
Improve list UX and login feedback
1 parent 1de0e78 commit 342d4b5

9 files changed

Lines changed: 195 additions & 97 deletions

File tree

src/main/java/com/hihat/blog/controller/UserApiController.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ public class UserApiController {
2525
private final UserService userService;
2626
private final AuthTokenManager authTokenManager;
2727

28-
@PostMapping("/login")
29-
public void Login(@ModelAttribute LoginUserReauest loginInfo, HttpServletRequest request , HttpServletResponse response) throws IOException {
30-
User user = userService.login(loginInfo.getUsername(), loginInfo.getPassword());
31-
String targetUrl = authTokenManager.progressAuthenticationTokenIssuance(request, response, user);
32-
response.sendRedirect(targetUrl);
33-
}
28+
@PostMapping("/login")
29+
public void Login(@ModelAttribute LoginUserReauest loginInfo, HttpServletRequest request , HttpServletResponse response) throws IOException {
30+
try {
31+
User user = userService.login(loginInfo.getUsername(), loginInfo.getPassword());
32+
String targetUrl = authTokenManager.progressAuthenticationTokenIssuance(request, response, user);
33+
response.sendRedirect(targetUrl);
34+
} catch (IllegalStateException ex) {
35+
response.sendRedirect("/login?error");
36+
}
37+
}
3438

3539
@PostMapping("/user")
3640
public String signup(@ModelAttribute AddUserReauest reauest) {
Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1-
package com.hihat.blog.controller;
2-
3-
import lombok.RequiredArgsConstructor;
4-
import org.springframework.stereotype.Controller;
5-
import org.springframework.ui.Model;
6-
import org.springframework.web.bind.annotation.GetMapping;
7-
import org.springframework.web.bind.annotation.RequestParam;
8-
9-
@Controller
10-
@RequiredArgsConstructor
11-
public class UserViewController {
12-
1+
package com.hihat.blog.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.stereotype.Controller;
5+
import org.springframework.ui.Model;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
9+
@Controller
10+
@RequiredArgsConstructor
11+
public class UserViewController {
12+
1313
@GetMapping("/login")
14-
public String login() {
15-
return "oauthLogin";
16-
}
17-
18-
@GetMapping("/signup")
19-
public String signup(@RequestParam(required = false) String email,
20-
@RequestParam(required = false) String name,
21-
Model model) {
22-
if (email != null && name != null) {
23-
model.addAttribute("email", email);
24-
model.addAttribute("name", name);
14+
public String login(@RequestParam(required = false) String error, Model model) {
15+
if (error != null) {
16+
model.addAttribute("loginError", true);
2517
}
26-
return "signup";
18+
return "oauthLogin";
2719
}
28-
}
20+
21+
@GetMapping("/signup")
22+
public String signup(@RequestParam(required = false) String email,
23+
@RequestParam(required = false) String name,
24+
Model model) {
25+
if (email != null && name != null) {
26+
model.addAttribute("email", email);
27+
model.addAttribute("name", name);
28+
}
29+
return "signup";
30+
}
31+
}
Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,61 @@
1-
package com.hihat.blog.domain;
2-
3-
import jakarta.persistence.*;
4-
import lombok.AccessLevel;
5-
import lombok.Builder;
6-
import lombok.Getter;
7-
import lombok.NoArgsConstructor;
8-
import org.springframework.data.annotation.CreatedDate;
9-
import org.springframework.data.annotation.LastModifiedDate;
10-
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
11-
12-
import java.time.LocalDateTime;
13-
14-
@Table(name = "article")
15-
@Entity // 엔티티 지정
16-
@Getter
17-
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected 기본 생성자
18-
@EntityListeners(AuditingEntityListener.class)
19-
public class Article {
20-
21-
@Id
22-
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto Increment
23-
@Column(name ="id", updatable = false)
24-
private Long id;
25-
1+
package com.hihat.blog.domain;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AccessLevel;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import org.springframework.data.annotation.CreatedDate;
9+
import org.springframework.data.annotation.LastModifiedDate;
10+
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
11+
12+
import java.time.LocalDateTime;
13+
14+
@Table(name = "article")
15+
@Entity // 엔티티 지정
16+
@Getter
17+
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected 기본 생성자
18+
@EntityListeners(AuditingEntityListener.class)
19+
public class Article {
20+
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto Increment
23+
@Column(name ="id", updatable = false)
24+
private Long id;
25+
2626
@Column(name = "title", nullable = false) // Not Null
2727
private String title;
2828

29-
@Column(name = "content" ,nullable = false) // Not Null
29+
@Lob
30+
@Column(name = "content", nullable = false, columnDefinition = "TEXT") // Not Null
3031
private String content;
31-
32-
@Column(name = "author", nullable = false)
33-
private String author;
34-
35-
@Column(name = "type", nullable = false)
36-
private String type;
37-
38-
@CreatedDate
39-
@Column(name = "created_at")
40-
private LocalDateTime createdAt;
41-
42-
@LastModifiedDate
43-
@Column(name = "updated_at")
44-
private LocalDateTime updatedAt;
45-
46-
@Builder // 빌더 패턴으로 객체 생성
47-
public Article(String title, String content, String author, String type) {
48-
this.title = title;
49-
this.content = content;
50-
this.author = author;
51-
this.type = type;
52-
}
53-
54-
// 수정 메서드
55-
public void update(String title, String content, String type) {
56-
this.title = title;
57-
this.content = content;
58-
this.type = type;
59-
}
60-
}
32+
33+
@Column(name = "author", nullable = false)
34+
private String author;
35+
36+
@Column(name = "type", nullable = false)
37+
private String type;
38+
39+
@CreatedDate
40+
@Column(name = "created_at")
41+
private LocalDateTime createdAt;
42+
43+
@LastModifiedDate
44+
@Column(name = "updated_at")
45+
private LocalDateTime updatedAt;
46+
47+
@Builder // 빌더 패턴으로 객체 생성
48+
public Article(String title, String content, String author, String type) {
49+
this.title = title;
50+
this.content = content;
51+
this.author = author;
52+
this.type = type;
53+
}
54+
55+
// 수정 메서드
56+
public void update(String title, String content, String type) {
57+
this.title = title;
58+
this.content = content;
59+
this.type = type;
60+
}
61+
}

src/main/resources/data.sql

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('제목 1', '<p>내용 1</p>', '작성자 1', '이론정리', NOW(), NOW());
22
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('제목 2', '<p>내용 2</p>', '작성자 2', '이론정리', NOW(), NOW());
33
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES ('제목 3', '<p>내용 3</p>', '작성자 3', '문제풀이', NOW(), NOW());
4-
INSERT INTO users (email, password, nickname) VALUES ('beomseok.dev@gmail.com', '', 'TESTER');
4+
INSERT INTO article (title, content, author, type, created_at, updated_at) VALUES (
5+
'제목 4',
6+
'<p>이 글은 목록에서 스크롤을 확인할 수 있도록 길게 작성된 샘플입니다.</p><p>여러 문단과 코드 블록, 리스트를 포함해 실제 작성 흐름을 확인합니다.</p><ul><li>그래프 탐색</li><li>DP 최적화</li><li>그리디 전략</li></ul><p>긴 내용이 카드 영역을 넘어서더라도 스크롤로 자연스럽게 확인할 수 있어야 합니다.</p><p>추가 문단을 계속 이어서 표시합니다. 추가 문단을 계속 이어서 표시합니다. 추가 문단을 계속 이어서 표시합니다.</p><p>추가 문단을 계속 이어서 표시합니다. 추가 문단을 계속 이어서 표시합니다. 추가 문단을 계속 이어서 표시합니다.</p>',
7+
'작성자 4',
8+
'이론정리',
9+
NOW(),
10+
NOW()
11+
);
12+
INSERT INTO users (email, password, nickname) VALUES ('beomseok.dev@gmail.com', '', 'TESTER');

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,55 @@ a:hover {
382382
padding-right: 0.35rem;
383383
}
384384

385+
.scroll-fade {
386+
scrollbar-width: thin;
387+
scrollbar-color: transparent transparent;
388+
scrollbar-gutter: stable;
389+
}
390+
391+
.scroll-fade::-webkit-scrollbar {
392+
width: 6px;
393+
}
394+
395+
.scroll-fade::-webkit-scrollbar-thumb {
396+
background: transparent;
397+
border-radius: 999px;
398+
}
399+
400+
.scroll-fade::-webkit-scrollbar-track {
401+
background: transparent;
402+
}
403+
404+
.scroll-fade:hover::-webkit-scrollbar-thumb,
405+
.scroll-fade.is-scrolling::-webkit-scrollbar-thumb {
406+
background: rgba(47, 111, 106, 0.35);
407+
}
408+
409+
.scroll-fade.is-scrolling,
410+
.scroll-fade:hover {
411+
scrollbar-color: rgba(47, 111, 106, 0.45) transparent;
412+
}
413+
385414
.article-row__link {
386415
font-weight: 600;
387416
color: var(--accent-strong);
388417
}
389418

419+
.empty-state {
420+
padding: 2rem 0;
421+
text-align: center;
422+
color: var(--muted);
423+
border-top: 1px solid var(--border);
424+
border-bottom: 1px solid var(--border);
425+
}
426+
427+
.empty-state strong {
428+
display: block;
429+
color: var(--text);
430+
font-size: 1.1rem;
431+
margin-bottom: 0.35rem;
432+
}
433+
390434
.article-card {
391435
background: var(--surface);
392436
border-radius: 18px;
@@ -427,11 +471,11 @@ a:hover {
427471
}
428472

429473
.article-detail {
430-
background: var(--surface);
431-
border-radius: 20px;
432-
border: 1px solid var(--border);
433-
padding: 1.6rem;
434-
box-shadow: var(--shadow);
474+
background: transparent;
475+
border-radius: 0;
476+
border: none;
477+
padding: 0;
478+
box-shadow: none;
435479
}
436480

437481
.article-detail__header {
@@ -457,6 +501,12 @@ a:hover {
457501
gap: 0.5rem;
458502
}
459503

504+
.content-divider {
505+
border: none;
506+
border-top: 1px solid var(--border);
507+
margin: 0 0 1.5rem;
508+
}
509+
460510
.article-title {
461511
font-size: 1.8rem;
462512
font-weight: 700;
@@ -555,6 +605,16 @@ a:hover {
555605
gap: 1rem;
556606
}
557607

608+
.auth-error {
609+
background: #fceceb;
610+
color: #9c2f2f;
611+
border: 1px solid #f3c3c1;
612+
padding: 0.65rem 0.8rem;
613+
border-radius: 12px;
614+
font-size: 0.95rem;
615+
margin-bottom: 0.8rem;
616+
}
617+
558618
.auth-footer {
559619
display: flex;
560620
justify-content: center;

src/main/resources/templates/article.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ <h1 class="article-title" th:text="${article.title}">제목</h1>
3333
<span th:if="${article.updatedAt != article.createdAt}">(수정됨)</span>
3434
</span>
3535
</div>
36+
<hr class="content-divider">
3637

3738
<div class="article-body ck-content" th:utext="${article.content}"></div>
3839
</article>

src/main/resources/templates/articleList.html

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ <h1 class="page-title" th:text="${type}">알고리즘 노트</h1>
1919
</div>
2020
</div>
2121

22-
<div class="article-list stagger">
22+
<div class="article-list stagger" th:if="${!#lists.isEmpty(articles)}">
2323
<article class="article-row" th:each="item : ${articles}">
2424
<div class="article-row__meta">
2525
<span class="pill" th:text="${item.type}">이론 정리</span>
2626
<a class="article-row__link" th:href="@{/articles/{id}(id=${item.id})}">자세히 보기</a>
2727
</div>
2828
<a class="article-row__title" th:href="@{/articles/{id}(id=${item.id})}" th:text="${item.title}">제목</a>
29-
<div class="article-row__content ck-content scroll" th:utext="${item.content}"></div>
29+
<div class="article-row__content ck-content scroll-fade" th:utext="${item.content}"></div>
3030
</article>
3131
</div>
3232

33-
<nav aria-label="Page navigation" class="page-nav">
33+
<div class="empty-state" th:if="${#lists.isEmpty(articles)}">
34+
<strong>등록된 글이 없습니다.</strong>
35+
<p>첫 글을 작성하거나 다른 카테고리를 선택해 주세요.</p>
36+
</div>
37+
38+
<nav aria-label="Page navigation" class="page-nav" th:if="${totalPages > 0}">
3439
<ul class="pagination justify-content-center">
3540
<li class="page-item">
3641
<a th:class="|page-link ${pageable.getPageNumber() > 0 ? '' : 'disabled'}|"
@@ -39,7 +44,7 @@ <h1 class="page-title" th:text="${type}">알고리즘 노트</h1>
3944
<span aria-hidden="true">&laquo;</span>
4045
</a>
4146
</li>
42-
<th:block th:each="page : ${#numbers.sequence(1, totalPages)}">
47+
<th:block th:if="${totalPages > 0}" th:each="page : ${#numbers.sequence(1, totalPages)}">
4348
<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>
4449
</th:block>
4550
<li class="page-item">
@@ -54,5 +59,15 @@ <h1 class="page-title" th:text="${type}">알고리즘 노트</h1>
5459
</div>
5560
</mainSection>
5661
<scriptSection>
62+
<script>
63+
document.querySelectorAll('.scroll-fade').forEach((element) => {
64+
let timer;
65+
element.addEventListener('scroll', () => {
66+
element.classList.add('is-scrolling');
67+
window.clearTimeout(timer);
68+
timer = window.setTimeout(() => element.classList.remove('is-scrolling'), 900);
69+
}, { passive: true });
70+
});
71+
</script>
5772
</scriptSection>
5873
</html>

src/main/resources/templates/login.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<h1 class="auth-title">로그인</h1>
1111
<p class="auth-subtitle">서비스를 사용하려면 로그인이 필요합니다.</p>
1212
</div>
13+
<div class="auth-error" th:if="${loginError}">
14+
이메일 또는 비밀번호가 올바르지 않습니다.
15+
</div>
1316
<form action="/login" method="POST" class="auth-form">
1417
<input type="hidden" th:name="${_csrf?.parameterName}" th:value="${_csrf?.token}" />
1518
<div class="form-field">

src/main/resources/templates/oauthLogin.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<h1 class="auth-title">로그인</h1>
1111
<p class="auth-subtitle">계정 또는 소셜 로그인으로 시작하세요.</p>
1212
</div>
13+
<div class="auth-error" th:if="${loginError}">
14+
이메일 또는 비밀번호가 올바르지 않습니다.
15+
</div>
1316

1417
<form action="/login" method="POST" class="auth-form">
1518
<input type="hidden" th:name="${_csrf?.parameterName}" th:value="${_csrf?.token}" />

0 commit comments

Comments
 (0)