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에는 회원정보, 세션, 쿠키정보 뿐만 아니라 리다이렉트를 할 수 있게 하는 정보가 있는데 이를 이용해
코드를 가독성있게 만들 수 있다.
<!--회원가입에서 이미 사용중인 아디로 가입할 시 실행-->
<!-- 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이다
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 형태로 나오게 해준다
// 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파일이여서 다른 코드들이 추가가 가능하다
@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를 이용하여 성공과 실패메시지를 정확하게 가독성있게 표현이 가능하다
@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;
}
}
@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가 있다
@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));
}
}
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)의 타입을 나타낸다
<!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()" : 사용자가 키를 눌렀다 떼었을 때 호출되는 함수
// 사용자가 입력할 때마다 중복 검사를 수행하는 함수를 디바운스 처리하여 호출한다
<!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은 공통적으로 사용되고 중복적인 부분을 최소화하기 위해 만든 것이다