이번 프로젝트는 mybatis,vue js 를 사용했다.
우선 기본 회원가입(프로필 이미지 제외)을 위해 mapper를 만들었다.
Q) DAO대신 repo를 사용하여 mybatis를 사용하는 이유는?
- mybatis는 mapper인터페이스를 제공한다.
- mapper를 사용하면 일일이 DAO를 만들지 않고 인터페이스만을 이용해서 좀 더 편하게 개발할 수 있다.
- mybatis는 자바코드와 sql문을 분리하여 편리하게 관리하도록 한다.
<member-mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!-- ↑ 이것은 XML 헤더라고 한다(반드시 첫 번째 줄에 위치) -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- ↑ mapper 문법을 인식할 수 있는 문법 정보 import -->
<!-- mapper에는 SQL 구문을 작성하고 namespace는 구분을 위한 별칭 -->
<mapper namespace="member">
<!--
select 구문을 select 태그에 작성
- id : 외부에서 부를 이름
- resultType : 이 구문을 통해서 만들 결과 형태(자동 변환 결과) - select 전용(출력해서 봐야하니깐 반환형 dto),update,delete,insert는 입력만 해서 필요없음
- parameterType : 이 구문을 실행하기 위해 필요한 형태(생략 가능)
-->
<!-- 로그인 -->
<select id="memberLogin" resultType="MemberDto">
select * from member where member_id=#{memberId}
</select>
<!-- 회원가입 -->
<insert id="memberJoin">
insert into member(member_id,member_pw,member_name,member_level,member_email,member_gender,member_manner,member_birth)
values(#{memberId},#{memberPw},#{memberName},DEFAULT,#{memberEmail},#{memberGender},DEFAULT,#{memberBirth})
</insert>
</mapper>
<membeRepo>
package com.kh.finalkh11.repo;
import com.kh.finalkh11.dto.MemberDto;
public interface MemberRepo {
MemberDto selectOne(String memberId);//로그인
void insert(MemberDto memberDto);//회원가입
}
<memberRepoImpl>
package com.kh.finalkh11.repo;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.kh.finalkh11.dto.MemberDto;
@Repository
public class MemberRepoImpl implements MemberRepo{
@Autowired
private SqlSession sqlSession;
@Override
public MemberDto selectOne(String memberId) {
return sqlSession.selectOne("member.memberLogin",memberId);
}
@Override
public void insert(MemberDto memberDto) {
sqlSession.insert("member.memberJoin",memberDto);
}
}
<memberController>
package com.kh.finalkh11.controller;
import java.io.IOException;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.kh.finalkh11.dto.MemberDto;
import com.kh.finalkh11.repo.ImgRepo;
import com.kh.finalkh11.repo.MemberRepo;
@Controller
@RequestMapping("/member")
public class MemberController {
@Autowired
private MemberRepo memberRepo;
@Autowired
private ImgRepo imgRepo;
//로그인
@GetMapping("/login")
public String login() {
return "member/login";
}
@PostMapping("/login")
public String login(HttpSession session,@ModelAttribute MemberDto userDto) {
//userDto = 사용자가 입력한 dto, findDto = 찾은 dto
//로그인 검사 : 아이디 찾고, 비밀번호 일치 비교
MemberDto findDto = memberRepo.selectOne(userDto.getMemberId());
//존재하지 않는 아이디라면 -> redirect(get방식이여서 login페이지로 이동가능)
if(findDto == null) {
return "redirect:login";
}
//비밀번호가 일치않지 않는다면 ->오류
if(!userDto.getMemberPw().equals(findDto.getMemberPw())) {
return "redirect:login";
}
//로그인에 성공한 경우 session에 추가
session.setAttribute("memberId", findDto.getMemberId());
session.setAttribute("memberLevel", findDto.getMemberLevel());
return "redirect:/";//메인페이지로 이동
}
//회원가입
@GetMapping("/join")
public String join() {
return "member/join";
}
@PostMapping("/join")
public String join(@ModelAttribute MemberDto memberDto) throws IllegalStateException, IOException {
//memberDto 등록
memberRepo.insert(memberDto);
// if(!multipartFile.isEmpty()) {
// //2.첨부파일 저장 및 등록(첨부파일이 있으면)
// int attachmentNo = imgRepo.sequence();
//
// File dir = new File("D:/upload");
// dir.mkdirs();
//
// File target = new File(dir, String.valueOf(attachmentNo));
// multipartFile.transferTo(target);//저장
//
// imgRepo.insert(imgDto);
//
// //3.memberDto랑 첨부파일 정보를 연결(첨부파일이 있으면)
// memberRepo.insert(memberDto);
// }
return "redirect:joinFinish";
}
@GetMapping("/joinFinish")
public String joinFinish() {
return "member/joinFinish";
}
@GetMapping("/jointerm")//약관페이지
public String jointerm() {
return "member/jointerm";
}
@GetMapping("/joinprivacy")//개인정보페이지
public String joinprivacy() {
return "member/joinprivacy";
}
}
++ 아이디 중복검사를 실시간으로 하기위해 restcontroller를 하나 만들었다.
++ 실행 순서 : controller -> view -> restcontroller
<memberRestController>
package com.kh.finalkh11.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.kh.finalkh11.repo.MemberRepo;
import lombok.extern.slf4j.Slf4j;
//회원아이디와 관련된 비동기 처리(아이디 중복검사)
@Slf4j
@RestController
@RequestMapping("/rest/member")
public class MemberRestController {
@Autowired
private MemberRepo memberRepo;
//사용가능하면(없으면) Y
//사용불가하면(있으면) N
@GetMapping("/{memberId}")
public String findId(@PathVariable String memberId) {
// log.debug("result = {}, {}", memberRepo.selectOne(memberId), memberRepo.selectOne(memberId) == null);
String tep = memberRepo.selectOne(memberId) == null ? "Y":"N";
return tep;
}
}
**@PathVariable 이란?
REST API에서 URI에 변수가 들어가는걸 실무에서 많이 볼 수 있다.
예를 들면, 아래 URI에서 밑줄 친 부분이 @PathVariable로 처리해줄 수 있는 부분이다.
http://localhost:8080/api/user/1234
https://music.bugs.co.kr/album/4062464
사용법
Controller에서 아래와 같이 작성하면 간단하게 사용 가능하다.
- @GetMapping(PostMapping, PutMapping 등 다 상관없음)에 {변수명}
- 메소드 정의에서 위에 쓴 변수명을 그대로 @PathVariable("변수명")
- (Optional) Parameter명은 아무거나 상관없음(아래에서 String name도 OK, String employName도 OK
@RestController
public class MemberController {
// 기본
@GetMapping("/member/{name}")
public String findByName(@PathVariable("name") String name ) {
return "Name: " + name;
}
// 여러 개
@GetMapping("/member/{id}/{name}")
public String findByNameAndId(@PathVariable("id") String id, @PathVariable("name") String name) {
return "ID: " + id + ", name: " + name;
}
}
프론트엔드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원가입</title>
<!--아이콘 CDN-->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
<!-- bootswatch cdn-->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.2.3/litera/bootstrap.min.css" rel="stylesheet" >
<style>
.jcontainer{
clear: both;
margin: 0 auto;
width: 100%;
zoom: 1;
width:650px;
}
.text-size{
font-size: small;
margin-bottom: 5px;
}
</style>
</head>
<body>
<div class="jcontainer" id="app">
<div class="container-fluid mt-4">
<div class="row">
<div class="offset-md-2 col-md-8">
<!-- 문서 제목 (Jumbotron) -->
<div class="row text-center">
<div class="col">
<h1>MATCH-UP</h1>
</div>
</div>
<form action="join" method="post" enctype="multipart/form-data">
<div class="inner">
<div class="row mt-4">
<div class="col">
<label class="text-size">이름</label>
<input class="form-control rounded" name="memberName" type="text" placeholder="이름 입력"
v-model="memberName" :class="checkName">
<div class="valid-feedback"></div>
<div class="invalid-feedback">한글 이름 2~5자 이내로 입력해주세요.</div>
</div>
</div>
<div class="row mt-4">
<div class="col">
<label class="text-size">아이디</label>
<input class="form-control rounded" id="memberId" name="memberId" type="text" placeholder="아이디 입력"
v-model="memberId" :class="checkId" @blur="IdCheck">
<div class="valid-feedback" id="idValidCheck">사용할 수 있는 아이디입니다.</div>
<div class="invalid-feedback" id="idInValidCheck"></div>
</div>
</div>
<div class="row mt-4">
<div class="col">
<label class="text-size">비밀번호</label>
<input class="form-control" name="memberPw" type="password" placeholder="비밀번호 입력"
v-model="memberPw" :class="checkPw">
<div class="valid-feedback">사용할 수 있는 비밀번호입니다.</div>
<div class="invalid-feedback">최소한 한개의 대문자,소문자,숫자,특수문자를 포함하여 8~16 사이여야 합니다.</div>
</div>
<div class="col">
<label class="text-size">비밀번호 확인</label>
<input class="form-control rounded" name="memberPwCheck" type="password" placeholder="비밀번호 확인"
v-model="memberPwCheck" :class="checkPwRe">
<div class="valid-feedback">비밀번호가 일치합니다.</div>
<div class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
</div>
</div>
<div class="row mt-4 was-validated">
<div class="col">
<label class="text-size">성별</label>
<select class="form-select rounded" required aria-label="select example" name="memberGender">
<option value="">선택해주세요</option>
<option value="남">남</option>
<option value="여">여</option>
</select>
<div class="valid-feedback"></div>
<div class="invalid-feedback"></div>
</div>
</div>
<div class="row mt-4">
<div class="col">
<label class="text-size">이메일</label>
<input class="form-control" name="memberEmail" type="text" v-model="memberEmail"
placeholder="이메일 입력" :class="checkEmail">
<div class="valid-feedback"></div>
<div class="invalid-feedback">이메일이 형식에 맞지 않습니다.</div>
</div>
</div>
<div class="row mt-4" name="happyBirth">
<div class="col-md-4" style="margin-top: 7px;">
<div class="form-group" style="font-size: small; margin-bottom: 5px;">
<label for="birth-year">생년월일</label>
<select class="form-select" id="birth-year" v-model="birthYear" >
<option value="">년</option>
<option v-for="(birthYearValue,index) in years" v-bind:key="index" v-bind:value="birthYearValue">
{{birthYearValue}}
</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="birth-month"></label>
<select class="form-select" id="birth-month" v-model="birthMonth">
<option value="">월</option>
<option v-for="(birthMonthValue,index) in months" v-bind:key="index" v-bind:value="birthMonthValue">
{{birthMonthValue}}
</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="birth-day"></label>
<select class="form-select" id="birth-day" v-model="birthDay">
<option value="">일</option>
<option v-for="(birthDayValue,index) in days" v-bind:key="index" v-bind:value="birthDayValue">
{{birthDayValue}}
</option>
</select>
</div>
</div>
</div>
<input type="hidden" id="memberBirth" name="memberBirth" v-bind:value="happyBirth">
<!--
<div class="form-group">
<label for="formFile" class="form-label mt-4">프로필 이미지</label>
<input class="form-control" type="file" id="formFile" accept=".png,.jpg">
</div>
-->
<div class="row mt-4" style="margin-left: 0px;">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="flexRadioDefault" id="flexRadioDefault1">
<label class="form-check-label" for="flexRadioDefault1">
MATCH-UP <a href="jointerm">서비스 이용 약관</a> 및 <a href="joinprivacy">개인 정보 수집 및 이용</a>에 동의합니다.
</label>
</div>
</div>
</div>
<!-- 아웃라인 버튼 -->
<div class="row mt-4">
<div class="col">
<button type="submit" class="btn btn-outline-primary btn-md w-100">회원가입</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 부트스트랩 cdn -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" ></script>
<!-- Axios(비동기) CDN -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!--Lodash cdn-->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<!-- VueJS CDN -->
<script src="https://unpkg.com/vue@3.2.36"></script>
<script>
Vue.createApp({
data(){
return {
birthYear:'',
birthMonth:'',
birthDay:'',
happyBirth:'',
years:[], //연도 저장 배열ㅋ
months:[ //월 옵션 저장 배열
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
],
days:[],//일 옵션 저장 배열
memberName:"",
memberId:"",
isValid:false,
memberPw:"",
memberPwCheck:"",
memberGender:"",
memberEmail:"",
idFinalCheck:false,//아이디 중복검사, 정규표현식 검사 결과
};
},
computed:{ //실시간 계산영역
checkName(){ // 이름
const regex = /^[가-힣]{2,5}$/;
const nameValid = regex.test(this.memberName);
if(this.memberName.length == 0) return "";
return nameValid ? "is-valid" : "is-invalid";
},
checkId(){//아이디
const regex = /^[a-z][a-z0-9]{5,20}$/;
const idValid = regex.test(this.memberId);
const temp = document.querySelector("#idValidCheck"); //vue에서 id선택자 가져오는 코드
const temp2 = document.querySelector("#idInValidCheck");//vue에서 id선택자 가져오는 코드
if(this.memberId.length == 0) return "";
if(idValid){
temp.textContent='사용할 수 있는 아이디 입니다.'
this.idFinalCheck = true;
}else{
temp2.textContent='아이디는 소문자와 숫자 8~20 사이여야 합니다.';
this.idFinalCheck = false;
}
return idValid ? "is-valid" : "is-invalid";
},
checkPw(){//비밀번호
const regex = /^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$])[-A-Za-z~!@#$%^&*()_+=0-9]{8,16}$/;
const pwValid = regex.test(this.memberPw);
if(this.memberPw.length == 0) return "";
return pwValid ? "is-valid" : "is-invalid";
},
checkPwRe(){//비밀번호 확인
const pwCheckValid = this.memberPw == this.memberPwCheck;
if(this.memberPwCheck.length == 0) return "";
return pwCheckValid ? "is-valid" : "is-invalid";
},
checkEmail(){//이메일
const regex = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;
const emailValid = regex.test(this.memberEmail);
if(this.memberEmail.length == 0) return "";
return emailValid ? "is-valid" : "is-invalid";
}
},
mounted() {
this.initializeYears(); // 생년 옵션 초기화
this.initializeDays(); // 일 옵션 초기화
},
watch:{//변경될때마다 값을 업데이트
birthYear(){
this.happyBirth = String(this.birthYear).padStart(4, '0') + "-" + String(this.birthMonth).padStart(2, '0') + "-" + String(this.birthDay).padStart(2, '0');
},
birthMonth(){
this.happyBirth = String(this.birthYear).padStart(4, '0') + "-" + String(this.birthMonth).padStart(2, '0') + "-" + String(this.birthDay).padStart(2, '0');
},
birthDay(){
this.happyBirth = String(this.birthYear).padStart(4, '0') + "-" + String(this.birthMonth).padStart(2, '0') + "-" + String(this.birthDay).padStart(2, '0');
},
},
methods: {
initializeYears() { //연도
const currentYear = new Date().getFullYear();
const startYear = currentYear - 100; // 100년 전부터 시작
for (let year = startYear; year <= currentYear; year++) {
this.years.push(year); //위 계산한 공식을 years배열에 넣는다
}
},
initializeDays() { //날짜
for (let day = 1; day <= 31; day++) {
this.days.push(day);//위 계산한 공식은 days배열에 넣는다
}
},
async IdCheck() {
if(!this.idFinalCheck) return; //idFinalCheck 가 false면 return
const memberId = this.memberId.trim(); // 입력된 아이디를 가져옴
if (memberId === "") {
return; // 아이디가 비어있으면 검사하지 않음
}
const temp = document.querySelector("#idValidCheck");
const temp2 = document.querySelector("#idInValidCheck");
const memberIdTemp = document.querySelector("#memberId");
try {
const response = await axios.get(
`http://localhost:8078/rest/member/${memberId}`
);
if (response.data === "Y") {
this.isValid = true;
console.log("사용 가능한 아이디");
temp.textContent = "아이디가 중복되지 않습니다.";
memberIdTemp.className = "form-control rounded is-valid";
// TODO: 사용 가능한 아이디 처리
} else {
this.isValid = false;
console.log("사용 불가능한 아이디");
temp2.textContent = "아이디가 중복되었습니다.";
memberIdTemp.className = "form-control rounded is-invalid";
// TODO: 사용 불가능한 아이디 처리
}
} catch (error) {
console.error("아이디 중복 검사 실패:", error);
temp2.textContent = "아이디 중복 검사 실패";
memberIdTemp.className = "form-control rounded is-invalid";
// TODO: 에러 처리
}
}
}
}).mount("#app");
</script>
</body>
</html>
'Project > 매치업(풋살 매칭 사이트)' 카테고리의 다른 글
암호화 (0) | 2023.07.17 |
---|---|
최종 코드 (0) | 2023.07.17 |
로그아웃, 회원탈퇴 구현 (0) | 2023.07.17 |
Final Project - 풋살 팀 매칭 사이트(Match-Up) (0) | 2023.07.01 |
+이메일 중복검사, 프로필 이미지 올리기 (0) | 2023.05.17 |