[springBoot] 스프링 부트 회원가입

인철·2024년 3월 2일
0

Spring boot Code

목록 보기
2/3
post-thumbnail

스프링부트 회원가입 하기

  • 부가적인 클래스 생성 후 controller, service, repositroy, entity 설명


package com.sideproject.healMingle.base.rq;

import com.sideproject.healMingle.base.ut.Ut;
import com.sideproject.healMingle.boundContext.member.entity.Member;
import com.sideproject.healMingle.boundContext.member.service.MemberService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

@Component 
@RequestScope
// 스프링 컴포넌트로 선언하여 스프링 빈으로 관리되게 하고, 요청 스코프로 지정
public class Rq {
	private final MemberService memberService;
	private final HttpServletRequest req; // HTTP 요청 정보
	private final HttpServletResponse resp; // HTTP 응답 정보
	private final HttpSession session;
	private final User user;
	private Member member = null;

	public Rq(MemberService memberService, HttpServletRequest req, HttpServletResponse resp, HttpSession session) {
		// 생성자에서 의존성 주입과 함께 현재 인증 정보를 가져옴
		this.memberService = memberService;
		this.req = req;
		this.resp = resp;
		this.session = session;

		// 현재 로그인한 회원의 인증정보를 SecurityContextHolder 에서 가져옴
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

		if (authentication.getPrincipal() instanceof User) { // 인증 정보가 User 타입인 경우 해당 정보를 User 필드에 저장
			this.user = (User) authentication.getPrincipal();
		} else {
			this.user = null;
		}
	}

	// 로그인한 회원의 사용자명을 가져오는 메서드
	private String getLoginedMemberUsername() {
		if (isLogout()) return null;

		return user.getUsername(); // 로그인 상태인 경우 사용자명 반환
	}

	// 로그인 상태 메서드
	public boolean isLogin() {
		return user != null;
	}

	// 로그아웃 상태 메서드
	public boolean isLogout() {
		return !isLogin();
	}

	// 현재 로그인한 회원 정보를 가져오는 메서드
	public Member getMember() {
		if (isLogout()) {
			return null;
		}

		if (member == null) {
			member = memberService.findByUsername(getLoginedMemberUsername()).get();
		}

		return member;
	}

	// 현재 사용자가 관리자인지 확인하는 메서드
	public boolean isAdmin() {
		if (isLogout()) return false;

		return getMember().isAdmin();
	}

	// 세션 관련 함수
	// 세션에 속성을 설정하는 메서드
	public void setSession(String name, Object value) {
		session.setAttribute(name, value); 
	}

	// 세션에 속성을 설정하는 메서드, 기본값 설정 가능
	private Object getSession(String name, Object defaultValue) {
		Object value = session.getAttribute(name); // 세션에서 속성값 가져오기

		if (value == null) {
			return defaultValue;
		}

		return value;
	}

	// 세션에서 long 타입 속성값을 가져오는 메서드
	private long getSessionAsLong(String name, long defaultValue) {
		Object value = getSession(name, null); // 세션에서 속성값 가져오기

		if (value == null) return defaultValue;

		return (long) value;
	}

	// 세션에서 속성을 제거하는 메서드
	public void removeSession(String name) {
		session.removeAttribute(name);
	}

	// 쿠키 관련
	// 쿠키를 설정하는 메서드
	public void setCookie(String name, String value) {
		Cookie cookie = new Cookie(name, value);
		cookie.setPath("/");
		resp.addCookie(cookie);
	}

	// 쿠키에서 값을 가져오는 메서드
	private String getCookie(String name, String defaultValue) {
		Cookie[] cookies = req.getCookies();

		if (cookies == null) {
			return defaultValue;
		}

		for (Cookie cookie : cookies) {
			if (cookie.getName().equals(name)) {
				return cookie.getValue();
			}
		}

		return defaultValue;
	}

	// 쿠키에서 long 타입 값을 가져오는 메서드
	private long getCookieAsLong(String name, int defaultValue) {
		String value = getCookie(name, null);

		if (value == null) {
			return defaultValue;
		}

		return Long.parseLong(value);
	}

	// 쿠키를 제거하는 메서드
	public void removeCookie(String name) {
		Cookie cookie = new Cookie(name, "");
		cookie.setMaxAge(0);
		cookie.setPath("/");
		resp.addCookie(cookie);
	}


	// 모든 쿠키 값을 문자열로 반환하는 메서드
	public String getAllCookieValuesAsString() {
		StringBuilder sb = new StringBuilder();

		Cookie[] cookies = req.getCookies();
		if (cookies != null) {
			for (Cookie cookie : cookies) {
				sb.append(cookie.getName()).append(": ").append(cookie.getValue()).append("\n");
			}
		}

		return sb.toString();
	}

	// 세션에 저장 된 모든 값을 문자열로 반환하는 메서드
	public String getAllSessionValuesAsString() {
		StringBuilder sb = new StringBuilder();

		java.util.Enumeration<String> attributeNames = session.getAttributeNames();
		while (attributeNames.hasMoreElements()) {
			String attributeName = attributeNames.nextElement();
			sb.append(attributeName).append(": ").append(session.getAttribute(attributeName)).append("\n");
		}

		return sb.toString();
	}

	// 이전 페이지로 돌아가는 자바 스크립트를 실행하는 메서드
	public String historyBack(String msg) {
		// referer 헤더에서 이전 페이지의 URL을 가져온다
		// 사용자가 현재 페이지로 오기 전에 마지막으로 방문한 페이지의 URL
		String referer = req.getHeader ( "referer" );
		// referer URL 기반으로 키를 생성한다
		// 로컬 스토리지에 저장될 오류 메시지의 키로 사용됨
		String key = "historyBackFailMsg___" + referer;
		// 생성한 키를 요청 속성으로 설정한다
		// 클라이언트 사이트 스크립트에서 로컬 스토리지에 접근할 때 사용됨
		req.setAttribute("localStorageKeyAboutHistoryBackFailMsg", key);
		// 오류 메시지를 요청 속성으로 설정
		// 클라이언트 사이드에서 사용자에게 보여줄 수 있다
		req.setAttribute ( "historyBackFailMsg", msg );
		// 응답 코드를  400으로 설정
		resp.setStatus ( HttpServletResponse.SC_BAD_REQUEST );

		return "common/js";
	}

	// 특정 URL로 리다이렉트하는 메서드
	public String redirect(String url, String msg) {
		return "redirect:" + url + "?msg=" + Ut.url.encode(msg);
	}
}

Rq에는 회원정보, 세션, 쿠키정보 뿐만 아니라 리다이렉트를 할 수 있게 하는 정보가 있는데 이를 이용해
코드를 가독성있게 만들 수 있다.


history.back()이 실행이 되었을 때 실행되는 html js.html 생성


<!--회원가입에서 이미 사용중인 아디로 가입할 시 실행-->
<!-- Thymeleaf 템플릿 엔진을 사용하여 서버 측에서 설정한 변수를 클라이언트 측 JavaScript에서 사용하는 코드-->
<!-- 코드는 서버에서 전달한 오류 메시지를 클라이언트의 로컬 스토리지에 저장하는 기능을 수행 -->
<script th:inline="javascript">
	const msg = /*[[${msg}]]*/ null;
	if(msg) alert (msg);

	// 서버 측에서 설정한 'localStorageKeyAboutHistoryBackFailMsg' 값을 JavaScript 변수에 할당합니다.
	// Thymeleaf의 표현식을 사용하여 서버 측 변수 값을 가져옵니다. 값이 없으면 null을 할당합니다.
	const localStorageKeyAboutHistoryBackFailMsg = /*[[${localStorageKeyAboutHistoryBackFailMsg}]]*/ null;

	// 서버 측에서 설정한 'historyBackFailMsg' 값을 JavaScript 변수에 할당합니다.
	// 이것도 Thymeleaf의 표현식을 사용하여 값이 없을 때 null을 할당합니다.
	const historyBackFailMsg = /*[[${historyBackFailMsg}]]*/ null;

	// 'localStorageKeyAboutHistoryBackFailMsg' 변수에 값이 존재하고, 공백을 제거한 길이가 0보다 큰 경우
	// 즉, 변수가 유효한 문자열을 포함하고 있을 때 조건문 내의 코드가 실행됩니다.
	if (localStorageKeyAboutHistoryBackFailMsg && localStorageKeyAboutHistoryBackFailMsg.trim().length > 0) {
		// 로컬 스토리지에 'localStorageKeyAboutHistoryBackFailMsg' 변수의 값을 키로 하고,
		// 'historyBackFailMsg' 변수의 값을 값으로 하는 항목을 저장합니다.
		localStorage.setItem(localStorageKeyAboutHistoryBackFailMsg, historyBackFailMsg);
	}

	history.back();
</script>

Rq에서 redirectd가 실행이 되었을 때 js.html이 실행이 되는데 이를 위한 html이다


회원가입에서 메시지를 URL로 인코딩해주는 Ut.java 생성

public class Ut{
	public static class url(){
   	public static String encode(String message){
       // 주어진 message을 URL로 인코딩하고, 인코딩된 문자열을 반환한다
       	try{
           	return URLEncoder.encode(message, "UTF-8");
               } catch (UnsupportedEncodingException e) {
               	return null;
               }
       }
   }
}

회원가입에서 에러메시지나, 성공메시지를 URL에 message 형태로 나오게 해준다


모든 알림메시지를 alert가 아닌 toastr 알림을 위한 script


   // toastr 라이브러리의 설정 옵션을 정의합니다.
   toastr.options = {
       closeButton: true, // 알림에 닫기 버튼을 활성화합니다.
       debug: false, // 디버그 모드를 비활성화합니다.
       newestOnTop: true, // 새 알림을 상단에 표시합니다.
       progressBar: true, // 알림에 진행 상태 바를 표시합니다.
       positionClass: "toast-top-right", // 알림의 위치를 화면의 오른쪽 상단으로 설정합니다.
       preventDuplicates: false, // 중복 알림을 방지하지 않습니다.
       onclick: null, // 알림 클릭 시 실행할 함수를 null로 설정합니다. 즉, 클릭 이벤트가 없음을 의미합니다.
       showDuration: "300", // 알림이 나타나는 데 걸리는 시간을 300ms로 설정합니다.
       hideDuration: "1000", // 알림이 사라지는 데 걸리는 시간을 1000ms로 설정합니다.
       timeOut: "5000", // 알림이 자동으로 사라지기까지의 시간을 5000ms로 설정합니다.
       extendedTimeOut: "1000", // 알림에 마우스를 올렸을 때 사라지기까지 추가로 대기하는 시간을 1000ms로 설정합니다.
       showEasing: "swing", // 알림이 나타날 때의 easing 효과를 "swing"으로 설정합니다.
       hideEasing: "linear", // 알림이 사라질 때의 easing 효과를 "linear"으로 설정합니다.
       showMethod: "fadeIn", // 알림이 나타나는 방식을 "fadeIn" 효과로 설정합니다.
       hideMethod: "fadeOut" // 알림이 사라지는 방식을 "fadeOut" 효과로 설정합니다.
   };

   // 성공 알림을 표시하는 함수를 정의합니다.
   function toastNotice(msg) {
       toastr["success"](msg, "알림"); // toastr 라이브러리의 success 메서드를 사용하여 "알림" 제목으로 성공 메시지를 표시합니다.
   }

   function toastMsg(isNotice, msg){
       if(isNotice) toastNotice(msg);
       else toastWarning(msg);
   }

   // 경고 알림을 표시하는 함수를 정의합니다.
   function toastWarning(msg) {
       toastr["warning"](msg, "알림"); // toastr 라이브러리의 warning 메서드를 사용하여 "알림" 제목으로 경고 메시지를 표시합니다.
   }


   // 메시지의 유효성을 파싱하는 함수를 정의합니다.
   function parseMsg(msg) {
       const [pureMsg, ttl] = msg.split(";ttl="); // 메시지를 ";ttl=" 기준으로 분리하여 순수 메시지와 ttl(time-to-live) 값을 추출합니다.

       const currentJsUnixTimestamp = new Date().getTime(); // 현재 시간의 Unix 타임스탬프를 밀리초 단위로 가져옵니다.

       // ttl 값이 있고, 파싱된 ttl 값에 5000ms를 더한 값이 현재 시간보다 작다면 메시지가 만료된 것으로 판단합니다.
       if (ttl && parseInt(ttl) + 5000 < currentJsUnixTimestamp) {
           return [pureMsg, false]; // 만료된 경우 순수 메시지와 함께 false를 반환합니다.
       }

       return [pureMsg, true]; // 만료되지 않았다면 순수 메시지와 함께 true를 반환합니다.
   }

	// 아이디 중복체크에서 사용되는 코드
   // 어떠한 기능을 살짝 늦게(0.1 초 미만)
   function setTimeoutZero(callback) {
       setTimeout(callback);
   }

모든 html에서 사용되는 toastr을 위해 따로 common.js파일을 만들어서 생성을 하였지만
common.js는 공용 js파일이여서 다른 코드들이 추가가 가능하다


정보를 더 쉽게 알 수 있게 해주는 RsData.java 생성

@Getter
@AllArgsConstructor
public class RsData<T> { // 제네릭 타입 T를 사용하는 RsData 선언
	private String resultCode;
	private String msg;
	private T data;

	public static <T> RsData<T> of(String resultCode, String msg, T data) {
		// resultCode, msg, data를 매개변수로 받아 RsData 객체를 생성하고 반환하는 정적 팩토리 메서드
		return new RsData<>(resultCode, msg, data);
		// RsData 객체를 생성하고 반환
	}

	public static <T> RsData<T> of(String resultCode, String msg) {
		 // resultCode, msg를 매개변수로 받아 data 필드를 null로 가지는 RsData 객체를 생성하고 반환하는 정적 팩토리 메서드
		return of(resultCode, msg, null);
		// 위에 정의된 of 메서드를 재사용하여 RsData 객체를 생성하여 반환
	}

	public boolean isSuccess() {
		return resultCode.startsWith("S-");
		// S- 로 시작하면 성공을 반환하는 메서드
	}

	public boolean isFail() {
		return !isSuccess();
		// S-로 그 외로 시작하면 실패를 반환하는 메서드
	}
}

RsData를 이용하여 성공과 실패메시지를 정확하게 가독성있게 표현이 가능하다


Member.java 생성

@Entity
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Member {
	@EqualsAndHashCode.Include
	@Id
	@GeneratedValue(strategy = IDENTITY)
	private Long id;
	@Column(unique = true)
	private String username;
	@Setter
	private String password;
	@Setter
	private String nickname;
	@Setter
	private String email;
	@Setter
	@Enumerated ( jakarta.persistence.EnumType.STRING)
//	열거형을 상수가 아닌 이름으로 sql에 나타내고 싶을 때 사용하면 됨
	private Jop jop;

	public boolean isAdmin() {
		return "admin".equals(username);
	}

	public List<? extends GrantedAuthority> getGrantedAuthorities() {
		List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

		// 모든 멤버는 member 권한을 가진다.
		grantedAuthorities.add(new SimpleGrantedAuthority("member"));

		// username이 admin인 회원은 추가로 admin 권한도 가진다.
		if (isAdmin()) {
			grantedAuthorities.add(new SimpleGrantedAuthority("admin"));
		}

		return grantedAuthorities;
	}
}

MemberContorller.java 생성

@Controller // Spring MVC 컨트룰러로 선언
@RequestMapping ( "/usr/member" ) // 이 컨트룰러의 모든 핸들러 메서드에 대한 기본 URL 지정
@RequiredArgsConstructor // final이나 @NonNull 필드에 대한 생성자 자동 생성
public class MemberController {

	private final MemberService memberService;
	private final Rq rq;

	@GetMapping("/join") // "usr/member/join" Get 요청이 오면 실행
	public String showJoin() {
		return "usr/member/join";
	}
	// usr/member/join.html 반환

	@PostMapping("/join") // usr/member/join 으로 POST 요청이오면 실행
	public String join( @Valid JoinForm joinForm) { // 클라이언트로부터 전달받은 JoinForm 객체 검증
		RsData<Member> joinRs = memberService.join(joinForm.getUsername(), joinForm.getPassword(), joinForm.getNickname(), joinForm.getEmail (), joinForm.getJop ());
		// MemberService를 통해 가입을 수행하고 결과를 받는다
		if (joinRs.isFail ()) { // 가입이 실패할 시 historyBack 실행
			return rq.historyBack ( joinRs.getMsg () ); // 실패 메시지 표현
		}

		return rq.redirect ( "/",joinRs.getMsg ());
		// 성공후 메인페이지로 이동후 성공 메시지 반환
	}

	// 아이디 중복 체크
	@GetMapping("/checkUsernameDup")
	@ResponseBody // 이 메시지의 반환 값은 응답 본문에 직접 작성
	public RsData checkUsernameDup(String username){ // 사용자 이름 중복 확인 요청을 처리
		return memberService.checkUsernameDup(username); // MemberService를 통해 사용자 이름의 중복 여부를 확인하고 결과를 반환
	}

	@InitBinder
	public void initBinder( WebDataBinder binder) {
//		WebDataBinder를 사용하여 요청으로부터 넘어오는 데이터를  Java  객체에 바인딩할 때 사용자는 설정을 커스터마이징한다
		binder.registerCustomEditor(Jop.class, new java.beans.PropertyEditorSupport () {
			@Override
			public void setAsText(String text) {
//				문자열 형태로 넘어온 값을 Jop 열거형으로 변환하는 로직을 정의
				// 예를 들어, "PHYSIOTHERAPIST" 문자열이 넘어오면 Jop.PHYSIOTHERAPIST 열거형 값으로 변환하여 설정합니다.
				setValue(Jop.valueOf(text.toUpperCase()));
			}
		});
	}

	@Getter
	@AllArgsConstructor
	public static class JoinForm {
		@NotBlank
		private String username;
		@NotBlank
		private String password;
		@NotBlank
		private String nickname;
		@NotBlank
		private String email;
		@NotNull
		private Jop jop; // 직업을 나타내는 열거형
	}
}

Controller에서는 회원가입 페이지로 가는 Get join, 회원가입을 실질적으로 성공하게 해주는 POST join하고 아이디 중복체크를 가능하게 해주는 checkUsernameDu가 있다


MemberService.java 생성

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // 메서드들이 트랜잭션 내에서 실행됨을 나타내며, 일반적으로 읽기전용 트랜잭션을 사용
public class MemberService {
	private final MemberRepository memberRepository;
	private final PasswordEncoder passwordEncoder;

	@Transactional // 이 메서드에서 수행되는 데이터베이스 작업을 트랜잭션으로 관리
	public RsData<Member> join( String username, String password, String nickname, String email, Jop jop ) {

		if(findByUsername ( username ).isPresent ()) // 제공된 사용자 이름으로 기존회원을 검색
			return RsData.of ( "F-1", "%s(은)는 사용중인 아이디 입니다" .formatted ( username ));
		// 존재할 시 에러메시지 구현

		Member member = Member // 빌더 형식으로 mebmer 객체 생성
				.builder()
				.username(username) // 사용자 이름 설정
				.password(passwordEncoder.encode(password)) // 비밀번호 암호화하여 설정
				.nickname(nickname) // 별명 설정
				.email ( email ) // 이메일 설정
				.jop ( jop ) // 직업 설정
				.build(); // member 객체를 빌드

		member = memberRepository.save(member); // 생성된 mebmer객체를 데이터베이스에 저장

		return RsData.of ( "S-1", "회원가입이 완료되었습니다", member ); // 성공 응답과 함께 member 객체 반환
	}

	public Optional < Member> findByUsername( String username) {
		return memberRepository.findByUsername(username);
		// 사용자 이름을 검색하고 없는 경우 Optional.empty()를 반환
	}

	public Optional < Member> findById( long id) {
		return memberRepository.findById(id);
		// 사용자 아이디을 검색하고 없는 경우 Optional.empty()를 반환
	}


	public RsData checkUsernameDup(String username) { // 아이디 중복 체크 관련 성공과 실패 응답 메시지 반환
		// RsData에서 Member 객체를 반환하지 않는 이유는 여기서는 단순한 성공 실패 응답 메시지만 반환 하면 되기 때문이다
		if (findByUsername(username).isPresent()) return RsData.of("F-1", "%s(은)는 사용중인 아이디입니다.".formatted(username));

		return RsData.of("S-1", "%s(은)는 사용 가능한 아이디입니다.".formatted(username));
		
	}
}

MemberRepository.java 생성

public interface MemberRepository extends JpaRepository<Member, Long> {
	// JpaRepository에서 기본적으로 제공하는 메서드
	// save() : 업데이트
	// findById() : 아이디 찾기
	// finalAll()  : 모든 엔티티의 리스트 반환
	// deleteById() // 주어진 아이디에 해당하는 엔티티 삭제
	// count() : 총 개수를 반환
	Optional<Member> findByUsername(String username);
}

MemberRepository 인터페이스는 Spring Data JPA를 사용하여 데이터베이스와의 상호작용을 추상화한 것이다. 이 인터페이스가 JpaRepository<Member, Long>를 확장함으로써, Member 엔티티에 대한 기본적인 CRUD(Create, Read, Update, Delete) 작업을 손쉽게 수행할 수 있는 메서드를 자동으로 제공받는다. 여기서 Long은 Member 엔티티의 기본 키(Primary Key)의 타입을 나타낸다


join.html 생성

<!doctype html>
<html layout:decorate = "~{usr/common/layout}" >

<head >
	<title >회원가입</title >
</head >

<body >
<!-- 회원가입  -->
<section class = "m-8 flex" layout:fragment="content">
<!-- 로그인 이미지	-->
	<div class = "w-2/5 h-full hidden lg:block" >
		<img src = "https://image.musinsa.com/mfile_s01/2019/03/18/156005c5baf40ff51a327f1c34f2975b203251.jpg"
			 alt = "로그인 이미지" class = "h-full w-full object-cover rounded-3xl" >
	</div >
<!--  회원가입 공간	 -->
	<div class = "w-full lg:w-3/5 flex flex-col items-center justify-center" >
		<div class = "text-center" >
			<h2 class = "block antialiased tracking-normal font-sans text-4xl leading-[1.3] text-inherit font-bold mb-4" >
				HealMingle
			</h2 >
			<h3 class = "block antialiased tracking-normal font-sans text-4xl leading-[1.3] text-inherit font-bold mb-4" >
				Sing up
			</h3 >
		</div >
		<form th:action method="POST" name="join-form" onsubmit="submitJoinForm(this); return false;" class = "mt-8 mb-2 mx-auto w-80 max-w-screen-lg lg:w-1/2" >
			<div class = "mb-1 flex flex-col gap-6" >
				<!--      아이디    -->
				<div class = "w-full" >
					<input type="text" name="username" placeholder="username" class=" w-full input input-bordered" autofocus
						   maxlength="30" onchange="$(this).keyup();" onpaste="setTimeoutZero(() => $(this).keyup());"
						   onkeyup="checkUsernameDupDebounce();">
					<div class="mt-2 text-sm"></div>
				</div >

				<!--      비밀번호    -->
				<label class = "w-full" >
					<input type = "password" class = "w-full input input-bordered " placeholder = "password" name = "password" />
				</label >

				<!--      비밀번호 입력 확인    -->
				<label class = "w-full" >
					<input type = "password" class = "w-full input input-bordered" placeholder = "password check" name = "passwordConfirm" />
				</label >

				<!--      이메일    -->
				<label class = "w-full" >
					<input type = "email" class = "w-full input input-bordered" placeholder = "email" name = "email" />
				</label >

				<!--      별명    -->
				<label class = "w-full" >
					<input type = "text" class = "w-full input input-bordered" placeholder = "nickname" name = "nickname" />
				</label >

				<!--      직업    -->
				<div class = "w-full" >
					<select class = "select select-bordered w-full font-semibold" name = "jop" >
						<!-- html에서 name이랑 back에서 정보를 받는 이름이 다를 시 정보가 넘어가지 않고 null로 넘어감-->
						<option disabled selected >Choose a jop</option >
						<option value = "PHYSIOTHERAPIST" >물리치료사</option >
						<option value = "MANUAL_THERAPIST" >도수치료사</option >
						<option value = "NURSE" >간호사</option >
						<option value = "NURSING_ASSISTANT" >간호조무사</option >
						<option value = "DOCTOR" >의사</option >
					</select >
				</div >
				<button class = "custom-btn btn-5 w-full bg-black text-white" ><span >Sing in</span >
				</button >
				<!--      로그인 그외 페이지 이동경로는 나중에 추가 예정    -->
			</div >
		</form >
	</div >

	<script >
		function checkUsernameDup(form) {
			 form.username.value = form.username.value.trim();

			 if ( form.username.value.length == 0 ) {
				 clearUsernameInputMsg();
				 return;
			 }

			 if ( form.username.value.length < 4 ) {
				 clearUsernameInputMsg();
				 return;
			 }

			 if ( validUsername == form.username.value ) return;

			 if ( lastCheckedUsername == form.username.value ) return;

			 lastCheckedUsername = form.username.value;

			 clearUsernameInputMsg();

			 fetch(
				 'checkUsernameDup?username=' + form.username.value
			 )
				 .then(res => res.json())
				 .then((rsData) => {
					 if ( rsData.success ) {
						 validUsername = form.username.value;
					 }

					 setUsernameInputMsg(rsData.success, rsData.msg);
				 });
		 }

		 const joinForm = document['join-form'];

		 const checkUsernameDupDebounce = _.debounce(() => checkUsernameDup(joinForm), 500);

		 function clearUsernameInputMsg() {
			 $(joinForm.username).removeClass('input-info  input-error');
			 $(joinForm.username).next().removeClass('text-sky-300	 text-red-400');
			 $(joinForm.username).next().empty();
		 }

		 function setUsernameInputMsg(isSuccess, msg) {
			 if ( isSuccess ) $(joinForm.username).addClass('input-info ');
			 if ( !isSuccess ) $(joinForm.username).addClass('input-error');

			 $(joinForm.username).next().addClass(isSuccess ? 'text-sky-300	' : 'text-red-400');

			 $(joinForm.username).next().text(msg);
		 }

		 let validUsername = '';
		 let lastCheckedUsername = '';
		 let submitJoinFormDone = false;
	 function submitJoinForm(form) {

		 if ( submitJoinFormDone ) return;
		// username 이(가) 올바른지 체크

		form.username.value = form.username.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

		if (form.username.value.length == 0) {
			form.username.focus();
			toastWarning('아이디를 입력해주세요.');
			return;
		}

		if (form.username.value.length < 4) {
			form.username.focus();
			toastWarning('아이디를 4자 이상 입력해주세요.');
			return;
		}

		// password 이(가) 올바른지 체크

		form.password.value = form.password.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

		if (form.password.value.length == 0) {
			form.password.focus();
			toastWarning('비밀번호를 입력해주세요.');
			return;
		}

		if (form.password.value.length < 4) {
			   form.password.focus();
			toastWarning('비밀번호를 4자 이상 입력해주세요.');
			return;
		}

		// passwordConfirm 이(가) 올바른지 체크

		form.passwordConfirm.value = form.passwordConfirm.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

		if (form.passwordConfirm.value.length == 0) {
			form.passwordConfirm.focus();
			toastWarning('비밀번호 확인(을)를 입력해주세요.');
			return;
		}

		if (form.passwordConfirm.value.length < 4) {
			 form.passwordConfirm.focus();
			 toastWarning('비밀번호 확인을(를) 4자 이상 입력해주세요.');
			 return;
		}

		if ( form.password.value != form.passwordConfirm.value ) {
			 form.passwordConfirm.focus();
			 toastWarning('비밀번호를 확인이 일치하지 않습니다.');
			 return;
		}

		 // email 이(가) 올바른지 체크

		form.email.value = form.email.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

		if (form.email.value.length == 0) {
			form.email.focus();
			toastWarning('이메일을(를) 입력해주세요.');
			return;
		}

		if (form.email.value.length < 4) {
			form.email.focus();
			toastWarning('이메일을(를) 4자 이상 입력해주세요.');
			return;
		}

		// nickname 이(가) 올바른지 체크

		form.nickname.value = form.nickname.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

		if (form.nickname.value.length == 0) {
			form.nickname.focus();
			toastWarning('별명을 입력해주세요.');
			return;
		}

		if (form.nickname.value.length < 4) {
			form.nickname.focus();
			toastWarning('별명을 4자 이상 입력해주세요.');
			return;
		}

		 form.jop.value = form.jop.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

		  if (form.jop.value === "Choose a jop" || form.jopChoose.value === "") {
			 form.jop.focus();
			 toastWarning('직업을 선택해주세요.');
			 return;
		 }

		  if ( validUsername != form.username.value ) {
			 $(form.username).next().focus();
			 toastWarning('아이디 중복체크를 해주세요.');
			 return;
		  }

		form.submit(); // 폼 발송
		submitJoinFormDone = true;
	}
	</script >

</section >

</body >
</html >
  // autofocus : 페이지 로딩 시 이 필드에 자동으로 포커스를 맞춘다
  // maxlength = 30 : 사용자가 입력할 수 있는 최대 문자수
  // : 필드의 값이 변경될 때마다 'keyup' 이벤트를 강제로 발생시키는 jQuery 코드
  // : 사용자가 필드에 텍스트를 붙여넣을 때 keyup()이벤트를 발생시킬 때 속도를 늦추기 위한 코드
  // onkeyup = "checkUsernameDupDebounce()" : 사용자가 키를 눌렀다 떼었을 때 호출되는 함수
  // 사용자가 입력할 때마다 중복 검사를 수행하는 함수를 디바운스 처리하여 호출한다

layout.html 생성

<!doctype html>
<html lang = "ko" >
<head >
	<meta charset = "UTF-8" >
	<meta name = "viewport"
		  content = "width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" >
	<meta http-equiv = "X-UA-Compatible" content = "ie=edge" >
	<title layout:title-pattern = "$CONTENT_TITLE | $LAYOUT_TITLE" th:text = "Heal_Mingle" ></title >

	<!-- 제이쿼리 불러오기 -->
	<script src = "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" ></script >
	<!-- Lodash는 _ 객체를 통해 함수형 프로그래밍을 지원하는 메서드를 제공-->
	<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>

	<!-- toastr 불러오기 -->
	<script src = "https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js" ></script >
	<link rel = "stylesheet" href = "https://jhs512.github.io/toastr/toastr.css" >

	<!-- 폰트어썸 아이콘 -->
	<link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" >

	<!-- 테일윈드 , 데이지 UI-->
	<link href = "https://cdn.jsdelivr.net/npm/daisyui@3.7.7/dist/full.css" rel = "stylesheet" type = "text/css" />
	<script src = "https://cdn.tailwindcss.com" ></script >

	<!-- 글꼴 -->
	<link href = "https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel = "stylesheet" />

	<!--  풀페이지 css, js -->
	<script src = "https://cdnjs.cloudflare.com/ajax/libs/fullPage.js/3.1.0/fullpage.min.js"
			integrity = "sha512-HqbDsHIJoZ36Csd7NMupWFxC7e7aX2qm213sX+hirN+yEx/eUNlZrTWPs1dUQDEW4fMVkerv1PfMohR1WdFFJQ=="
			crossorigin = "anonymous" ></script >
	<link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/fullPage.js/3.1.0/fullpage.css"
		  integrity = "sha512-hGBKkjAVJUXoImyDezOKpzuY4LS1eTvJ4HTC/pbxn47x5zNzGA1vi3vFQhhOehWLTNHdn+2Yqh/IRNPw/8JF/A=="
		  crossorigin = "anonymous" />

	<script src="/resource/common/common.js"></script>

	<!-- 버튼, 글꼴 스타일 -->
	<style >
		@font-face {
			font-family: 'GmarketSansMedium';
			src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2001@1.1/GmarketSansMedium.woff') format('woff');
			font-weight: normal;
			font-style: normal;
		}

		html > body {
			font-family: "GmarketSansMedium";
			text-underline-position: under;
		}

		  button {
		  margin: 20px;
		  outline: none;
		}
		.custom-btn {
		  height: 40px;
		  padding: 10px 25px;
		  border: 2px solid #000;
		  background: transparent;
		  cursor: pointer;
		  transition: all 0.3s ease;
		  position: relative;
		  display: inline-block;
		}

		/* 5 */
		.btn-5 {
		  line-height: 42px;
		  padding: 0;
		  border: none;
		}
		.btn-5:hover {
		  background: transparent;
		  color: #000;
		  box-shadow: -7px -7px 20px 0px #fff9, -4px -4px 5px 0px #fff9,
			7px 7px 20px 0px #0002, 4px 4px 5px 0px #0001;
		}
		.btn-5:before,
		.btn-5:after {
		  content: "";
		  position: absolute;
		  top: 0;
		  right: 0;
		  height: 2px;
		  width: 0;
		  background: #000;
		  transition: 400ms ease all;
		}
		.btn-5:after {
		  right: inherit;
		  top: inherit;
		  left: 0;
		  bottom: 0;
		}
		.btn-5:hover:before,
		.btn-5:hover:after {
		  width: 100%;
		  transition: 800ms ease all;
		}

	</style >
</head >


<body >

<div >
	<header >
		<!-- navbar -->
		<div class = "navbar shadow-md shadow-inner flex justify-between" >
			<!--  logo  -->
			<div class = "p-2 w-72 h-16" >
				<img src = "/resource/logoImg/NoColorLogo.png" alt = "로고" >
			</div >
			<!--  menu  -->
			<div class = "flex justify-evenly w-full text-lg" >
				<div >
					<a href = "#" >자유 게시판</a >
				</div >
				<div >
					<a href = "#" >공지사항</a >
				</div >
			</div >
			<!--  profile img/ search input  -->
			<div class = "p-3" >
				<div class = "flex items-center md:ml-auto md:pr-4" >
					<div class = "relative flex flex-wrap items-stretch w-full transition-all rounded-lg ease-soft" >
						<input type = "text"
							   class = "pl-3 text-sm focus:outline-slate-500 placeholder:text-slate-500 ease-soft w-1/100 leading-5.6 relative -ml-px block min-w-0 flex-auto rounded-lg border border-solid  bg-clip-padding py-2 pr-3 transition-all focus:outline-none focus:transition-shadow"
							   placeholder = "🔍     궁금을 검색하세요" />
					</div >
				</div >
				<div class = "dropdown dropdown-end" >
					<div tabindex = "0" role = "button" class = "btn btn-ghost btn-circle avatar" >
						<div class = "w-10 rounded-full" >
							<img alt = "Tailwind CSS Navbar component"
								 src = "https://daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg" />
						</div >
					</div >
					<ul tabindex = "0"
						class = "menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52" >
						<li >
							<a class = "justify-between" >
								내 정보
							</a >
						</li >
						<li ><a >관리자</a ></li >
						<li ><a href = "/usr/member/join" >회원가입</a ></li >
						<li ><a >로그인</a ></li >
						<li ><a >로그아웃</a ></li >
					</ul >
				</div >
			</div >
		</div >
	</header >
	<!-- fragment -->
	<main class = "mb-3" >
		<th:block layout:fragment = "content" ></th:block >
	</main >
</div >

<script>
	// URL에서 msg 파라미터의 값을 가져오는 함수
    function getMsgFromURL() {
        const url = new URL(window.location.href);
        return url.searchParams.get('msg');
    }

    function getFailMsgFromURL() {
        const url = new URL(window.location.href);
        return url.searchParams.get('failMsg');
    }

    const msg = getMsgFromURL();

    // msg 파라미터의 값이 있으면 toastr로 알림을 표시
    if (msg) {
        toastNotice(decodeURIComponent(msg));
    }

    const failMsg = getFailMsgFromURL();

    // msg 파라미터의 값이 있으면 toastr로 알림을 표시
    if (failMsg) {
        toastWarning(decodeURIComponent(failMsg));
    }

	// historyBack 으로 인해 돌아온 경우 실행
	// 페이지가 표시될 때마다 특정 조건 하에 로컬 스토리지에서 오류 메시지를 검색하여 화면에 표시하고, 해당 메시지를
	// 로컬 스토리지에서 제거하는 기능을 수행
	// 페이지가 새로 로드되거나 캐시에서 복원될 때 실행되는 이벤트 핸들러를 바인드합니다.
	$(window).bind("pageshow", function (event){
	  // 현재 페이지의 URL을 사용하여 로컬 스토리지 키를 생성합니다.
	  let localStorageKeyAboutHistoryBackFailMsg = "historyBackFailMsg___" + location.href;

	  // 해당 키에 대한 항목이 로컬 스토리지에 없는 경우, 'null'을 사용하여 기본 키를 설정합니다.
	  if (!localStorage.getItem(localStorageKeyAboutHistoryBackFailMsg)) {
		localStorageKeyAboutHistoryBackFailMsg = "historyBackFailMsg___null";
	  }

	  // 설정된 키를 사용하여 로컬 스토리지에서 오류 메시지를 검색합니다.
	  const historyBackFailMsg = localStorage.getItem(localStorageKeyAboutHistoryBackFailMsg);
	  // 오류 메시지가 존재하는 경우
	  if (historyBackFailMsg) {
		toastWarning(historyBackFailMsg);
		// 오류 메시지를 화면에 표시합니다(toastr 라이브러리 사용).
		localStorage.removeItem(localStorageKeyAboutHistoryBackFailMsg);
		// 메시지를 표시한 후 해당 항목을 로컬 스토리지에서 제거합니다.
	  }
	});
</script>

</body >
</html >

layout.html은 공통적으로 사용되고 중복적인 부분을 최소화하기 위해 만든 것이다


  • 회원가입 페이지

  • 아이디 중복체크 성공

  • 아이디 중복체크 실패

  • 회원가입시 실행되는 에러메시지 중 하나

  • 회원가입 완료 메시지

  • 회원가입 성공 후 데이터베이스

profile
같은글이있어도양해부탁드려요(킁킁)

0개의 댓글