네비게이션 바를 생성 및 디자인 하고 기능을 구현하기로 한다.
[개발 목표]
- 타임리프 레이아웃 적용
- 네비게이션 바 생성/디자인 및 구현
1. 타임리프 레이아웃 적용
build.gradle
타임리프 레이아웃 기능을 이용하기 위해 일단 build.gradle에 아래와 같이 추가해준다.
이걸 해야 타임리프 레이아웃이 적용됨. 필수!
일단 나는 각종 CSS를 담아두는 config / Header / Body로 나뉘어진 비교적 엄청나게 간단한 레이아웃을 사용할 예정
layout.html
<!DOCTYPE html>
<html lagn="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<html th:fragment="layout">
<head>
<!--공통 JS / CSS 영역을 관리하는 환경 영역-->
<th:block th:replace="~{common/config::config}"></th:block>
</head>
<body>
<!-- header -->
<th:block th:replace="~{common/header::header}"></th:block>
<!-- content -->
<th:block layout:fragment="content"></th:block>
</body>
</html>
- 그래서 레이아웃을 이런식으로 구성했다.
config.html
<th:block th:fragment="config">
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<script th:src="@{/js/common/header.js}"></script>
</th:block>
- 부트스트랩 적용에 필요한 CSS와 header에 필요한 스크립트를 미리 넣어두었다. Header 스크립트는 매 화면에서 필요하기 때문에. 걍 여기 넣어버림!
header.html
<header class="p-3 text-bg-dark" style="background-color: black;" th:fragment="header">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap"><use xlink:href="#bootstrap"></use></svg>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a class="nav-link px-2 text-secondary" >OBRS</a></li>
<li><a class="nav-link px-2 text-white" name="allBookList">모든책</a></li>
<li><a class="nav-link px-2 text-white" name="myBooks">나의책</a></li>
<li><a class="nav-link px-2 text-white" name="rentalBooks">빌린책</a></li>
<li><a class="nav-link px-2 text-white" name="myInfo">내정보</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
<div id="loginInfo">
<span class="navbar-text" id="headerLoginName" style="color: white;"></span>
<span style="color: white;"> ( </span>
<span class="navbar-text" id="headerLoginId" style="color: white;"></span>
<span style="color: white;"> ) 님</span>
</div>
</form>
<div class="text-end">
<button type="button" id="loginBtn" class="btn btn-outline-light me-2" th:onclick="|location.href='@{/login}'|">Login</button>
<button type="button" id="logoutBtn" class="btn btn-outline-light me-2" th:onclick="|location.href='@{/logout}'|">Log-out</button>
<button type="button" id="joinBtn" class="btn btn-warning" th:onclick="|location.href='@{/member/join}'|">Sign-up</button>
</div>
</div>
</div>
</header>
- 헤더는 다소 복잡해 보이지만 부트스트랩을 사용하다보니 그래보이는 것일 뿐.
- 헤더에서 바로 메뉴명을 클릭 했을 때 해당 페이지로 넘어가지 않고 스크립트에서 처리하도록 했다. (로그인 여부 체크 때문에)
- loginInfo 존을 만들어서 로그인 시 '이름 (아이디) 님' 형식으로 보이게끔 만들어 두었다.
- 나중에는 이 메뉴도 시스템화 해서 추가/등록 기능을 만드는 것도 해보고 싶지만, 너무 작은 프로젝트라서 그럴 필요까지 없어서 안함.
- 헤더에서 사용된 기능들은 하단에 자세히 서술할 것임.
레이아웃에 사용되는 파일 구조는 이러하다.
이제 이 구조를 적용하려는 페이지마다 상단에 레이아웃을 적용한다. 아래는 Member.html 의 예시임.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{common/layout}"
layout:fragment="content">
<head>
<title>회원 정보</title>
</head>
<body>
//// 생략 /////
</body>
</html>
페이지마다 레이아웃을 적용하면 아래와 같은 형태로 구현됨.
헤더 디자인이 계속 거슬렸는데 드디어 바꿨다! 그냥 간단한 디자인이지만 너무 만족중
2. 네비게이션 바 생성/디자인 및 구현
먼저 구현하려는 네비게이션 형태는 아래와 같다.
원래 아래와 같은 네비게이션 형태를 만들려고 했는데 적용해보니
생각보다 내가 컨트롤하기 쉽지 않고 생각보다 깔끔하지 않았으며
너무 자바스크립트, CSS에만 치중하는 느낌이 들어서 위처럼 변경하기로 했다.
header.js
html은 위에 있으니 자바스크립트 구현한 부분만 가져왔음.
function showButton(btn){
btn.style.display = '';
}
function hideButton(btn){
btn.style.display = 'none';
}
function goLogin(){
alert("로그인이 필요한 서비스입니다.");
window.location.href = "/login";
}
function menuClick(){
let menuItems = document.querySelectorAll('.nav-link');
menuItems.forEach(function(item) {
item.addEventListener('click', function (event) {
let menuItemName = event.target.getAttribute('name');
let loginId = document.getElementById('headerLoginId').textContent;
switch (menuItemName) {
case 'allBookList':
window.location.href = "/book";
break;
case 'myBooks':
if (loginId === '') {
goLogin();
return;
} else {
window.location.href = "/book/" + loginId + "/booksByAuthorId";
break;
}
case 'rentalBooks':
if (loginId === '') {
goLogin();
return;
} else {
window.location.href = "/book/" + loginId + "/booksByBookRentalId";
break;
}
case 'myInfo':
if (loginId === '') {
goLogin();
return;
} else {
window.location.href = "/member/" + loginId;
break;
}
}
});
});
}
function getLoginInfo() {
let loginButton = document.getElementById('loginBtn');
let logoutButton = document.getElementById('logoutBtn');
let joinButton = document.getElementById('joinBtn');
fetch('/findLoginInfo')
.then(response => {
if (!response.ok) {
throw new Error('요청 실패');
}
return response.json();
})
.then(data => {
if(data.loginId == null && data.loginName == null){
document.getElementById("loginInfo").style.display = 'none';
showButton(loginButton);
hideButton(logoutButton);
} else {
document.getElementById("loginInfo").style.display = '';
document.getElementById("headerLoginId").textContent = data.loginId;
document.getElementById("headerLoginName").textContent = data.loginName;
showButton(logoutButton);
hideButton(loginButton);
hideButton(joinButton);
}
console.log(data);
})
.catch(error => {
console.error('에러:', error);
});
}
document.addEventListener('DOMContentLoaded', function () {
getLoginInfo();
menuClick();
});
생각보다 내용이 많으니 톧아보기로 한다.
1) 버튼 숨기기 / 보이기 기능
function showButton(btn){
btn.style.display = '';
}
function hideButton(btn){
btn.style.display = 'none';
}
- 로그인 여부에 따라서 [로그인] / [로그아웃] / [회원가입] 버튼이 보이고 숨겨야 하는 부분이 반복적으로 구현되어 있어서 아예 공통 함수를 만들어 사용했다.
- display = 'block' 으로 했었는데, CSS가 깨지는 부분이 있어서 '' 빈칸으로 아무것도 주지 않았다.
2) 로그인 페이지로 이동
function goLogin(){
alert("로그인이 필요한 서비스입니다.");
window.location.href = "/login";
}
- [모든책] 을 제외한 [나의책], [빌린책], [내정보] 의 경우 모두 '로그인'이 필요한 서비스인데, 로그인이 된사람만 메뉴를 보여주는 방식이 아니라 메뉴를 비회원/회원 동일하게 보여주지만 로그인이 필요한 메뉴인 경우 '로그인이 필요한 서비스입니다' 로 알려주고 로그인 창으로 이동하는 방식을 택했다. 내가 타 사이트를 이용했을 때 이런 방식이 더 친절하고 편하다고 느껴서이다.
- 그래서 이 부분도 꽤 반복적으로 사용이 되서 공통 함수로 만들어 호출하는 방식을 선택했다.
3) [회원가입], [로그인], [로그아웃] 버튼 제어
function getLoginInfo() {
let loginButton = document.getElementById('loginBtn');
let logoutButton = document.getElementById('logoutBtn');
let joinButton = document.getElementById('joinBtn');
fetch('/findLoginInfo')
.then(response => {
if (!response.ok) {
throw new Error('요청 실패');
}
return response.json();
})
.then(data => {
if(data.loginId == null && data.loginName == null){
document.getElementById("loginInfo").style.display = 'none';
showButton(loginButton);
hideButton(logoutButton);
} else {
document.getElementById("loginInfo").style.display = '';
document.getElementById("headerLoginId").textContent = data.loginId;
document.getElementById("headerLoginName").textContent = data.loginName;
showButton(logoutButton);
hideButton(loginButton);
hideButton(joinButton);
}
console.log(data);
})
.catch(error => {
console.error('에러:', error);
});
}
[로그인], [로그아웃], [회원가입] 버튼의 경우 로그인/비로그인에 따라 보여지는 버튼들이 각자 다르다. 그래서 애초에 Header를 로드할 때 세션에 저장된 로그인 정보를 가져와서 그 값이 Null이 아닐 때 즉, 로그인이 됐을 때는 상단 헤더에 로그인한 회원 아이디와 이름이 보여지게끔 구현했다.
30-1) LoginController.java
맨 처음 구현한 소스는 아래와 같았는데, 버그가 발생했다.
회원수정에서 닉네임을 변경하면 헤더에도 변경된 닉네임이 바로 반영되도록 구현을 했는데 안되는거다. 아무리해도 안됨. 디버그를 걸어보면 회원수정 쪽에서는 수정이 되는데 회원 정보 조회할 때 header에서 다시 이전 닉네임으로 변경되버리는 거다. Js쪽에서 해결해보려고 엄청 노력을 했는데 사실은 그게 문제가 아니였다.
@GetMapping("/findLoginInfo")
@ResponseBody
public Map<String, String> findLoginInfo(HttpServletRequest request, Model model){
String loginId = (String) request.getSession().getAttribute("loginId");
String loginName = (String) request.getSession().getAttribute("loginName");
Map<String, String> loginInfo = new HashMap<>();
loginInfo.put("loginId", loginId);
loginInfo.put("loginName", loginName);
model.addAttribute("loginInfo", loginInfo);
return loginInfo;
}
여기서 문제가 발생한다. 회원정보를 수정하긴 했지만 세션에는 처음 로그인할 때의 아이디와 닉네임만 저장되어 있다. 그 세션을 그대로 가져오니까 이전 닉네임으로 자꾸 돌아가는 거였음. 그래서 변경된 소스에서는 세션을 다시 수정해주는 방식으로 변경했다. 진짜 보면 별거 아닌데.....이거 고치는데 엄청 고생했다.
그렇게해서 완성한 두번째 코드!
근데 또 문제가 발생했다. 로그인을 안한경우에는 member가 Null을 반환해서 Member.getmemberName() 메소드에서 에러가 나는 것이다. 이건 비교적 콘솔에러로 빨리 파악했고 수정했다.
@GetMapping("/findLoginInfo")
@ResponseBody
public Map<String, String> findLoginInfo(HttpServletRequest request, Model model){
String loginId = (String) request.getSession().getAttribute("loginId");
Member member = memberService.findById(loginId);
request.setAttribute("loginName", member.getMemberName());
Map<String, String> loginInfo = new HashMap<>();
loginInfo.put("loginId", loginId);
loginInfo.put("loginName", member.getMemberName());
model.addAttribute("loginInfo", loginInfo);
return loginInfo;
}
[최종_최종_최종 코드]
@GetMapping("/findLoginInfo")
@ResponseBody
public Map<String, String> findLoginInfo(HttpServletRequest request, Model model){
String loginId = (String) request.getSession().getAttribute("loginId");
Member member = memberService.findById(loginId);
String loginName;
if (member == null){
loginName = null;
} else {
loginName = member.getMemberName();
}
request.setAttribute("loginName", loginName);
Map<String, String> loginInfo = new HashMap<>();
loginInfo.put("loginId", loginId);
loginInfo.put("loginName", loginName);
model.addAttribute("loginInfo", loginInfo);
return loginInfo;
}
- header.js에서 호출된 findLoginInfo컨트롤러는 LoginController에 구현되어 있는데, 로그인 시 생성된 세션을 가지고와서 기존에 만들어져 있던 Memberservice.findById로 데이터베이스에 변경된 회원의 이름을 조회하여 기존에 있는 LoginName을 수정해준다.
- 그렇게 받아온 로그인 정보로 자바스크립트에서 버튼 보이기/숨기기를 제어함.
- 비회원일 경우 Member객체가 Null을 반환함으로 Null을 반환할경우 loginName에 Null 값을 담아준다.
4) 메뉴에 따른 페이지 이동 구현
function menuClick(){
let menuItems = document.querySelectorAll('.nav-link');
menuItems.forEach(function(item) {
item.addEventListener('click', function (event) {
let menuItemName = event.target.getAttribute('name');
let loginId = document.getElementById('headerLoginId').textContent;
switch (menuItemName) {
case 'allBookList':
window.location.href = "/book";
break;
case 'myBooks':
if (loginId === '') {
goLogin();
return;
} else {
window.location.href = "/book/" + loginId + "/booksByAuthorId";
break;
}
case 'rentalBooks':
if (loginId === '') {
goLogin();
return;
} else {
window.location.href = "/book/" + loginId + "/booksByBookRentalId";
break;
}
case 'myInfo':
if (loginId === '') {
goLogin();
return;
} else {
window.location.href = "/member/" + loginId;
break;
}
}
});
});
}
- 각 메뉴에 대한 페이지 이동을 Html에서 하지 않고 자바스크립트로 처리했다. 훨씬 복잡해졌지만, 로그인 여부를 체크해야 해서 어쩔수 없이 자바스크립트에서 기능을 다로 구현했다.
- 각 메뉴마다 Name값을 부여해 그 값에 따라서 각각의 컨트롤러를 호출하는 방식으로 구현하였다. 로그인 여부를 체크하는 것 외에는 별다른 건 없다.
- 메뉴가 많아진다면 이 방법은 어려울 것같다. 메뉴를 시스템화 하는 형태가 필요할 것이라는 생각이 들지만 지금은 메뉴가 많지 않기 때문에, 나쁘지 않은 방법이라 생각 중.
[끝내는 말]
Header 온전히 만져본게 얼마만이던가. 가장 핵심이 되는 부분이고 꼭 되야 하는 기능들이 많은데 처음해보거나 낯선 부분들이 있어서 버벅이고 오래 걸렸다. 그래도 한번 완성하고 나니 감은 확실이 온다. 다음엔 더 잘해야지.
'💻 뚝딱뚝딱 > 팀내도서대여시스템(OBRS)' 카테고리의 다른 글
[개발일지#006] 책 대여 / 반납 기능 구현 (0) | 2024.04.07 |
---|---|
[개발일지#005] 데이터베이스 수정작업 그리고 그에 따른 XML 수정 (0) | 2024.04.06 |
[개발일지#003] 책 등록 / 책 정보수정 / 책 목록조회 구현 (0) | 2024.04.02 |
[개발일지#002] 회원 가입 / 회원조회(단건) / 회원정보수정 구현 (1) | 2024.04.01 |
[개발일지#001] 데이터베이스를 생성하고 스프링부트에 Mybatis 설정하기 (회원목록 만들기) (0) | 2024.03.28 |