1.백엔드
controller
@GetMapping("/login") // 로그인 페이지로 이동하는 컨트롤러.
public String gologin() {
return "login";
}
@PostMapping("/login")
public String login(@RequestParam("memberId") String memberId,
@RequestParam("password") String password,
HttpSession session, RedirectAttributes attributes) {
try {
// 실제로는 데이터베이스나 다른 인증 로직을 통해 사용자를 검증하는 작업이 필요
memberDTO result = memberService.login(new memberDTO(memberId, password));
if (result != null) {
// 로그인 성공 시 세션에 정보 저장
session.setAttribute("loggedInUser", result.getMemberId());
// 리다이렉트 URL과 함께 성공 메시지 전달
attributes.addFlashAttribute("successMessage", "로그인에 성공했습니다.");
return "redirect:/goofficer";
} else {
// 로그인 실패 시 리다이렉트 URL과 함께 실패 메시지 전달
attributes.addFlashAttribute("errorMessage", "아이디 또는 비밀번호가 올바르지 않습니다.");
return "redirect:/login";
}
} catch (Exception e) {
// 로그인 중 오류 발생 시 리다이렉트 URL과 함께 오류 메시지 전달
attributes.addFlashAttribute("errorMessage", "로그인 중 오류가 발생했습니다.");
return "redirect:/login";
}
}
// 로그아웃에 관한 get매핑이다.
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
// 세션에서 사용자 정보 삭제
request.getSession().invalidate();// 로그아웃은 이거 한줄이면 뚝딱이다.
return "redirect:/main";
}
참고로, 로그아웃에 대한 controller 코드도 있다는 것을 알 수 있다.
service
package com.example.finalproject.finalproject.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.finalproject.finalproject.dto.memberDTO;
import com.example.finalproject.finalproject.entity.MemberEntity;
import com.example.finalproject.finalproject.repository.memberRepository;
@Service
@Component
public class memberservice {
/*
* dto와 entity의 관계
* service의 코드들을 잘 보기 전에, dto,entity의 관계를 잘 볼 필요가 있다.
* 1.entity는 실제 디비와 매핑되는 핵심 클래스이다.
* 테이블이 가지지 않는 컬럼을 필드로 가져서는 안된다
* entity는 데이터베이스 영속성을 목적으로 사용되기 때문에 request,response값을 전달하는 클래스로 쓰지 말것.
*
* 2.dto는 레이어간 데이터 교환이 이뤄질 수 있도록 하는 객체이다.
* 디비에서 데이터를 얻어, service나 controller등으로 보낼 때 사용한다.
*
* 3.entity를 직접 반환할 경우에는 엔터티의 이름이 변경될 경우, 추가 작업이 필요할 수 있다.
* 또한, 보안 문제도 있고, 필요한 데이터만 전송하기 어렵다.
*
*
* 4.컨트롤러에서는 dto의 형태로 데이터를 받아 서비스에 전달한다.
* 5.서비스에서는 컨트롤러에서 받은 DTO를 Entity로 변환하고, 필요한 작업을 수행한 뒤에 Repository에 Entity를
* 전달한다.
*
*/
// service 객체에 쓰일 repository를 정의한다.
// repository에 jpa가 있기 때문에 꼭 정의를 해야 한다.
@Autowired
private final memberRepository memberRepository;
// memberRepository가 null인 상태에서 save 메서드를 호출하려 하면 오류가 난다. 그래서 이 코드를 추가.
@Autowired
public memberservice(memberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 받아온 데이터들을 저장하는 메소드이다.
public void save(memberDTO memberDTO) {
// repository의 save 메서드 호출.(entity를 넘겨야 한다.)
// 1.dto를 entity로 변환한다.(dto 클래스에 구현)
try {
MemberEntity memberEntity = MemberEntity.toMemberEntity(memberDTO);
memberRepository.save(memberEntity); // 이렇게 레포지토리로 데이터를 save시킨다.
}
// 2.save 메서드를 호출한다.(jpa에서 호출한다.)
// 3.이 쿼리를 통해 데이터베이스 내에서 쿼리를 만들어 주는 것이다.
catch (Exception e) {
e.printStackTrace();
}
}
public memberDTO login(memberDTO memberDTO) {
// 로그인을 수행할 때 수행되는 함수이다.
// 1.회원이 입력한 아이디로 db에서 조회를 한다.
// Optional은 memberentity를 한번 더 감싸는 개념이다.
Optional<MemberEntity> findById = memberRepository.findByMemberId(memberDTO.getMemberId());
// 조회 결과가 있다면
if (findById.isPresent()) {
// 일단 이 구문을 통해 데이터를 벗겨낸다.
MemberEntity memberEntity = findById.get();
// 2.db에서 조회한 비밀번호(entity)가 사용자가 입력한 비밀번호(dto)가 일치하는지 판단한다.
if (memberEntity.getPassword().equals(memberDTO.getPassword())) {
// 비밀번호 일치
// entity를 dto로 변환한 후 리턴해야 한다.(번거롭다.)
// 결국, 로그인을 성공했을 때만 dto에 뭘 담아서 주는 것이다.
memberDTO dto = memberDTO.toMemberDTO(memberEntity);
return dto;
} else {
// 비밀번호 불일치(로그인 실패)
return null;
}
} else {
// 3.없다면
return null;
}
}
// 회원 조회를 위한 함수이다.
public List<memberDTO> findAll() {
List<MemberEntity> memberEntityList = memberRepository.findAll();
// entity list를 dto list로 변환해야 한다.
List<memberDTO> memberDTOList = new ArrayList<>();
// 하나하나 꺼낸다.
for (MemberEntity memberEntity : memberEntityList) {
memberDTOList.add(memberDTO.toMemberDTO(memberEntity));
}
return memberDTOList;
}
public String emailCheck(String memberEmail) {
// repository 함수를 통해 사용자가 입력한 이메일 값으로 조회를 한다. 이렇게 조회하는 것은 JPA의 repository를 통해
// 조회할 수 있다.
Optional<MemberEntity> byMemberEmail = memberRepository.findByEmail(memberEmail);
if (byMemberEmail.isPresent()) {
// 이메일 값이 이미 있으면(회원이 중복되어 있으면)사용할 수 없다.
return null;
}
else {
// 조회 결과가 없으면 사용할 수 있다.
return "ok";
}
}
public String idCheck(String memberId) {
// repository 함수를 통해 사용자가 입력한 아이디 값으로 조회를 한다.
Optional<MemberEntity> byMemberId = memberRepository.findByMemberId(memberId);
if (byMemberId.isPresent()) {
// 아이디 값이 이미 있으면(회원이 중복되어 있으면)사용할 수 없다.
return null;
}
else {
// 조회 결과가 없으면 사용할 수 있다.
return "ok";
}
}
public MemberEntity getPasswordByEmail(String memberEmail) {
Optional<MemberEntity> byMemberEmail = memberRepository.findByEmail(memberEmail);
return byMemberEmail.orElse(null);
}
// Transactional 어노테이션은 여러 줄의 코드를 하나의 작업으로 처리해준다.
// 하나의 작업으로 처리해주면, 부분적으로 오류가 난 것을 같이 처리할 수 있다는 장점이 있다.
@Transactional
public boolean changePassword(String email, String newPassword) {
Optional<MemberEntity> userOptional = memberRepository.findByEmail(email);
if (userOptional.isPresent()) {
MemberEntity user = userOptional.get();
user.setPassword(newPassword);
memberRepository.save(user);
return true;
}
return false;
}
// 이메일로 아이디 찾기
public Optional<MemberEntity> findIdByEmail(String email) {
return memberRepository.findByEmail(email);
}
// 아이디로 비밀번호 찾기
public Optional<MemberEntity> findPasswordById(String memberId) {
return memberRepository.findByMemberId(memberId);
}
}
controller에서 쓰인 함수를 유의깊게 관찰하자.
repository
앞선 글에서 설명했던 repository와 똑같다.
그리고, 앞선 글에서 설명하지 못한 것이 있는데,
각 기능이 실행될 때 마다, 미리 설정해 둔 dto,entity가 반영된다는 것을 잊지 말자.
2.프론트엔드-html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<link rel="icon" th:href="@{/favicon.ico}" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>로그인</title>
<link th:href="@{/static/css/login.css}" rel="stylesheet"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat%3A400%2C600%2C700"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro%3A400%2C600%2C700"/>
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
</head>
<body>
<div class="item--brL">
<div class="frame-3851-Fvt">
<div class="frame-3851-nfv">
<div class="roads-dgY">ROADs</div>
<p class="road-obstacle-ai-detection-service-75v">Road Obstacle Ai Detection service</p>
</div>
<div class="frame-830-aVJ">
<form id="loginForm" action="/login" method="post">
<input type="text" id="memberId" name="memberId" placeholder="ID" class="text-field-sjJ"></input>
<input type="password" id="password" name="password" placeholder="PW" class="text-field-Lep"></input>
<div class="text-field-7oz">ID/PW 찾기</div>
<button type="submit" class="button-w2L">로그인</button>
<button class="button-jTz">회원가입</button>
</form>
</div>
</div>
</div>
<script src="static/js/login.js"></script>
</body>
</html>
jQuery,js,css,thymeleaf,글꼴 등에 관한 설정은 앞의 글의 설명과 같다.
3.프론트엔드-css
html {
font-size:62.5%;
}
* {
margin: 0;
padding: 0;
}
ul, li {
list-style: none;
}
input {
border: none;
}
body {
width: 144rem;
}.item--brL {
box-sizing: border-box;
padding: 15.7rem 44.7rem 18.3rem 44.8rem;
width: 100%;
height: 80rem;
overflow: hidden;
position: relative;
background-color: #ffffff;
border-radius: 3rem;
}
.item--brL .frame-3851-Fvt {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.item--brL .frame-3851-Fvt .frame-3851-nfv {
margin-bottom: 4rem;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.item--brL .frame-3851-Fvt .frame-3851-nfv .roads-dgY {
margin-bottom: 2rem;
font-size: 5.4rem;
font-weight: 700;
line-height: 1.2175;
color: #000000;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
flex-shrink: 0;
}
.item--brL .frame-3851-Fvt .frame-3851-nfv .road-obstacle-ai-detection-service-75v {
font-size: 1.4rem;
font-weight: 600;
line-height: 1.7142857143;
letter-spacing: 0.014rem;
color: #000000;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
flex-shrink: 0;
}
.item--brL .frame-3851-Fvt .frame-830-aVJ {
width: 100%;
row-gap: 2rem;
align-items: center;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .text-field-sjJ {
box-sizing: border-box;
padding: 1.3rem 2rem;
width: 100%;
font-size: 1.4rem;
font-weight: 400;
line-height: 1.7142857143;
letter-spacing: 0.014rem;
color: #828282;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
display: flex;
align-items: baseline;
border: solid 0.1rem #e0e0e0;
flex-shrink: 0;
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .text-field-sjJ .text-field-sjJ-sub-0 {
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .text-field-sjJ .text-field-sjJ-sub-1 {
font-size: 1.4rem;
font-weight: 400;
line-height: 1.7142857143;
letter-spacing: 0.014rem;
color: #828282;
font-family: Montserrat, 'Source Sans Pro';
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .text-field-Lep {
box-sizing: border-box;
padding: 1.3rem 2rem;
width: 100%;
font-size: 1.4rem;
font-weight: 400;
line-height: 1.7142857143;
letter-spacing: 0.014rem;
color: #828282;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
border: solid 0.1rem #e0e0e0;
flex-shrink: 0;
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .text-field-7oz {
box-sizing: border-box;
padding: 0.3rem 2.9rem 0.3rem 44.9rem;
width: 100%;
font-size: 1.4rem;
font-weight: 400;
line-height: 1.7142857143;
letter-spacing: 0.014rem;
color: #828282;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
border: solid 0.1rem #ffffff;
flex-shrink: 0;
cursor:pointer; /*커서를 올렸을 때, 마우스가 포인터로 변환된다.*/
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .button-w2L {
width: 100%;
height: 5rem;
font-size: 1.6rem;
font-weight: 700;
line-height: 1.2175;
text-transform: uppercase;
color: #ffffff;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
background-color: #5491f5;
flex-shrink: 0;
cursor:pointer;
}
.item--brL .frame-3851-Fvt .frame-830-aVJ .button-jTz {
margin-top:10px;
width: 100%;
height: 5rem;
font-size: 1.6rem;
font-weight: 700;
line-height: 1.2175;
text-transform: uppercase;
color: #ffffff;
font-family: Montserrat, 'Source Sans Pro';
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
background-color: #5491f5;
flex-shrink: 0;
cursor:pointer;
}
.button-jTz:hover{
background-color: #50bcdf;
}
.button-w2L:hover{
background-color: #50bcdf;
}
.roads-dgY:hover{
cursor:pointer;
}
딱히 설명할 부분은 없다.
하나 있다면 rem에 관한 것이다.
font-size: 1.6rem;
이런 코드에서 rem은 절대적인 px값에 따라 비례해서 증가하거나 감소하는
단위이다. 1rem이면 그대로이고, 1.6이면 1.6배인 식이다.
4.프론트엔드-javascript
document.addEventListener('DOMContentLoaded', function() {
//roads 클릭 시 메인 페이지로 이동
document.querySelector('.roads-dgY').addEventListener('click', function() {
window.location.href = '/main';
});
// ID/PW 찾기 버튼 클릭 시 이벤트
document.querySelector('.text-field-7oz').addEventListener('click', function() {
window.location.href = '/findpassword';
});
// 로그인 버튼 클릭 시 이벤트
document.querySelector('.button-w2L').addEventListener('click', function() {
window.location.href = '/main';
});
// 회원가입 버튼 클릭 시 이벤트
document.querySelector('.button-jTz').addEventListener('click', function() {
// 이벤트의 기본 동작을 막음 (기본 동작이 페이지 이동인 경우)
event.preventDefault();
window.location.href = '/signup';
});
});
//DOMContentLoaded를 쓰는 이유: DOM들이 완전히 로드되기 전에 JavaScript가 실행되기때문.
//addEventListener,querySelector 등을 통해 요소를 검색한다.
"왜 javascript에 페이지 이동 말고는 다른게 없지?"
라고 생각할 수 있고, 그게 맞다.
하지만, 위에서도 볼 수 있듯 백엔드에서 로그인에 대한 로직 설정을 해두었기 때문에 따로 javascript에서 로그인 로직을 구현할 필요가 없었다.
5.스크린샷
로그인을 하기 전의 화면이다.

로그인 페이지이다.

로그인 페이지에서 로그인을 하면, goofficer 페이지에 이동하게 된다.

로그인을 한 상태에서 메인 페이지에 돌아가면, 메인 화면에서 접속 가능한
페이지가 더 많아진 것을 알 수 있다.
