비즈니스 요구사항과 설계
회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 user와 admin 두 가지 등급이 있다.
DB
- 데이터 : 회원 ID, 등급, 로그인 아이디, 이름, 비밀번호
- 기능 : 회원 가입, 로그인, 로그아웃, 회원 조회, 회원 정보 수정
의존성 선택

홈 화면 생성
home.html
<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<div class="container max600 vh-100 d-flex flex-column justify-content-center align-content-center" layout:fragment="content">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<div class="row">
<div class="col">
<button type="button" th:onclick="|location.href='@{/add}'|" class="w-100 btn btn-secondary btn-lg">회원 가입</button>
</div>
<div class="col">
<button type="button" th:onclick="|location.href='@{/login}'|" class="w-100 btn btn-dark btn-lg">로그인</button>
</div>
</div>
</div>
</body>
</html>
<!--
[button 태그에 URL 연결]
th:onclick: 버튼 클릭 시 실행할 JavaScript 코드를 설정
location.href: JavaScript 구문으로 브라우저의 현재 URL을 변경하여 다른 페이지로 이동
@{/URL}: Thymeleaf의 URL 작성 구문으로, 애플리케이션의 컨텍스트 경로를 포함하여 URL을 생성
-->
loginHome.html
<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<div class="container max600 vh-100 d-flex flex-column justify-content-center align-content-center" layout:fragment="content">
<div class="text-center">
<h2>홈 화면</h2>
</div>
<div class="py-3 row">
<p th:text="${loginMember.name} + '님, 환영합니다.'" class="mb-3 col-7 text-end"></p>
<form th:action="@{/logout}" method="post" class="col-5 text-start">
<button type="submit" class="btn btn-secondary btn-sm">로그아웃</button>
</form>
</div>
<div class="row">
<div class="col">
<button type="button" th:onclick="|location.href='@{/members}'|" class="w-100 btn btn-dark btn-lg">회원 목록</button>
</div>
</div>
</div>
</body>
</html>
HomeController.java (Cookie 사용)
package login.login_spring.controller;
import login.login_spring.domain.EzenMember;
import login.login_spring.repository.EzenMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class HomeController {
private final EzenMemberRepository ezenMemberRepository;
//required = false 비회원, 로그인 안한 사용자도 홈에 접속해야 함
@GetMapping("/")
public String home(@CookieValue(name="memberId", required = false) Long memberId, Model model) {
if(memberId == null) {
return "home";
}
//로그인, 저장소에 가서 아이디를 찾아 꺼냄
Optional<EzenMember> loginMember = ezenMemberRepository.findById(memberId);
if(loginMember.isPresent()){
EzenMember findLoginMember = loginMember.get();
model.addAttribute("loginMember", findLoginMember);
return "loginHome";
}
return "home";
}
}
HomeController.java (Session 사용)
package login.login_spring.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import login.login_spring.domain.EzenMember;
import login.login_spring.repository.EzenMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class HomeController {
private final EzenMemberRepository ezenMemberRepository;
@GetMapping("/")
public String home(HttpServletRequest request, Model model) {
//세션이 없으면 home
HttpSession session = request.getSession(false);
if(session == null) {
return "home";
}
//로그인 시점에 세션에 보관한 회원 객체를 찾는다
Object loginMember = session.getAttribute(SessionConst.LOGIN_MEMBER);
//세션에 회원 데이터가 없으면 home
if(loginMember == null){
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("loginMember", loginMember);
return "loginHome";
}
}
//세션이 쿠키보다 보안적으로 좋음
세션 쿠키 삭제(로그아웃)

회원가입 화면 생성
addMemberForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>회원 가입</title>
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body class="bg-body-tertiary vh-100 d-flex flex-column justify-content-center align-content-center">
<div class="container" style="width: 500px;">
<div class="bg-white border rounded-4 p-5">
<h1 class="pb-3">회원 가입</h1>
<form th:action th:Object="${memberForm}" method="post">
<!-- th:Object는 폼과 연관된 객체를 지정함. 이 객체를 통해 폼의 필드와 DTO간의 바인딩(데이터 간의 연결)을 가능하게 함 -->
<!-- Controller에서 받은 DTO를 View단에서 보여줌 -->
<div class="mb-3">
<label th:for="name" class="form-label">이름</label>
<input type="text" class="form-control" th:errorClass="field-error" th:field="*{name}" placeholder="이름을 입력하세요">
<div th:errors="*{name}" class="field-error"></div>
</div>
<!-- th:errorClass 폼 필드에 대한 유효성 검사 오류가 발생했을 때 해당 필드에 클래스 추가 -->
<!-- th:errors 유효성 검사 오류 메시지를 표시하는 데 사용되는 속성 -->
<!-- th:field를 이용하면 id, name 값을 DTO 필드값에 맞춰서 작성하지 않아도 자동 생성됨 -->
<div class="mb-3">
<label th:for="loginId" class="form-label">아이디</label>
<input type="text" class="form-control" th:errorClass="field-error" th:field="*{loginId}" placeholder="아이디를 입력하세요">
<div th:errors="*{loginId}" class="field-error"></div>
</div>
<div class="mb-3 pb-3">
<label th:for="password" class="form-label">비밀번호</label>
<input type="password" class="form-control" th:errorClass="field-error" th:field="*{password}" placeholder="비밀번호를 입력하세요">
<div th:errors="*{password}" class="field-error"></div>
</div>
<div class="row">
<div class="col">
<button type="submit" class="w-100 btn btn-primary btn-lg">회원 가입</button>
</div>
<div class="col">
<button type="button" th:onclick="|location.href='@{/}'|" class="w-100 btn btn-secondary btn-lg">취소</button>
</div>
</div>
</form>
</div>
</div>
</body>
</html>
회원목록 화면 생성
memberList.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>회원 목록</title>
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body class="bg-body-tertiary vh-100 d-flex flex-column justify-content-center align-content-center">
<div class="container" style="width: 800px;">
<div class="bg-white border rounded-4 p-5">
<h1 class="pb-4">회원 목록</h1>
<table class="table text-center">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">아이디</th>
<th scope="col">이름</th>
<th scope="col">비밀번호</th>
<th scope="col">등급</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr class="align-middle" th:each = "member : ${ezenMembers}">
<!-- 받은 변수를 각각 돌려 줄 지역변수 : ${Model로 받은 변수} -->
<th scope="row" th:text="${member.id}"></th>
<td th:text="${member.loginId}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.password}"></td>
<td th:text="${member.grade}"></td>
<td><a th:href="@{/members/{id}/edit (id=${member.id})}" class="btn btn-outline-primary" role="button">수정</a></td>
<!-- role : 웹브라우저에게 어떤 역할인지 알려줌 -->
<td><a th:href="@{/members/{id}/delete (id = ${member.id})}" class="btn btn-danger" role="button">삭제</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
회원 수정 화면 생성
updateMemberForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>회원 수정</title>
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body class="bg-body-tertiary vh-100 d-flex flex-column justify-content-center align-content-center">
<div class="container" style="width: 500px;">
<div class="bg-white border rounded-4 p-5">
<h1 class="pb-3">회원 수정</h1>
<form th:action th:Object="${memberform}" method="post">
<input type="hidden" th:field="*{id}">
<div class="mb-3">
<label th:for="name" class="form-label">이름</label>
<input type="text" class="form-control" th:errorClass="field-error" th:field="*{name}" placeholder="이름을 입력하세요">
<div th:errors="*{name}" class="field-error"></div>
</div>
<div class="mb-3">
<label th:for="loginId" class="form-label">아이디</label>
<input type="text" class="form-control" th:errorClass="field-error" th:field="*{loginId}" placeholder="아이디를 입력하세요">
<div th:errors="*{loginId}" class="field-error"></div>
</div>
<div class="mb-3">
<label th:for="password" class="form-label">비밀번호</label>
<input type="password" class="form-control" th:errorClass="field-error" th:field="*{password}" th:placeholder="'기존 비밀번호는 ' + *{password} + ' 입니다.'">
<div th:errors="*{password}" class="field-error"></div>
</div>
<div class="mb-3 pb-3">
<label th:for="grade" class="form-label">등급</label>
<select class="form-select" th:field="*{grade}">
<option value="user" selected>user</option>
<option value="admin">admin</option>
</select>
</div>
<div class="row">
<div class="col">
<button type="submit" class="w-100 btn btn-primary btn-lg">수정</button>
</div>
<div class="col">
<button type="button" th:onclick="|location.href='@{/members}'|" class="w-100 btn btn-secondary btn-lg">취소</button>
</div>
</div>
</form>
</div>
</div>
</body>
</html>
멤버 컨트롤러
MemberController.java
package login.login_spring.controller;
import jakarta.validation.Valid;
import login.login_spring.domain.EzenMember;
import login.login_spring.dto.MemberForm;
import login.login_spring.service.EzenMemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@Controller
@RequiredArgsConstructor
public class MemberController {
private final EzenMemberService ezenMemberService;
//회원 등록
@GetMapping("/add")
public String addMember(Model model) {
model.addAttribute("memberForm", new MemberForm()); //DTO 받아서 View단에 넘겨줌
return "user/addMemberForm";
}
@PostMapping("/add")
public String createMember(@Valid @ModelAttribute("memberForm") MemberForm form, BindingResult bindingResult) {
//@Valid : 유효성 검사 수행
//BindingResult : 유효성 검사 결과는 BindingResult 객체에 담기며, 이를 통해 오류가 있는지 확인할 수 있음
//@ModelAttribute : Model의 memberForm 객체를 가져옴, 그 객체의 자료형과 변수명
//이렇게 하면 사용자 입력 데이터가 MemberForm DTO로 자동으로 바인딩. 즉, 폼에서 제출된 데이터가 form 객체의 필드에 채워짐
if(bindingResult.hasErrors()) {
return "user/addMemberForm"; //bindingResult에 Error가 있다면 해당 html로 리턴
}
EzenMember ezenMember = new EzenMember();
ezenMember.setName(form.getName());
ezenMember.setLoginId(form.getLoginId());
ezenMember.setPassword(form.getPassword());
ezenMember.setGrade("user"); //회원가입시 등급 기본값 = user
ezenMemberService.join(ezenMember);
return "redirect:/";
}
//회원 목록
@GetMapping("/members")
public String memberList(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, Model model) {
List<EzenMember> ezenMembers = ezenMemberService.findMember();
model.addAttribute("ezenMembers", ezenMembers);
model.addAttribute("loginMember", loginMember);
return "admin/memberList";
}
//회원 수정
@GetMapping("/members/{id}/edit")
public String editMember(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, @PathVariable("id") Long id, Model model) {
Optional<EzenMember> findMember = ezenMemberService.findOne(id);
EzenMember findMemberGet = findMember.get();
MemberForm form = new MemberForm();
form.setId(findMemberGet.getId());
form.setLoginId(findMemberGet.getLoginId());
form.setPassword(findMemberGet.getPassword());
form.setName(findMemberGet.getName());
form.setGrade(findMemberGet.getGrade());
model.addAttribute("memberform", form);
model.addAttribute("loginMember", loginMember);
return "admin/updateMemberForm";
}
@PostMapping("/members/{id}/edit")
public String updateMember(@SessionAttribute(name = SessionConst.LOGIN_MEMBER) EzenMember loginMember, @Valid @ModelAttribute("memberform") MemberForm form, BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()){
model.addAttribute("loginMember", loginMember);
return "admin/updateMemberForm";
}
EzenMember ezenMember = new EzenMember();
ezenMember.setId(form.getId());
ezenMember.setName(form.getName());
ezenMember.setLoginId(form.getLoginId());
ezenMember.setPassword(form.getPassword());
ezenMember.setGrade(form.getGrade());
ezenMemberService.update(ezenMember);
model.addAttribute("loginMember", loginMember);
return "redirect:/members";
}
//회원 삭제
@GetMapping("/members/{id}/delete")
public String deleteMember(@PathVariable("id") Long id) {
Optional<EzenMember> findMember = ezenMemberService.findOne(id);
if(findMember.isPresent()){
ezenMemberService.delete(findMember.get());
}
return "redirect:/members";
}
}
PRG 패턴
Post/Redirect/Get의 약자로, 웹 애플리케이션에서 폼 제출 후 페이지의 상태를 관리하는 데 사용되는 디자인 패턴
로그인 화면 생성
loginForm.html
<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<div class="container vh-100 d-flex flex-column justify-content-center align-content-center" style="width: 500px;" layout:fragment="content">
<div class="bg-white border rounded-4 p-5">
<h1 class="pb-3">로그인</h1>
<form th:action th:Object="${loginform}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<!-- 만약 error가 있으면, 글로벌 오류 메시지가 나옴 -->
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}" class="field-error">전체 오류 메세지</p>
<!-- 오류가 여러 개 일수도 있음 -->
</div>
<div class="mb-3">
<label th:for="loginId" class="form-label">아이디</label>
<input type="text" class="form-control" th:errorClass="field-error" th:field="*{loginId}" placeholder="아이디를 입력하세요">
<div th:errors="*{loginId}" class="field-error"></div>
</div>
<div class="mb-3 pb-3">
<label th:for="password" class="form-label">비밀번호</label>
<input type="password" class="form-control" th:errorClass="field-error" th:field="*{password}" placeholder="비밀번호를 입력하세요">
<div th:errors="*{password}" class="field-error"></div>
</div>
<div class="row">
<div class="col">
<button type="submit" class="w-100 btn btn-primary btn-lg">로그인</button>
</div>
<div class="col">
<button type="button" th:onclick="|location.href='@{/}'|" class="w-100 btn btn-secondary btn-lg">취소</button>
</div>
</div>
</form>
</div>
</div>
</body>
</html>
LoginController.java (Cookie 사용)
package login.login_spring.controller;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import login.login_spring.domain.EzenMember;
import login.login_spring.dto.LoginForm;
import login.login_spring.service.LoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
//@Slf4j
public class LoginControllerCookie {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(Model model) {
model.addAttribute("loginform", new LoginForm());
return "user/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginform") LoginForm form, BindingResult bindingResult, HttpServletResponse response){
if(bindingResult.hasErrors()){
return "user/loginForm";
}
EzenMember loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 일치하지 않습니다.");
//글로벌 에러 처리
return "user/loginForm";
}
//쿠키에 시간 정보를 주지 않으면 세션 쿠키는 브라우저 종료시 모두 종료
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
//log.info("누가 로그인 했을까?", "loginMember.getLoginId()");
return "redirect:/";
}
//로그아웃
@PostMapping("/logout")
public String logout(HttpServletResponse response){
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String memberId) {
Cookie cookie = new Cookie(memberId, null);
cookie.setMaxAge(0); //쿠키의 시간을 0으로 만듦
response.addCookie(cookie);
}
}
LoginController.java (Session 사용)
package login.login_spring.controller;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import login.login_spring.domain.EzenMember;
import login.login_spring.dto.LoginForm;
import login.login_spring.repository.EzenMemberRepository;
import login.login_spring.service.LoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(Model model) {
model.addAttribute("loginform", new LoginForm());
return "user/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginform") LoginForm form, BindingResult bindingResult, HttpServletRequest request){
if(bindingResult.hasErrors()){
return "user/loginForm";
}
EzenMember loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 일치하지 않습니다.");
//글로벌 에러 처리
return "user/loginForm";
}
//로그인 성공시 세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인한 회원정보를 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
//로그아웃
@PostMapping("/logout")
public String logout(HttpServletRequest request){
//세션을 삭제
HttpSession session = request.getSession(false);
//기본값인 true는 세션이 없으면 만들어 버린다. 일단 가지고 오는데 없으면 null
if(session != null){
session.invalidate(); //세션 제거
}
return "redirect:/";
}
}
SessionConst.java (Session 사용)
package login.login_spring.controller;
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
//HttpSession에 데이터를 보관하고 조회할 때 같은 이름이 중복 되어 사용되므로 상수를 하나 정의함
}
쿠키 생성 확인

도메인과 리포지토리 생성
EzenMember.java
package login.login_spring.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter @Setter
public class EzenMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String grade;
private String loginId;
private String name;
private String password;
}
EzenMemberRepository.java
package login.login_spring.repository;
import login.login_spring.domain.EzenMember;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface EzenMemberRepository extends JpaRepository<EzenMember, Long> {
//사용자 아이디로 조회 기능 추가
Optional<EzenMember> findByLoginId(String loginId);
}
서비스 생성
EzenMemberService.java
package login.login_spring.service;
import login.login_spring.domain.EzenMember;
import login.login_spring.repository.EzenMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class EzenMemberService {
private final EzenMemberRepository ezenMemberRepository;
//회원 가입
public Long join(EzenMember ezenMember) {
validateDuplicateEzenMember(ezenMember);
this.ezenMemberRepository.save(ezenMember);
return ezenMember.getId();
}
//중복회원 검증
private void validateDuplicateEzenMember(EzenMember ezenMember) {
Optional<EzenMember> findMember = ezenMemberRepository.findByLoginId(ezenMember.getLoginId());
if(findMember.isPresent()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
//전체회원 조회
public List<EzenMember> findMember() {
return ezenMemberRepository.findAll();
}
//회원한명 조회
public Optional<EzenMember> findOne(Long memberId) {
return ezenMemberRepository.findById(memberId);
}
//회원 수정
public Long update(EzenMember ezenMember) {
this.ezenMemberRepository.save(ezenMember);
return ezenMember.getId();
}
//회원 삭제
public void delete(EzenMember ezenMember) {
this.ezenMemberRepository.deleteById(ezenMember.getId());
}
}
LoginService.java
package login.login_spring.service;
import login.login_spring.domain.EzenMember;
import login.login_spring.repository.EzenMemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class LoginService {
private final EzenMemberRepository ezenMemberRepository;
//로그인
public EzenMember login(String loginId, String password) {
Optional<EzenMember> findLoginId = ezenMemberRepository.findByLoginId(loginId);
if(findLoginId.isPresent()){
EzenMember findMember = findLoginId.get();
if(findMember.getPassword().equals(password)){
return findMember;
} else {
return null;
}
}
return null;
}
}
DTO 생성
MemberForm.java
package login.login_spring.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class MemberForm {
private Long id;
private String grade;
@NotEmpty(message = "아이디를 적어주세요.") //message를 적지않으면 기본값 실행
private String loginId;
@NotEmpty(message = "이름을 적어주세요.")
private String name;
@NotEmpty(message = "비밀번호를 적어주세요.")
private String password;
}
// @NotEmpty : 입력받을 때 필수적으로 요구되는 속성
DTO에 기본키를 포함하지 않는 이유?
DTO에는 사용자가 입력하거나 수정할 수 있는 데이터(예: 이름, 이메일, 주소 등)를 포함한다. 이러한 속성들은 클라이언트 측에서 수집된 정보를 기반으로 하며, 서버에서 처리 후 데이터베이스에 반영된다. 즉 변경 가능한 속성들이 포함된다.
기본키는 도메인 모델의 중요한 식별자로, 데이터베이스 내에서 해당 엔티티를 고유하게 식별한다. 따라서 기본키는 DTO에 포함하지 않고, 서버에서 내부적으로 관리하는 것이 일반적이다. 즉, 일반적으로 변경할 일이 없다.
LoginForm.java
package login.login_spring.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class LoginForm {
@NotEmpty(message = "아이디를 적어주세요.")
private String loginId;
@NotEmpty(message = "비밀번호를 적어주세요.")
private String password;
}
layout 설정
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' //추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
layout.html
<!DOCTYPE html>
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>layout</title>
<link href="/css/bootstrap.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<header th:fragment="header" class="fixed-top">
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom border-2 border-black py-3">
<div class="container-fluid">
<a class="navbar-brand" href="/">Home</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<div th:if="${loginMember == null}">
<a class="nav-link" href="/login">로그인</a>
</div>
<div th:if="${loginMember}" class="mt-1">
<p th:text="${loginMember.name} + '님 환영합니다.'" class="m-0 fw-bold"></p>
</div>
</li>
<li class="nav-item">
<div th:if="${loginMember == null}">
<a class="nav-link" href="/add">회원 가입</a>
</div>
<div th:if="${loginMember}">
<form th:action="@{/logout}" method="post" class="ms-2">
<button type="submit" class="btn btn-secondary btn-sm">로그아웃</button>
</form>
</div>
</li>
</ul>
</div>
</div>
</nav>
</header>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<footer th:fragment="footer" class="bg-body-tertiary fixed-bottom">
<div class="container d-flex justify-content-center align-items-center py-4">
<p class="m-0">이젠 ⓒ Designed by Crystal</p>
</div>
</footer>
<!-- 자바스크립트 -->
<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
<script src="/js/bootstrap.bundle.js"></script>
<th:block layout:fragment="script"></th:block>
</body>
</html>
layout을 부여할 html 파일에 하단 코드 삽입
<!-- decorate="~{레이아웃 html 이름}" -->
<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<!-- div 안에 하단 코드 삽입 -->
layout:fragment="content"
style.css
.max600 {
max-width: 600px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
.content {
flex: 1; /* 남은 공간을 채우도록 설정 */
padding: 20px;
overflow-y: auto; /* 스크롤이 가능하도록 설정 */
margin-top: 80px; /* 헤더 높이만큼 여백 */
margin-bottom: 80px; /* 푸터 높이만큼 여백 */
}
.questionTitle {
color: black;
text-decoration: none;
}
.questionTitle:hover {
text-decoration: underline;
}
환경 설정
application.yml
spring:
application:
name: login-spring
mvc:
pathmatch:
matching-strategy: ant_path_matcher #세션 사용시 추가
datasource:
url: jdbc:h2:tcp://localhost/~/testApi
driver-class-name: org.h2.Driver
username: sa
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
thymeleaf:
prefix: classpath:/templates/
suffix: .html
logging:
level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
경로


'Framework > Spring' 카테고리의 다른 글
| [Spring] Thymeleaf - Paging (0) | 2024.11.06 |
|---|---|
| [Spring] Q&A 페이지 예제 (0) | 2024.11.05 |
| [Spring] HTML 구성 요소의 통합 (2) | 2024.10.31 |
| [Spring] 회원 관리 예제 (1) | 2024.10.31 |
| [Spring] H2 서버 연결하기 (0) | 2024.10.30 |