우선 회원클래스를 만든다.
회원
Lombok의 @Data를 이용해 생성자, getter, setter를 설정해준다.
다음 검증을 위해 @NotEmpty를 설정해준다.
우선 나는 MySQL을 DB로 사용할 예정인데 먼저 다른 부분을 완성한 후에 연결할 예정이다.
따라서 hashmap을 사용해서 repository를 만들것이다.
package com.shopingmall.seungjae.domain;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Data
public class Member {
private Long id;
@NotEmpty
private String name;
@NotEmpty
private String loginId;
@NotEmpty
private String password;
@NotEmpty
private String description;
}
이렇게 Member 클래스를 만들고 나서
MemberRepository 인터페이스를 생성한다.
package com.shopingmall.seungjae.repository;
import com.shopingmall.seungjae.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface MemberRepository {
public Member save(Member member);//저장소에 저장하기
public Optional<Member> findByLoginId(String id);
public Member findById(Long id);
public List<Member> findAll();
public void clearStore();
}
그리고 구현체는 아래와 같다.
package com.shopingmall.seungjae.repository;
import com.shopingmall.seungjae.domain.Member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.*;
@Repository @RequiredArgsConstructor @Slf4j
public class MemberRepositoryImpl implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>(); //static 사용
private static long sequence = 0L; //static 사용
@Override
public Member save(Member member) {
member.setId(++sequence);
log.info("save: member={}", member);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findByLoginId(String loginId) {
return findAll().stream()
.filter(member -> member.getLoginId().equals(loginId))
.findFirst();
}
@Override
public Member findById(Long id) {
return store.get(id);
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public void clearStore() {
store.clear();
}
}
@RequiredArgsConstructor는 생성자를 자동으로 생성해주는 어노테이션이고 @Slf4j는 log를 찍기 위해 필요한 어노테이션이다.
이제 Controller를 생성해야 한다.
controller.Member 패키지에 ShoppingMallMemberJoinController를 생성한다.
package com.shopingmall.seungjae.controller.Member;
import com.shopingmall.seungjae.domain.Member;
import com.shopingmall.seungjae.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
@Controller
@Slf4j @RequestMapping("member/signup") @RequiredArgsConstructor
public class ShoppingMallMemberJoinController {
private final MemberService memberService;
@GetMapping
public String JoinPage(@ModelAttribute Member member, Model model){
model.addAttribute("member", member);
return "join/joinPage";
}
@PostMapping
public String CreateMember(@ModelAttribute Member member, BindingResult bindingResult){
//검증
if(!StringUtils.hasText(member.getName())){
bindingResult.addError(new FieldError("member", "name", "문자를 적으셔야 합니다."));
}
if(!StringUtils.hasText(member.getLoginId())||member.getLoginId().length()<3||member.getLoginId().length()>10){
bindingResult.addError(new FieldError("member", "loginId", "올바르지 않은 ID: 문자가 있어야하고 아이디의 길이는 3자 이상 10자 이하여야 합니다."));
}
if(member.getPassword().length()<3||member.getPassword().length()>10){
bindingResult.addError(new FieldError("member", "password", "올바르지 않은 PASSWORD: 비밀번호의 길이는 3자 이상 10자 이하여야 합니다."));
}
if(bindingResult.hasErrors()){
log.info("bindingResult = {}", bindingResult);
return "join/joinPage";
}
memberService.join(member);
log.info("member={}", member);
return "redirect:/";
}
}
코드를 보면 @RequestMapping(/member/signup)이 전체 Controller에 작용하고 있는 것을 볼 수 있다.
JoinPage는 /member/signup에 GET방식으로 요청했을 때 실행되고 join/joinPage를 반환한다. 여기서 join/joinPage란 resources/templates 안에 들어있는 html파일을 말한다.
model.addAtribute를 넣은 이유는 아래에서 설명할 검증오류시에 redirect할텐데 이때 사용자가 join form에 입력한 내용을 유지하면서 redirect하기 위함이다.
@PostMapping은 /memeber/signup에 POST방식으로 요청했을 때 실행된다.
사용자가 입력한 내용을 제출을 눌렀을 때 실행된다.
따라서 ModelAttribute가 사용자가 입력한 내용을 Member에 매핑하여 member를 생성한다.
검증을 위해 여러가지 조건문을 추가해주고 만일 문제가 생겼다면 오류메시지와 함께 redirect한다.
만약 Id가 같은 회원이 있다면 예외를 발생시켜서 로그인하지 못하도록 하는 로직을 MemberService에 넣어준다.
package com.shopingmall.seungjae.service;
import com.shopingmall.seungjae.domain.Member;
import com.shopingmall.seungjae.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor @Service
public class MemberService {
private final MemberRepository memberRepository;
public String join(Member member) {
//같은 이름이 있는 중복 회원은 안된다.
memberRepository.findByLoginId(member.getLoginId())
.ifPresent(m->{
throw new IllegalStateException();
});
memberRepository.save(member); //저장
return member.getLoginId();
}
}
타임리프를 사용해서 joinPage를 구성했다. Front적인 부분은 내가 미적감각이 없고 많이 해보지 않아서 그냥 기능만 하도록 만들었다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원가입</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
} .container {
width: 400px;
margin: 0 auto;
padding: 20px;
background-color: #f0f0f0;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
.form-group textarea {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
button[type="submit"] {
padding: 10px 20px;
background-color: #4285f4;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error-message{
color: red;
}
</style>
</head>
<body>
<div class="container">
<form action="/member/signup" th:object="${member}" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" th:field="*{name}" name="name" placeholder="이름을 입력하세요" required>
</div>
<div th:errors="*{name}" class="error-message">
<p>이름 오류 메시지</p>
</div>
<div class="form-group">
<label for="loginId">아이디</label>
<input type="text" id="loginId" th:field="*{loginId}" name="loginId" placeholder="아이디를 입력하세요" required>
</div>
<div th:errors="*{loginId}" class="error-message">
<p>아이디 오류 메시지</p>
</div>
<div class="form-group">
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" placeholder="비밀번호를 입력하세요" required>
</div>
<div th:errors="*{password}" class="error-message">
<p>비밀번호 오류 메시지</p>
</div>
<div class="form-group">
<label for="description">자기 소개</label>
<textarea id="description" name="description" th:field="*{description}" placeholder="자기소개를 입력하세요" required></textarea>
</div>
<button type="submit">가입하기</button>
</form>
<div class="form-group">
<button type="button" onclick="location.href='/';">홈으로 돌아가기</button>
</div>
</div>
</body>
</html>


안녕하세요 잘 보고 있습니다 혹시 타임리프 뷰 파일은 어느 폴더 위치에 만들어야 하나요?