팀 최종 프로젝트(23.12~24.02)(10)-프론트3-로그인 구현

이장한·2024년 2월 22일

팀프로젝트

목록 보기
11/15

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 페이지에 이동하게 된다.

로그인을 한 상태에서 메인 페이지에 돌아가면, 메인 화면에서 접속 가능한

페이지가 더 많아진 것을 알 수 있다.

profile
기술을 통해 세상을 이롭게 하리라

0개의 댓글