로그인 페이지 개선

This commit is contained in:
pandoli365 2026-05-03 18:07:38 +09:00
parent 3508a4b3bb
commit 49dd1e5a6f
5 changed files with 322 additions and 11 deletions

View File

@ -13,6 +13,7 @@ import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
//이곳에서는 뷰만 제어
@Controller
@ -28,15 +29,20 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
return mv;
}
/* ✅ 허용된 페이지 목록 */
private static final List<String> ALLOWED_PAGES = Arrays.asList(
"error", "login", "profile", "signup", ""
);
@GetMapping("/{pageName}")
public ModelAndView mainView(@PathVariable("pageName") String pageName,
@RequestParam(name = "id", required = false) String id,
HttpSession session,
HttpServletRequest request) {
@RequestParam(name = "id", required = false) String id,
HttpSession session,
HttpServletRequest request) {
ModelAndView mv = new ModelAndView();
// if (!ALLOWED_PAGES.contains(keyword)) {
// return new ModelAndView("redirect:/main");
// }
if (!ALLOWED_PAGES.contains(pageName)) {
return new ModelAndView("redirect:/error");
}
switch (pageName) {
case "error":
mv.setViewName("/errer");
@ -46,8 +52,17 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
}
break;
case "login":
if (isLoggedIn(session)) {
return new ModelAndView("redirect:/profile");
}
mv.setViewName("/login");
break;
case "profile":
if (!isLoggedIn(session)) {
return new ModelAndView("redirect:/login");
}
mv.setViewName("/profile");
break;
case "signup":
mv.setViewName("/signup");
break;
@ -59,6 +74,10 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
return mv;
}
private boolean isLoggedIn(HttpSession session) {
return session != null && session.getAttribute("userId") != null;
}
/// 접속기기 모바일 확인 함수
private boolean isMobileDevice(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");

View File

@ -204,6 +204,7 @@ public class UserController {
session.setAttribute("status", user.getStatus());
session.setAttribute("authProvider", identity.getProvider());
session.setAttribute("authIdentityId", identity.getId());
session.setAttribute("lastLoginAt", user.getLastLoginAt());
Map<String, Object> account = new LinkedHashMap<>();
account.put("id", user.getId());
@ -214,6 +215,7 @@ public class UserController {
account.put("status", user.getStatus());
account.put("authProvider", identity.getProvider());
account.put("authIdentityId", identity.getId());
account.put("lastLoginAt", user.getLastLoginAt());
session.setAttribute("account", account);
}

View File

@ -1,5 +1,11 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
<%
jakarta.servlet.http.HttpSession headerSession = request.getSession(false);
boolean headerLoggedIn = headerSession != null && headerSession.getAttribute("userId") != null;
String headerProfileHref = request.getContextPath() + (headerLoggedIn ? "/profile" : "/login");
String headerProfileLabel = headerLoggedIn ? "프로필" : "로그인";
%>
<style>
/* 기본(라이트) 헤더 — data-theme 없을 때도 동일 톤 */
.site-header {
@ -144,7 +150,7 @@
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
</svg>
</button>
<a class="site-header__profile" href="${pageContext.request.contextPath}/login" aria-label="프로필" title="프로필">
<a class="site-header__profile" href="<%= headerProfileHref %>" aria-label="<%= headerProfileLabel %>" title="<%= headerProfileLabel %>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>

View File

@ -284,9 +284,7 @@
body: new URLSearchParams(new FormData(form))
}).then(function (res) {
if (res.ok) {
openModal('로그인 완료', '로그인되었습니다.', '홈으로 이동', function () {
window.location.href = ctx + '/';
});
window.location.href = ctx + '/';
return null;
}

View File

@ -0,0 +1,286 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" language="java" %>
<%@ page import="org.springframework.web.util.HtmlUtils" %>
<%@ page import="java.util.Locale" %>
<!DOCTYPE html>
<%
String ctx = request.getContextPath();
String rawDisplayName = session.getAttribute("displayName") == null ? "" : String.valueOf(session.getAttribute("displayName"));
String rawEmail = session.getAttribute("email") == null ? "" : String.valueOf(session.getAttribute("email"));
String rawAvatarUrl = session.getAttribute("avatarUrl") == null ? "" : String.valueOf(session.getAttribute("avatarUrl"));
String rawRole = session.getAttribute("role") == null ? "" : String.valueOf(session.getAttribute("role"));
String rawStatus = session.getAttribute("status") == null ? "" : String.valueOf(session.getAttribute("status"));
String rawAuthProvider = session.getAttribute("authProvider") == null ? "" : String.valueOf(session.getAttribute("authProvider"));
String rawLastLoginAt = session.getAttribute("lastLoginAt") == null ? "" : String.valueOf(session.getAttribute("lastLoginAt"));
String initialSource = rawDisplayName.isBlank() ? rawEmail : rawDisplayName;
String initial = initialSource.isBlank() ? "B" : initialSource.substring(0, 1).toUpperCase(Locale.ROOT);
String displayName = HtmlUtils.htmlEscape(rawDisplayName.isBlank() ? "bibimbap 사용자" : rawDisplayName);
String email = HtmlUtils.htmlEscape(rawEmail.isBlank() ? "이메일 정보 없음" : rawEmail);
String avatarUrl = HtmlUtils.htmlEscape(rawAvatarUrl);
String role = HtmlUtils.htmlEscape(rawRole.isBlank() ? "USER" : rawRole);
String status = HtmlUtils.htmlEscape(rawStatus.isBlank() ? "ACTIVE" : rawStatus);
String authProvider = HtmlUtils.htmlEscape(rawAuthProvider.isBlank() ? "email" : rawAuthProvider);
String lastLoginAt = HtmlUtils.htmlEscape(rawLastLoginAt.isBlank() ? "이번 세션" : rawLastLoginAt);
String avatarInitial = HtmlUtils.htmlEscape(initial);
%>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
<title>프로필 | bibimbap</title>
<style>
html {
color-scheme: light;
--surface: #faf8f5;
--card-bg: #fff;
--text: #1a1a1a;
--text-muted: #5c5c5c;
--accent: #e8a54b;
--border: rgba(0, 0, 0, 0.08);
--card-shadow: rgba(0, 0, 0, 0.06);
--button-text: #1a1a1a;
}
html[data-theme="dark"] {
color-scheme: dark;
--surface: #121212;
--card-bg: #1e1e1e;
--text: #ece8e1;
--text-muted: #a39e96;
--border: rgba(255, 255, 255, 0.1);
--card-shadow: rgba(0, 0, 0, 0.35);
--button-text: #1a1a1a;
}
body {
margin: 0;
min-height: 100vh;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
background: var(--surface);
color: var(--text);
display: flex;
flex-direction: column;
}
.profile-main {
width: 100%;
max-width: 72rem;
box-sizing: border-box;
margin: 0 auto;
padding: 2rem max(1rem, env(safe-area-inset-left)) 3rem max(1rem, env(safe-area-inset-right));
flex: 1;
}
.profile-shell {
width: min(100%, 42rem);
margin: 0 auto;
}
.profile-heading {
margin: 0 0 1rem;
}
.profile-heading__eyebrow {
margin: 0 0 0.35rem;
font-size: 0.75rem;
font-weight: 800;
color: var(--accent);
letter-spacing: 0.02em;
}
.profile-heading__title {
margin: 0;
font-size: 1.75rem;
line-height: 1.2;
letter-spacing: 0;
}
.profile-panel {
border: 1px solid var(--border);
border-radius: 12px;
background: var(--card-bg);
box-shadow: 0 2px 8px var(--card-shadow);
overflow: hidden;
}
.profile-summary {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.profile-avatar {
flex-shrink: 0;
width: 4.5rem;
height: 4.5rem;
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: rgba(232, 165, 75, 0.18);
color: var(--text);
font-size: 1.75rem;
font-weight: 900;
border: 1px solid var(--border);
}
.profile-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.profile-summary__body {
min-width: 0;
}
.profile-summary__name {
margin: 0;
font-size: 1.35rem;
line-height: 1.25;
letter-spacing: 0;
word-break: break-word;
}
.profile-summary__email {
margin: 0.35rem 0 0;
color: var(--text-muted);
font-size: 0.9375rem;
word-break: break-word;
}
.profile-details {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
}
.profile-detail {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.profile-detail:nth-child(odd) {
border-right: 1px solid var(--border);
}
.profile-detail__label {
margin: 0 0 0.25rem;
font-size: 0.75rem;
font-weight: 800;
color: var(--text-muted);
}
.profile-detail__value {
margin: 0;
font-size: 0.9375rem;
font-weight: 700;
word-break: break-word;
}
.profile-actions {
display: flex;
flex-wrap: wrap;
gap: 0.625rem;
padding: 1.25rem 1.5rem 1.5rem;
}
.profile-button {
min-height: 2.75rem;
border: 1px solid var(--border);
border-radius: 10px;
padding: 0 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: var(--card-bg);
color: var(--text);
font-size: 0.9375rem;
font-weight: 800;
text-decoration: none;
cursor: pointer;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
.profile-button:hover {
border-color: rgba(232, 165, 75, 0.45);
box-shadow: 0 4px 12px var(--card-shadow);
}
.profile-button:active {
transform: scale(0.98);
}
.profile-button--primary {
border-color: transparent;
background: var(--accent);
color: var(--button-text);
}
.profile-actions form {
margin: 0;
}
@media (max-width: 560px) {
.profile-main {
padding-top: 1.5rem;
}
.profile-summary {
align-items: flex-start;
flex-direction: column;
padding: 1.25rem;
}
.profile-details {
grid-template-columns: 1fr;
}
.profile-detail {
padding: 1rem 1.25rem;
}
.profile-detail:nth-child(odd) {
border-right: none;
}
.profile-actions {
padding: 1.25rem;
}
.profile-button,
.profile-actions form {
width: 100%;
}
}
</style>
</head>
<body>
<jsp:include page="/WEB-INF/views/header.jsp"/>
<main class="profile-main">
<section class="profile-shell" aria-labelledby="profile-title">
<div class="profile-heading">
<p class="profile-heading__eyebrow">BIBIMBAP ACCOUNT</p>
<h1 class="profile-heading__title" id="profile-title">프로필</h1>
</div>
<div class="profile-panel">
<div class="profile-summary">
<div class="profile-avatar" aria-hidden="true">
<% if (!avatarUrl.isBlank()) { %>
<img src="<%= avatarUrl %>" alt="" width="96" height="96" />
<% } else { %>
<span><%= avatarInitial %></span>
<% } %>
</div>
<div class="profile-summary__body">
<h2 class="profile-summary__name"><%= displayName %></h2>
<p class="profile-summary__email"><%= email %></p>
</div>
</div>
<div class="profile-details" aria-label="계정 정보">
<div class="profile-detail">
<p class="profile-detail__label">권한</p>
<p class="profile-detail__value"><%= role %></p>
</div>
<div class="profile-detail">
<p class="profile-detail__label">상태</p>
<p class="profile-detail__value"><%= status %></p>
</div>
<div class="profile-detail">
<p class="profile-detail__label">로그인 방식</p>
<p class="profile-detail__value"><%= authProvider %></p>
</div>
<div class="profile-detail">
<p class="profile-detail__label">최근 로그인</p>
<p class="profile-detail__value"><%= lastLoginAt %></p>
</div>
</div>
<div class="profile-actions">
<a class="profile-button profile-button--primary" href="<%= ctx %>/">홈으로 이동</a>
<form action="<%= ctx %>/logout" method="post">
<button class="profile-button" type="submit">로그아웃</button>
</form>
</div>
</div>
</section>
</main>
<jsp:include page="/WEB-INF/views/footer.jsp"/>
</body>
</html>