
✔️webapp(WEB-INF(views(common))) → header.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<link rel="stylesheet" href="/resources/css/main-style.css">
<!-- font awesome 라이브러리 추가 + key 등록 -->
<script src="https://kit.fontawesome.com/f8b69bd1ba.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/resources/css/header-style.css">
<header>
<section>
<!-- 클릭 시 메인 페이지로 이동하는 로고 -->
<a href="/">
<img src="/resources/images/logo.jpg" alt="로고" id="homeLogo">
</a>
</section>
<section class="search-content">
<!-- 검색어 입력할 수 있는 요소 배치 -->
<article class="search-area">
<!--
action : 내부 input에 작성된 값을 제출할 경로/주소
method : 어떤 방식으로 제출할지 지정
- GET : input태그 값을 주소에 담아서 제출(주소에 보임)
- POST : input태그 값을 주소에 담지 않고 제출(주소에 안보임)
-> HTTP Body에 담아서 제출
-->
<form action="/board/1" method="GET">
<fieldset> <!-- form태그 내 영역 구분 -->
<!--
input의 name 속성 == 제출 시 key
input에 입력된 내용 == 제출 시 value
autocomplete="off" : 브라우저 제공 자동완성 off
-->
<input type="search" name="query" id="query"
placeholder="검색어를 입력해주세요."
autocomplete="off" value="${param.query}">
<%-- 제목 검색 --%>
<input type="hidden" name="key" value="t">
<!-- 검색 버튼 -->
<!-- button type="submit" 이 기본값 -->
<button id="searchBtn" class="fa-solid fa-magnifying-glass"></button>
</fieldset>
</form>
<%-- 검색어 자동완성이 보여질 구역 --%>
<!-- --><ul id="autoComplete"></ul>
<!-- board/1/3116?cp=1 -->
</article>
</section>
<section></section>
<%-- 우측 상단 드롭다운 메뉴 --%>
<div class="header-top-menu">
<c:choose>
<c:when test="${empty loginMember}">
<%-- 로그인 X --%>
<a href="/">메인 페이지</a> | <a href="/member/login">로그인</a>
</c:when>
<c:otherwise>
<%-- 로그인 O --%>
<label for="headerMenuToggle">
${loginMember.memberNickname} <i class="fa-solid fa-caret-down"></i>
</label>
<input type="checkbox" id="headerMenuToggle">
<div class="header-menu">
<a href="/myPage/info">내정보</a>
<a href="/member/logout">로그아웃</a>
</div>
</c:otherwise>
</c:choose>
</div>
</header>
<nav>
<ul>
<%--
<li><a href="#">공지사항</a></li>
<li><a href="#">자유 게시판</a></li>
<li><a href="#">질문 게시판</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">1:1문의</a></li>
--%>
<%-- interceptor를 이용해서 조회된 boardTypeList를 application scope에서 얻어와서 화면에 출력 --%>
<c:forEach var="boardType" items="${boardTypeList}">
<li>
<a href="/board/${boardType.BOARD_CODE}">${boardType.BOARD_NAME}</a>
</li>
</c:forEach>
<%-- 로그인 했을 때 채팅 보여짐 --%>
<c:if test="${!empty loginMember}">
<li><a href="/chatting">채팅</a></li>
</c:if>
</ul>
</nav>
<script>
const autoComplete = document.getElementById("autoComplete");
const query = document.getElementById("query");
query.addEventListener("input", () => {
if (query.value.trim() == "") {
autoComplete.hidden = true;
return;
}
fetch("/board/searchAutoComplete", {
method: "POST",
headers: { "Content-Type": "application/text" },
body: query.value // JS 객체 -> JSON 파싱
})
.then(response => response.json())
.then(data => {
autoComplete.innerHTML = "";
autoComplete.hidden = false;
console.log(data)
if (data.length > 0) {
data.forEach(item => {
const li = document.createElement("li");
li.classList.add("autocomplete-item");
const a = document.createElement("a");
a.innerText = item.boardName + " - " + item.boardTitle;
a.setAttribute("href", `/board/\${item.boardCode}/\${item.boardNo}`);
li.append(a);
autoComplete.append(li);
});
} else {
const div = document.createElement("div");
div.classList.add("autocomplete-item");
div.innerText = "검색 결과가 없습니다.";
autoComplete.appendChild(div);
}
})
.catch(e => {
console.log("예외 발생");
console.log(e);
})
});
document.addEventListener("click", (event) => {
const isClickInsideInput = query.contains(event.target);
const isClickInsideAutoComplete = autoComplete.contains(event.target);
if (!isClickInsideInput && !isClickInsideAutoComplete) {
autoComplete.hidden = true;
}
});
</script>
위치를 고정적으로 하기 위해서는 css를 absoulte, 부모는 relative 넣기
header에 메인페이지 , 로그인 창을 만들어주기
<a href=”/member/logout”>로그아웃</a>
로그아웃은 이미 기능이 있으니깐 불러온다
<label for="headerMenuToggle"> ${loginMember.memberNickname} <i class="fa-solid fa-caret-down"></i> </label>
닉네임 대신으로 유저일으로 표현하고싶다
${loginMember.memberNickname}
✔️webapp(resources(css)) → main-style.css
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
/* div, main, section, header, footer, article, nav, aside, form
{ border : 1px solid black; } */
/* 다 만들고 삭제 */
main{
width: 1140px;
margin : auto;
/* min-height : 요소의 최소 높이
-> 내부 요소가 없어도 최소 높이 유지
-> 내부 요소가 지정된 크기를 초과하면
그에 맞에 늘어남
*/
min-height: 500px;
}
/* header */
header{
height: 200px;
display: flex;
flex-direction: row;
}
header > section:nth-child(1){ width: 15%;}
header > section:nth-child(2){ width: 70%;}
header > section:nth-child(3){ width: 15%; }
/* 로고 */
header > section:first-child{
display: flex;
justify-content: center;
align-items: center;
}
#homeLogo{
width: 120px;
height: auto; /* 이미지 기본 비율에 맞게 자동 지정 */
}
/* 검색 영역 */
header > section:nth-child(2){
display: flex;
justify-content: center;
align-items: center;
}
.search-area{
width: 500px;
}
.search-area fieldset{
border: 2px solid #455ba8;
padding : 2px;
border-radius: 5px;
display: flex;
}
/* 검색 창 */
#query{
border : none; /* 테두리 없애기 */
/* outline :
포커스가 맞춰진 input의 테두리 */
/* outline : none; */
outline : 0;
font-size: 18px;
font-weight: bold;
padding : 10px;
width: 92%;
}
/* 검색 버튼 */
#searchBtn{
width: 8%;
cursor: pointer;
border : none;
font-size: 1.2em;
color : #455ba8;
background-color: white;
}
/* ---------------------------- */
/* 헤더 오른쪽 상단 메뉴 */
header{
position: relative;
}
.header-top-menu{
position: absolute;
right: 10px;
top: 5px;
}
.header-top-menu *{
font-size: 15px;
font-weight: bold;
}
/* 헤더 메뉴 체크 박스 */
#headerMenuToggle{display: none;}
/* 오른쪽 상단 닉네임 부분 */
label[for='headerMenuToggle']{cursor: pointer;}
label[for='headerMenuToggle']:hover{text-decoration: underline;}
/* 체크박스 다음 형제 요소 div */
#headerMenuToggle + div{display: none;}
/* 체크된 #headerMenuToggle의 다음 형제 요소 div */
#headerMenuToggle:checked + div{display: block;}
/* 닉네임 클릭 시 보이는 메뉴 */
.header-menu{
position: absolute;
width: 100px;
height: 80px;
right: 0;
margin-top: 5px;
border: 1px solid black;
}
.header-menu > a{
display: flex;
height: 50%;
justify-content: center;
align-items: center;
}
.header-menu > a:first-child{
border-bottom: 1px solid #ccc;
}
.header-menu > a:hover{
background-color: #455ba8;
color: white;
}
/* nav */
nav{
height: 50px;
position: sticky;
/* 화면 밖으로 벗어났을 때
지정된 위치에 고정*/
top : 0; /* 최상단에 붙임 */
background-color: white;
border-bottom: 2px solid black;
z-index: 1000;
}
nav > ul{
display: flex;
/* li 태그 앞에 기호 삭제 */
list-style: none;
height: 100%;
}
nav li{
flex : 0 1 150px;
/* grow shrink basis */
}
nav a{
text-decoration: none;
color : black;
font-size: 18px;
font-weight: bold;
padding : 11px 0;
display: block;
text-align: center;
border-radius: 5px;
transition-duration: 0.1s;
height: 100%;
}
nav a:hover{
color : white;
background-color: #455ba8;
}
/* content */
.content{
height: 800px;
display: flex;
/* flex 지정 시 기본값 */
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
}
.content-1{
flex-basis: 70%;
padding : 20px;
}
.content-2{ flex-basis: 30%;}
.content-1 > *{
margin-bottom: 20px;
}
.content-1 > pre{
font-size: 1rem;
font-family: '고딕';
}
/* 로그인 영역 */
.content-2 > form{
height: 150px;
padding : 10px;
margin-top : 10px;
display: flex;
flex-direction: column;
justify-content: center;
}
.id-pw-area{
border: 1px solid #ccc;
display: flex;
/* 중심축 방향 크기 지정 */
flex-basis: 60%;
}
/* id/pw 입력 영역*/
.id-pw-area > section:first-child{
width: 75%;
display: flex;
flex-direction: column;
}
.id-pw-area > section:first-child > input{
border : 0;
border-right : 1px solid #ccc;
flex-basis: 50%;
outline: none;
padding: 5px;
}
/* id input 태그 */
input[name='inputId']{
border-bottom : 1px solid #ccc !important;
}
/* id/pw에 초점이 맞춰졌을 때 */
.id-pw-area > section:first-child > input:focus{
border: 2px solid #455ba8 !important;
}
/* 로그인 버튼 영역 */
.id-pw-area > section:last-child{
width: 25%;
display: flex;
justify-content: center;
}
/* 로그인 버튼 */
.id-pw-area > section:last-child > button{
width: 100%;
background-color: white;
cursor: pointer;
border : none;
}
/* 로그인 버튼에 마우스가 올라갔을 때 */
.id-pw-area > section:last-child > button:hover{
background-color: #455ba8;
color: white;
}
/* 아이디 저장 label */
.content-2 label{
margin-top: 5px;
font-size: 14px;
display: flex;
padding: 5px 0;
}
input[name='saveId']{
margin-right: 5px;
}
/* 회원가입, ID/PW 찾기 영역 */
.signup-find-area{
margin-top: 10px;
padding-left: 5px;
}
.signup-find-area > a{
color : black;
text-decoration: none;
font-size: 14px;
}
.signup-find-area > span{
padding: 0 10px;
}
/* footer */
footer{
height: 200px;
background-color: #a3add342;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
footer > p{
font-weight: bold;
margin : 16px 0;
}
footer > article > *{
font-size: 14px;
}
footer a{
color:black;
text-decoration: none;
}
footer span{
padding : 0 10px;
}
/* ---------------------------------------------------------------------------- */
/* 로그인 성공 시 화면 */
.login-area{
height: 120px;
padding: 30px 10px;
margin: 10px 0;
border: 1px solid black;
border-radius: 10px;
display: flex;
align-items: center;
}
/* 프로필 이미지 */
#memberProfile{
width: 64px;
height: 64px;
border-radius: 50%;
}
/* 프로필 이미지를 감싸고 있는 a태그 */
.login-area > a{
display: block;
width: 70px;
height: 70px;
border: 3px solid #ddd;
border-radius: 50%;
}
/* 회원 정보 + 로그아웃 버튼 영역 설정 */
.my-info{
width: 100%;
height: 100%;
margin-left: 20px;
}
.my-info > div{
display: flex;
justify-content: space-between;
}
a{
text-decoration: none;
color : black;
}
/* 닉네임 */
#nickname{
color : black;
font-size: 20px;
font-weight: bold;
letter-spacing: -3px;
}
/* 로그아웃 버튼 */
#logoutBtn{
color : gray;
font-size: 14px;
font-weight: bold;
border: 2px solid gray;
padding : 3px 5px;
border-radius: 10px;
}
#logoutBtn:hover{
color : white;
background-color: gray;
}
#logoutBtn:active{
transform: scale(0.8);
}
/* 내정보 - 이메일 */
.my-info > p{
margin: 0;
color : gray;
}
#autoDisplayList{
display: none;
}
/* 오른쪽 상단 닉네임 부분 */ label[for='headerMenuToggle']{cursor: pointer;} label[for='headerMenuToggle']:hover{text-decoration: underline;} /* 체크박스 다음 형제 요소 div */ #headerMenuToggle + div{display: none;} /* 체크된 #headerMenuToggle의 다음 형제 요소 div */ #headerMenuToggle:checked + div{display: block;}따로 js 처리 귀찮으니깐 checked 넣기
근데 MemberController에 로그인을 @PostMapping으로 함
그래서 get방식으로 줘야하는데 문제가 생김
따로 방식이 다르니깐 만들 수 있음
✔️ → login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>로그인 페이지</title>
<link rel="stylesheet" href="/resources/css/member/login.css">
<script src="https://kit.fontawesome.com/f7459b8054.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<section class="logo-area">
<a href="/">
<img src="/resources/images/logo.jpg">
</a>
</section>
<form action="/member/login" method="post">
<section class="input-box">
<input type="text" name="memberEmail" placeholder="Email" value="${cookie.saveId.value}"/>
</section>
<section class="input-box">
<input type="password" name="memberPw" placeholder="Password"/>
</section>
<button class="login-btn">Login</button>
<%-- 쿠키에 saveId가 있는 경우--%>
<c:if test="${ !empty cookie.saveId.value}">
<%-- chk 변수 생성(page scope)--%>
<c:set var="chk" value="checked"/>
</c:if>
<div class="saveId-area">
<input type="checkbox" name="saveId" id="saveId" ${chk}>
<label for="saveId"><i class="fas fa-check"></i>아이디 저장</label>
</div>
<p class="text-area">
<a href="#">회원가입</a>
|
<a href="#">ID/PW 찾기</a>
</p>
</form>
</main>
</body>
</html>
✔️ → login.css
* {
box-sizing: border-box;
margin : 0;
padding : 0;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
margin: 0;
}
main {
padding: 20px 40px;
border-radius: 5px;
}
main .logo-area {
text-align: center;
margin-bottom: 30px;
}
.logo-area img{
width: 150px;
}
.input-box {
position: relative;
margin: 20px 0 40px;
width: 300px;
}
.input-box input {
border: 0;
border-bottom: 2px solid black;
display: block;
width: 100%;
padding: 15px 0;
font-size: 18px;
}
.input-box input:focus{
outline: 0;
border-bottom-color: lightblue;
}
.login-btn {
cursor: pointer;
display: inline-block;
width: 100%;
background: #455ba8;
color : white;
padding: 15px;
font-size: 18px;
border: 0;
border-radius: 5px;
}
.login-btn:focus {
outline: 0;
}
.login-btn:active {
transform: scale(0.98);
}
.saveId-area{
margin-top: 10px;
padding: 10px 0;
border-bottom: 2px solid #ccc;
display: flex;
align-items: center;
}
.saveId-area > input{
display: none;
}
#saveId+label{
display: flex;
cursor: pointer;
}
#saveId+label > i{
display: flex;
justify-content: center;
align-items: center;
width: 25px;
height: 25px;
border: 1px solid #ccc;
border-radius: 50%;
margin-right: 10px;
color : #ccc;
font-size: 1.05em;
}
.saveId-area > input:checked + label > i{
background-color: #455ba8;
border-color: #455ba8;
color: white !important;
}
.text-area {
margin-top: 10px;
text-align: center;
}
.text-area a{
text-decoration: none;
color: black;
margin: 0 20px;
}
✔️webapp(WEB-INF(views(common))) → main.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판 프로젝트</title>
<link rel="stylesheet" href="/resources/css/main-style.css">
</head>
<body>
<main>
<%-- header.jsp 추가(포함) --%>
<%--
<jsp:include page="jsp파일경로" />
- JSP 액션 태그 (jsp에 기본 내장된 태그)
- 다른 jsp 파일의 코드를 현재 위치에 추가(포함)
- jsp 파일 경로는 webapp 폴더를 기준으로 작성
--%>
<jsp:include page="/WEB-INF/views/common/header.jsp" />
<section class="content">
<section class="content-1">
<h3>로그인된 회원 정보</h3>
${sessionScope.loginMember}
<h3>닉네임이 일치하는 회원의 전화번호 조회</h3>
<input type="text" id="inputNickname">
<button id="btn1">조회</button>
<h4 id="result1"></h4>
<hr>
<h3>이메일을 입력받아 일치하는 회원의 정보를 조회</h3>
email : <input type="text" id="inputEmail">
<button id="btn2">조회</button>
<ul id="result2">
</ul>
<hr>
<h3>이메일이 일부라도 일치하는 모든 회원 조회</h3>
검색어 : <input type="text" id="input">
<button id="btn3">조회</button>
<table border="1" style="border-collapse: collapse;">
<thead>
<tr>
<th>회원번호</th>
<th>이메일</th>
<th>닉네임</th>
</tr>
</thead>
<tbody id="result3">
<tr>
<td>1</td>
<td>user01@kh.or.kr</td>
<td>유저일</td>
</tr>
</tbody>
</table>
</section>
<section class="content-2">
<c:choose>
<%--
c:choose 내부에는 c:when, c:otherwise, jsp 주석만 작성 가능
c:when, c:otherwise 내부에는 다른 코드 작성 가능!
--%>
<%-- EL empty 연산자 : 비어 있거나 null 이면 true --%>
<c:when test="${empty sessionScope.loginMember}">
<!-- method="POST" : 제출 되는 값이 주소에 안보임 -->
<form action="/member/login" method="POST" id="loginFrm">
<fieldset class="id-pw-area">
<section>
<input type="text" name="memberEmail" placeholder="이메일" autocomplete="off"
value="${cookie.saveId.value}" id="memberEmail">
<input type="password" name="memberPw" placeholder="비밀번호" id="memberPw">
</section>
<section>
<button>로그인</button>
</section>
</fieldset>
<label>
<%-- <c:if test="${empty cookie.saveId.value}">
<input type="checkbox" name="saveId"> 아이디 저장
</c:if>
<c:if test="${!empty cookie.saveId.value}">
<input type="checkbox" name="saveId" checked> 아이디 저장
</c:if> --%>
<c:if test="${!empty cookie.saveId.value}">
<%-- 쿠키에 저장된 이메일이 있으면 save 변수 선언
-> page scope(페이지 내에서 사용 가능, if문 끝나도 가능!)
--%>
<c:set var="save" value="checked" />
</c:if>
<input type="checkbox" name="saveId" ${save}> 아이디 저장
</label>
<article class="signup-find-area">
<a href="/member/signUp">회원가입</a>
<span>|</span>
<a href="#">ID/PW 찾기</a>
</article>
</form>
</c:when>
<%-- 로그인 되었을 때 --%>
<c:otherwise>
<article class="login-area">
<a href="/myPage/profile">
<%-- 프로필 이미지가 없으면 기본 이미지 --%>
<c:if test="${empty loginMember.profileImage}" >
<img src="/resources/images/user.png" id="memberProfile">
</c:if>
<%-- 프로필 이미지가 있으면 있는 이미지 --%>
<c:if test="${!empty loginMember.profileImage}" >
<img src="${loginMember.profileImage}" id="memberProfile">
</c:if>
</a>
<div class="my-info">
<div>
<a href="/myPage/info" id="nickname">${sessionScope.loginMember.memberNickname}</a>
<a href="/member/logout" id="logoutBtn">로그아웃</a>
</div>
<p>${loginMember.memberEmail}</p>
</div>
</article>
</c:otherwise>
</c:choose>
</section>
</section>
</main>
<%-- footer --%>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<%-- SockJS 추가 --%>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<!-- main.js 추가 -->
<script src="/resources/js/main.js"></script>
</body>
</html>
<!-- main.js 추가 --> <script src="/resources/js/main.js"></script>
✔️webapp(resources(js)) → main.js
const loginFrm = document.getElementById("loginFrm");
const memberEmail = document.querySelector("#loginFrm input[name='memberEmail']");
const memberPw = document.querySelector("#loginFrm input[name='memberPw']");
if(loginFrm != null){
// 로그인 시도를 할 때
loginFrm.addEventListener("submit", e => {
// alert("로그인");
// form 태그 기본 이벤트 제거
// e.preventDefault();
// 이메일이 입력되지 않은 경우
if(memberEmail.value.trim().length == 0){
alert("이메일을 입력해 주세요.");
// 잘못 입력된 값(공백) 제거
memberEmail.value = "";
// 이메일 input 태그에 초점 맞춤
memberEmail.focus();
// 제출 못 하게 하기
e.preventDefault();
return;
}
// 비밀번호가 입력되지 않은 경우
if(memberPw.value.trim().length == 0){
alert("비밀번호를 입력해 주세요.");
// 잘못 입력된 값(공백) 제거
memberPw.value = "";
// 이메일 input 태그에 초점 맞춤
memberPw.focus();
// 제출 못 하게 하기
e.preventDefault();
return;
}
})
}
e라고 쓰면 event
근데 로그인이 main 화면에만 있는 게 아니잖아!
login.jsp에도 있잖아!
✔️project(member(controller)) → MemberController.java
...
// 로그인 전용 화면 이동
@GetMapping("/login")
public String login() { // 오버로딩
return "member/login";
}
...