국비학원/수업기록

국비 지원 개발자 과정_Day83

루팽 2023. 3. 28. 20:15

데이터 → 상태(변하는 것) → state.js에서 관리 → 변수({}, [{}])

 

Action - 시그널, 구분필요 → switch문 사용 → Action의 타입필요(Action.type)

    type과 payload

Dispatcher - 허브, Store에 전달 → 경우의 수에 따라 Store에 payload가 전달됨

    useDispatch(함수)훅 → 콜백함수(함수 안에 또 다른 함수)

Store - payload → 꾸러미, 변수(객체, 배열객체 등), 여러 가지 타입이 있음(payload.변수)

View - Store에서 처리된 것이 즉시 화면에 반영됨

 

<js 콜백연습 - callback1.js>

function first(param) {
  console.log(param) // [Function]
  param() // 전달된 함수 호출
}

function second() {
  console.log(2)
}

first(second)

// 순서대로 처리해야할 때 아래와같이 사용
function func1() { // outter함수 - 클로저
  let num = 0 // 선언된 변수
  return () => { // 반환형이 함수인 경우
    return ++num // 여기서(내부함수) 사용 가능
  }
}

let account = func1()
console.log(account) // 괄호가 없으면 [Function]
console.log(account()) // 1
console.log(account()) // 2

function one() {
  console.log(1)
}

function two() {
  console.log(2)
}

one()
two()

 

<js복습 - test.js>

let global = -99 // 전역변수

// 아무런 변화도 없음
function func1() {
  let num = 0 // 지역변수
  return num
}

// 파라미터로 넘어온 값과 나가는 값이 다름 - 변함
function func2(num) {
  num = num + 1
  return num
}

// 관전포인트는 num의 값이 바뀌지 않는 것 - 불변성
function func3() {
  let num = 0 // 지역변수 불변성
  global = num + 1
  return global
}

console.log(func1()) // 0
console.log(func2(2)) // 3
console.log(global) // -99
console.log(func3()) // 1
console.log(global) // 1

 

<리덕스 연습 - app.js>

// Flux Architecture - One way binding

// 콜백함수
// document.querySelector("#root").addEventListener('click', function(){})
// 함수 선언 - 일급객체(함수를 파라미터로 넘김, 리턴으로 넘김, 할당가능)
// 함수는 어디든 갈 수 있는 권리가 있다
const createStore = () => {
  console.log(worker)
  // 외부함수에서 선언한 변수를 내부함수에서 사용 가능
  let state; // step1에선 state.js - 상태 관리가 필요한 변수 꾸러미(묶음)
  // 외부에서 구독신청한 회원들에게 알림처리 - 구독발행모델 패턴 적용
  // 구독 신청한 이벤드들의 꾸러미 담기
  let handlers = []
  const subscribe = (handler) => { // 자바스크립트 문법 코드 분석 가치
    handlers.push(handler)
  }
  // 위에서 선언된 상태 정보를 담은 변수를 새로운 상태정보로 치환 - 기존 참조를 끊는다 - 안전하니까
  const send = (action) => {
    // worker함수의 파라미터로 state를 두는 건 기존의 상태정보에 추가된 상태정보이다 - 변경사항을 담기위해
    // 위에서 선언된 state변수에 새로운 상태정보가 추가된 상태정보를 갖는 주소번지로 치환
    state = worker(state, action)
    handlers.forEach(handler => handler())
  }
  // 내부함수 - 클로저검색
  // https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
  const getState = () => { // react-redux에서 제공함 - 모방하기
    return state // 여기서 관리하는 상태값 모두를 리턴함 - 객체{} 리터럴 사용
  }
  // 리턴타입에 함수이름을 반환하는 건 외부에서 호출하기 위해서이다 - API
  return { // 객체리터럴을 사용하면 여러개의 함수를 외부에서 사용 가능함
    getState,
    send,
  }
}

const worker = (state = {count: 0}, action) => { // state의 undefined방지로 객체리터럴 대입해줌
  // 여기서 상태를 바꾸면 createStore에 있는 state의 참조 무결성이 깨짐
  // redux에서는 반드시 이 worker, 즉 상태를 바꾸는 함수는 새로운 상태를 반환하라는 규칙을 만듦
  // 새로운 상태를 반환하라는 건 기존의 참조를 끊으라는 의미
  // 기존의 참조를 끊어야 예상하지 못함 side effect를 원천적으로 차단할 수 있음
  switch(action.type) {
    case 'increase':
      return {...state, count: state.count + 1}
    case 'decrease':
      return {...state, count: state.count - 1}
    default:
      return {...state} // 원본이 지켜짐, 새로운 객체가 만들어짐(깊은복사)
  }
}

// 스토어 함수 호출하기
// const store = legacy_createStore(reducer)
// 상태는 createStore함수 안에 있다(let state)
// 누가 이 상태를 변경하고 읽어가는가?
// worker함수의 switch문에서 action.type에 따라서 상태를 변경하거나 읽어낸다
// 변경되고 읽어낸 정보는 return으로 처리함
// store를 모아서 상태의 묶음을 넘겨줄 것
const store = createStore(worker)
store.subscribe =() => {
  console.log(store.getState())
}
// 아래와 같이 store의 내부함수를 외부에서 호출하려면 return에 반드시 등록할 것
// action의 내용을 만드는 역할은 send를 하는쪽에서 만들어줌
store.send({type: 'increase'})
// 아래 코드로는 새로운 상태값 확인이 불가
// console.log(store.send()) -> undefined
console.log(store.getState()) // {count: 1}
store.send({type: 'increase'})
console.log(store.getState()) // {count: 2}
store.send({type: 'decrease'})
console.log(store.getState()) // {count: 1}

/*
  UI한테는 직접적인 상태를 주지 않는다
  그래서 여기서 return하는 것에는 state를 주지 않을 것 - 리덕스 컨셉
  state를 그냥 주는 것은 자바스크립트의 컨셉

  문제제기
  지금은 맥락없이 1을 증가하는 컨셉
*/

 

<리덕스 예제 - store.js>

// 액션
export const increase = (mem_name) => ({
  type: 'INCREASE',
  payload: mem_name,
})
export const decrease = (empVO) => ({ type: "DECREASE", payload: empVO })

export const deptlist = (depts) => ({ type:'DEPTLIST', payload: depts })

// 초기상태 만들기
const initstate = {
  number: 0,
  mem_name: '이순신',
  empVO: {empno: 7566, ename: '나신입'},
  depts: [
    { DEPTNO: 10, DNAME: '총무부', LOC: '인천'},
    { DEPTNO: 20, DNAME: '영업부', LOC: '서울'},
    { DEPTNO: 30, DNAME: '개발부', LOC: '괌'},
  ],
  auth: '',
  googleProvider: '',
}

// worker가 reducer함수로 전환
//액션의 결과를 필터링 - 리듀서
const reducer = (state = initstate, action) => {
  switch(action.type) {
    case "INCREASE":
      // return이되면 그걸 호출한 쪽에서 받는게 아니고, return되는 순간 바로 화면이 변경됨
      return { ...state, number: state.number + 1, mem_name: action.payload }
    case "DECREASE":
      return { ...state, number: state.number - 1, empVO: action.payload }
    case "DEPTLIST":
      return {depts: action.payload}
    default:
      return { ...state } // 깊은 복사, 새로운 객체 생성됨
  }
}
export default reducer

 

<리덕스 예제 - ReduxHeader.jsx>

import React from 'react'
import { useSelector } from 'react-redux'

const ReduxHeader = () => {
  const empVO = useSelector((store) => store.empVO)
  return (
    <>
      <div className='header_container'>
        리덕스 헤더영역<br />
        {empVO && `사원번호: ${empVO.empno}`}
      </div>
    </>
  )
}

export default ReduxHeader

 

<리덕스 예제 - ReduxMainPage.jsx>

import React from 'react'
import { useSelector } from 'react-redux'

const ReduxMainPage = () => {
  const number = useSelector(store => store.number)
  const mem_name = useSelector(store => store.mem_name)
  const depts = useSelector(store => store.depts)
  return (
    <>
      <div className='main_container'>
        컨텐츠 영역
        <h4>번호: {number}</h4>
        <h4>이름: {mem_name}</h4>
        {
          depts && depts.map((dept, index) => (
            <h4 key={index}>{dept.DEPTNO} {dept.DNAME} {dept.LOC}</h4>
          ))
        }
      </div>
    </>
  )
}

export default ReduxMainPage

 

<회원가입페이지 만들기 - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "@fortawesome/fontawesome-free/js/all.js";
import ImageUploader from "./service/imageUploader";
import { Provider } from "react-redux";
import { legacy_createStore } from "redux";
import rootReducer from "./redux/rootReducer";
import AuthLogic from "./service/authLogic";
import firebaseApp from "./service/firebase";
import { setAuth } from "./redux/userAuth/action";

// 리덕스 적용하기
const store = legacy_createStore(rootReducer)
// AuthLogic객체 생성하기
const authLogic = new AuthLogic(firebaseApp)
// store에 있는 초기 상태정보 출력하기
store.dispatch(setAuth(authLogic.getUserAuth(), authLogic.getGoogleAuthProvider()))
console.log(store.getState());
// 이미지 업로더 객체 생성
const imageUploader = new ImageUploader();
const root = ReactDOM.createRoot(document.getElementById("root"));
// 리덕스 추가 - store 생성
// createStore 호출
root.render(
  <>
    <Provider store={store}>
      <BrowserRouter>
        <App authLogic={authLogic} imageUploader={imageUploader} />
      </BrowserRouter>
    </Provider>
  </>
);

 

<회원가입페이지 만들기 - App.jsx>

import { Route, Routes } from 'react-router-dom';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
import HomePage from './components/page/HomePage';
import DeptPage from './components/page/DeptPage';
import DeptDetail from './components/dept/DeptDetail';
import RepleBoardPage from './components/page/RepleBoardPage';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';
import SignupPage from './components/auth/SignupPage';

function App({authLogic, imageUploader}) {
  const dispatch = useDispatch()
  const toastStatus = useSelector(state => state.toastStatus)
  useEffect(() => {
    dispatch(setToastMsg('회원가입을 해주세요.'))
  }, [])

  return (
    <>
      <div style={{height: '100vh'}}>
        {toastStatus.status && <Toast />}
        <Routes>
          <Route path='/login' exact={true} element={<LoginPage />} />
          <Route path='/' exact={true} element={<HomePage />} />
          <Route path='/home' exact={true} element={<HomePage />} />
          <Route path='/auth/signup' exact={true} element={<SignupPage authLogic={authLogic} />} />
          <Route path='/repleboard' element={<RepleBoardPage />} />
          <Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} />} />
          {/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
          <Route path='/deptdetail/:deptno' element={<DeptDetail imageUploader={imageUploader} />} />
          <Route path='/auth/kakao/callback' exact={true} element={<KakaoRedirectHandler />} />
          <Route path="/member" exact={true} element={<MemberPage imageUploader={imageUploader} />} />
          <Route path="/profile" exact={true} element={<Profile />} />
        </Routes>
      </div>
    </>
  );
}

export default App;

 

<회원가입페이지 만들기 - SignupPage.jsx>

import React from 'react'
import KhSignup from './KhSignup';
import Signuptype from './Signuptype';

const SignupPage = ({authLogic}) => {
  // http://localhost:3000/auth/signup?type=member
  const type = window.location.search.split('=')[1]
  // console.log(window.location.search.split('&'));
  // console.log(window.location.search.split('&')[0]);
  // console.log(window.location.search.split('&')[0].split('='));
  // console.log(window.location.search.split('&')[0].split('=')[0]);
  // console.log(window.location.search.split('&')[0].split('=')[1]);
  console.log(type)

  const signupage = () => {
    if(type){
      return <KhSignup authLogic={authLogic} />
    } else {
      return <Signuptype/>
    }
  } 

  return (
    signupage()
  );
};

export default SignupPage

 

<회원가입페이지 만들기 - KhSignup.jsx>

/* global daum */
import React, { useEffect, useState } from 'react';
import { Form } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { memberInsertDB, memberListDB } from '../../service/dbLogic';
import { linkEmail, onAuthChange, signupEmail } from '../../service/authLogic';
import { checkPassword, validateBirthdate, validateEmail, validateHp, validateName, validateNickname, validatePassword } from '../../service/validateLogic';
import { SignupForm, MyH1, MyInput, MyLabel,  MyLabelAb, SubmitButton, MyButton, PwEye, WarningButton} from '../styles/FormStyle';

const KhSignup = ({authLogic}) => {
  const auth = authLogic.getUserAuth();
  const userAuth = useSelector(state => state.userAuth);
  //window.location.search는 ?뒤의 쿼리스트링을 가져옴
  //
  //http://localhost:3000/login/signup?type=member
  const type = window.location.search.split('&')[0].split('=')[1];//member담김
  //console.log(window.location.search.split('&'));//['?type=member']
  //console.log(window.location.search.split('&')[0]);//?type=member
  //console.log(window.location.search.split('&')[0].split('='));//['?type', 'member']
  //console.log(window.location.search.split('&')[0].split('=')[0]);//?type
  //console.log(window.location.search.split('&')[0].split('=')[1]);//member
  //console.log(type);
  const [reSignCheck, setReSignCheck] = useState(false);
  const navigate = useNavigate();

  const[submitBtn, setSubmitBtn] = useState({
    disabled: true,
    bgColor: 'rgb(175, 210, 244)',
    hover: false
  });

  const toggleHover = () => {
    if(submitBtn.hover){
      setSubmitBtn({...submitBtn, hover: false, bgColor: 'rgb(105, 175, 245)'});
    } else {
      setSubmitBtn({...submitBtn, hover: true, bgColor: 'rgb(72, 145, 218)'});
    }
  }


  const[post, setPost] = useState({
    zipcode: "",
    addr: "",
    addrDetail: ""
  })

  const [memInfo, setMemInfo] = useState({
    email: "",
    password: "",
    password2: "",
    name: "",
    birthday: "",
    hp: "",
    nickname: "",
    gender: "없음"
  });

  const [comment, setComment] = useState({
    email: "",
    password: "",
    password2: "",
    name: "",
    birthday: "",
    hp: "",
    nickname: ""
  });

  const [star,setStar] = useState({
    email: "*",
    password: "*",
    password2: "*",
    name: "*",
    hp: "*",
    nickname: "*",
    birthday: "*"
  })


  const [passwordType, setPasswordType] = useState([
    {
      type:'password',
      visible:false
    },
    {
      type:'password',
      visible:false
    }
  ]);


  const [googleEmail, setGoogleEmail] = useState('');


  useEffect(()=> {
    let check = true;
    Object.keys(star).forEach((key)=>{
      if(star[key]==='*') check = false;
    })
    if(check){ 
      setSubmitBtn({disabled:false, bgColor: 'rgb(105, 175, 245)'});
    } else {
      setSubmitBtn({disabled:true, bgColor: 'rgb(175, 210, 244)'});
    }
  },[star]);

  useEffect(()=> {
    const onAuth = async() => {
      const user = await onAuthChange(userAuth.auth) ;
      if(user){
        setGoogleEmail(user.email);
        setStar({
          email: "",
          password: "*",
          password2: "*",
          name: "*",
          hp: "*",
          nickname: "*",
          birthday: "*"
        });
        setMemInfo({
          email: user.email,
          password: "",
          password2: "",
          name: "",
          hp: "",
          nickname: "",
          birthday: "",
          gender:"없음"
        });
      }
    };
    onAuth();
  },[setGoogleEmail,setStar,setMemInfo, userAuth.auth]);


  const passwordView = (e) => {
    const id = e.currentTarget.id;
    if(id==="password") {
      if(!passwordType[0].visible) {
        setPasswordType([{type: 'text', visible: true},passwordType[1]]);
      } else {
        setPasswordType([{type: 'password', visible: false},passwordType[1]]);
      }
    } else if(id==="password2") {
      if(!passwordType[1].visible) {
        setPasswordType([passwordType[0],{type: 'text', visible: true}]);
      } else {
        setPasswordType([passwordType[0],{type: 'password', visible: false}]);
      }
    }
  }

  const changeMemInfo = (e) => {
    console.log('changeMemInfo');
    const id = e.currentTarget.id;
    console.log(id);
    const value = e.target.value;
    console.log(value);
    setMemInfo({...memInfo, [id]: value});
  }
  //닉네임 중복확인 
  const overlap = async(key) => {
    console.log('닉네임 중복확인');
    let params;
    if(key === 'email') {
      params = {MEM_EMAIL: memInfo[key], type: 'overlap'}
    } else if(key === 'nickname') {
      params = {MEM_NICKNAME: memInfo[key], type: 'overlap'}
    }
    console.log(params)
    let response = {data: 0}
    response = await memberListDB(params)
    console.log(response.data)
    /*
    Array(1)
    0: {MEM_UID: 'kiwi', MEM_NAME: '강감찬'}
    */
    const data = JSON.stringify(response.data)
    const jsonDoc = JSON.parse(data)
    console.log(jsonDoc[0])
    // 닉네임이 존재할 때
    if(response.data){
      console.log('중복')
    }
    // 닉네임이 없을 때
    else {
      console.log('가능')
    }
  }

  const validate = (key, e) => {
    let result;
    if(key==='email'){
      result = validateEmail(e); 
    } else if(key==='nickname'){
      result = validateNickname(e); 
    } else if(key==='password'){
      result = validatePassword(e); 
    } else if(key==='password2'){
      result = checkPassword(memInfo.password, e); 
    } else if(key==='name'){
      result = validateName(e); 
    } else if(key==='hp'){
      result = validateHp(e); 
    } else if(key==='birthday'){
      result = validateBirthdate(e); 
    } 
    setComment({...comment, [key]: result}); 
    if(result){
      if(result===' '){
        setStar({...star, [key]:""});
      } else {
        setStar({...star, [key]:"*"});
      }
    }else {
      setStar({...star, [key]:""});
    }
  }
  //다음 우편번호 검색기 구현
  const openZipcode = () => {
    new daum.Postcode({
      oncomplete: function(data) {
        let addr = ''; 
        if (data.userSelectedType === 'R') { 
          addr = data.roadAddress;//도로명
        } else { 
          addr = data.jibunAddress;//지번
        }
        console.log(data);
        console.log(addr);
        console.log(post.postNum);
        setPost({...post, zipcode:data.zonecode, addr:addr}) ;
        document.getElementById("zipcode").value = data.zonecode;
        document.getElementById("addr").value = addr;
        document.getElementById("addrDetail").focus();
      }
    }).open();
  }
  //회원정보 수정하기 구현 예정
  const signUpdate = () => {

  }

  //회원 가입 구현
  const signup = async() => {
    console.log('회원가입 구현');
    try {
      let uid;
      console.log(googleEmail);
      if(googleEmail){
        console.log(auth); 
        console.log(memInfo); 
        uid = await linkEmail(auth, memInfo);
        console.log(uid);
      } else {
        uid = await signupEmail(auth, memInfo);
      }
      console.log(uid);
      //const pwd = pwdEncrypt(memInfo.password);
      const b = memInfo.birthday;
      let birthday = ""; 
      if(b!==""){
        birthday = b.slice(0,4) + '-' + b.slice(4, 6) + '-' + b.slice(6,8);
      }
      console.log('입력받은 생일정보 '+birthday);
      const datas = {
        MEM_UID: uid,
        MEM_NAME: memInfo.name,
        MEM_PW: memInfo.password,
        MEM_EMAIL: memInfo.email,
        MEM_BIRTHDAY: birthday,
        MEM_TEL: memInfo.hp,
        MEM_NICKNAME: memInfo.nickname,
        MEM_ZIPCODE: post.postNum,
        MEM_ADDR: post.post,
        MEM_ADDR_DTL: post.postDetail,
        MEM_AUTH: (type==='member'?1:2),
        MEM_GENDER: memInfo.gender
      }
      console.log(datas)
      const response = await memberInsertDB(datas);
      console.log(response);
      if(response.data!==1) {
      return "DB 오류: 관리자에게 연락바랍니다.";
    }
      sessionStorage.clear();
      navigate('/');
      return "회원가입되었습니다. 감사합니다.";
      
    } catch (error) {
      console.log(error+" 오류: 관리자에게 연락바랍니다.");
    }
  }//end of 회원가입 구현
  
  const checkboxLable = ['없음','남자','여자']

  const Checkbox = checkboxLable.map((item, index) => (
    <Form.Check inline label={item} value={item} name="group1" type='radio' checked={memInfo.gender===item?true:false} readOnly
    id={`inline-radio-${item}`} key={index} onClick={(e)=> {setMemInfo({...memInfo, gender: e.target.value})}}/>
  ))
  
  const handleSignup = (event) => {
    console.log('가입하기 또는 수정하기 버튼클릭 이벤트');
    signup()    
  }

  return (
    <div style={{display: 'flex', justifyContent: 'center'}}>
      <div style={{display: 'flex', width:"100%"}}>
        <SignupForm  suggested={false}>
          <MyH1>{'회원가입'}</MyH1>
          <div style={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center'}}>
            <div style={{padding: '30px 30px 0px 30px'}}>
              { googleEmail
                ?
                <>
                  <MyLabel> 이메일 
                    <MyInput type="email" id="email" defaultValue={googleEmail} disabled={true} />
                  </MyLabel>
                </>
                :
                <div style={{display: 'flex' , flexWrap: 'wrap'}}>
                  <MyLabel> 이메일 <span style={{color:"red"}}>{star.email}</span>
                    <MyInput type="email" id="email" placeholder="이메일를 입력해주세요." 
                    onChange={(e)=>{changeMemInfo(e); validate('email', e);}}/>
                    <MyLabelAb>{comment.email}</MyLabelAb>
                  </MyLabel>
                  <MyButton type="button" onClick={()=>{overlap('email');}}>중복확인</MyButton>
                </div>
              }
              <div style={{display: 'flex'}}>
                <MyLabel> 닉네임 <span style={{color:"red"}}>{star.nickname}</span>
                  <MyInput type="text" id="nickname" defaultValue={memInfo.nickname} placeholder="닉네임을 입력해주세요." 
                  onChange={(e)=>{changeMemInfo(e); validate('nickname', e);}}/>
                  <MyLabelAb>{comment.nickname}</MyLabelAb>
                </MyLabel>
                <MyButton type="button" onClick={()=>{overlap('nickname')}}>중복확인</MyButton>
              </div>
              <MyLabel> 비밀번호 <span style={{color:"red"}}>{star.password}</span>
                <MyInput type={passwordType[0].type} id="password" autoComplete="off" placeholder="비밀번호를 입력해주세요." 
                onKeyUp={(e)=>{setComment({...comment, password2: checkPassword(e.target.value,memInfo.password2)});}} 
                onChange={(e)=>{changeMemInfo(e);  validate('password', e);}}/>
                <div id="password" onClick={(e)=> {passwordView(e)}} style={{color: `${passwordType[0].visible?"gray":"lightgray"}`}}>
                  <PwEye className="fa fa-eye fa-lg"></PwEye>
                </div>
                <MyLabelAb>{comment.password}</MyLabelAb>
              </MyLabel>
              <MyLabel> 비밀번호 확인 <span style={{color:"red"}}>{star.password2}</span>
                <MyInput type={passwordType[1].type} id="password2"  autoComplete="off" placeholder="비밀번호를 한번 더 입력해주세요."
                onChange={(e)=>{changeMemInfo(e); validate('password2', e.target.value)}}/>
                <div id="password2" onClick={(e)=> {passwordView(e)}} style={{color: `${passwordType[1].visible?"gray":"lightgray"}`}}>
                  <PwEye className="fa fa-eye fa-lg"></PwEye>
                </div>
                <MyLabelAb>{comment.password2}</MyLabelAb>
              </MyLabel>         
            </div>

            <div style={{padding: '30px 30px 0px 30px'}}>
              <MyLabel> 이름 <span style={{color:"red"}}>{star.name}</span>
                <MyInput type="text" id="name" defaultValue={memInfo.name} placeholder="이름을 입력해주세요." 
                onChange={(e)=>{changeMemInfo(e); validate('name', e);}}/>
                <MyLabelAb>{comment.name}</MyLabelAb>
              </MyLabel>
              <MyLabel> 전화번호 <span style={{color:"red"}}>{star.hp}</span>
                <MyInput type="text" id="hp" defaultValue={memInfo.hp} placeholder="전화번호를 입력해주세요." 
                onChange={(e)=>{changeMemInfo(e); validate('hp', e);}} />
                <MyLabelAb>{comment.hp}</MyLabelAb>
              </MyLabel>

              <MyLabel> 생년월일 <span style={{color:"red"}}>{star.birthday}</span>
                <MyInput type="text" id="birthday" defaultValue={memInfo.birthday} placeholder="생년월일을 입력해주세요." 
                onChange={(e)=>{changeMemInfo(e); validate('birthday', e);}}/>
                <MyLabelAb>{comment.birthday}</MyLabelAb>
              </MyLabel>

              <MyLabel> 우편번호
                <MyInput type="text" id="zipcode" defaultValue={memInfo.zipcode} placeholder="우편번호를 입력해주세요." 
                onChange={(e)=>{changeMemInfo(e);}} />
                <MyLabelAb>{comment.zipcode}</MyLabelAb>
              </MyLabel>

              <div style={{display: 'flex'}}>
                <MyLabel> 주소
                  <MyInput type="text" id="addr" defaultValue={post.addr} readOnly placeholder="주소검색을 해주세요."/>
                </MyLabel>
                <MyButton type="button" onClick={()=>{openZipcode()}}>주소검색</MyButton>
              </div>
              <MyLabel> 상세주소
                <MyInput type="text" id="addrDetail" defaultValue={post.addrDetail} readOnly={post.addr?false:true}
                onChange={(e)=>{setPost({...post, addrDetail : e.target.value})}}/>
              </MyLabel>
            </div>
          </div>
          <MyLabel style={{margin:0}}> 성별
            <div style={{marginTop:10}} key={`inline-radio`} className="mb-3">
              {Checkbox}
            </div>
          </MyLabel>
            <SubmitButton type="button" style={{backgroundColor:submitBtn.bgColor }}
            onClick={handleSignup} onMouseEnter={toggleHover} onMouseLeave={toggleHover}>
              {'가입하기'}
            </SubmitButton>
        </SignupForm>
      </div>
    </div>
  );
};

export default KhSignup;

 

<회원가입페이지 만들기 - Signuptype.jsx>

import React from 'react'
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';

const TypeForm = styled.div`
  display: flex; 
  width: 80%;
  max-width: 1200px;
  height: 60vh;
  justify-content: center; 
  align-items: center;
  border: 2px solid lightgray;
  border-radius: 60px;
  margin: 150px 0px 150px 0px;
  
`;

const TypeDiv = styled.div`
  display: flex; 
  flex-direction: column; 
  align-items: center;
  justify-content: center;
  width: 50%;
  height:100%;
  cursor: pointer;
  border-radius: 55px;
  &:hover { 
    background-color: lightgray;
  }
`;

const Signuptype = () => {

  const navigate = useNavigate();

  return (
    <div style={{display: 'flex', justifyContent: 'center'}}>
      <TypeForm>
        <TypeDiv onClick={()=>{navigate('/auth/signup?type=member')}}>
          <i className="fas fa-person-running" style={{width: '40%', height:'80%'}}></i>
          <span style={{width: '50%', height:'20%', textAlign:'center', fontSize:'200%'}}>회원</span>
        </TypeDiv>
        <hr style={{height: '80%', width: '2px'}}/>
        <TypeDiv onClick={()=>{navigate('/auth/signup?type=teacher')}}>
          <i className="fas fa-person-chalkboard" style={{width: '50%', height:'80%'}}></i>
          <span style={{width: '50%', height:'20%', textAlign:'center', fontSize:'200%'}}>강사</span>
        </TypeDiv>
      </TypeForm>
    </div>
  );
};

export default Signuptype;

 

<회원가입페이지 만들기 - LoginPage.jsx>

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useNavigate } from 'react-router-dom'
import { setToastMsg } from '../../redux/toastStatus/action'
import { loginGoogle } from '../../service/authLogic'
import './loginpage.css'

const LoginPage = () => {
  const navigate = useNavigate()
  // One-Way 바인딩에서 액션을 스토어에 전달할 때 디스패치 사용함
  const dispatch = useDispatch()
  // store에 초기화된 state 안에 담긴 data 꺼내올 때 셀렉터 사용
  const userAuth = useSelector((store) => store.userAuth)
  const auth = useSelector((store) => store.userAuth.auth)
  console.log(auth);
  const googleProvider = useSelector((store) => store.userAuth.googleProvider)
  console.log(googleProvider);

  const handleGoogle = async(event) => {
    event.preventDefault()
    try {
      const result = await loginGoogle(auth, googleProvider)
      console.log(result.uid);
      window.sessionStorage.setItem('userId', result.uid)
      navigate('/')
    } catch (error) {
      dispatch(setToastMsg(error + ': 로그인 오류입니다.')) // [object, object] -> JSON.stringify()
    }
  } //end of handleGoogle

  return (
    <>
    <div className="bg"></div>
    <form className="login__form" action="">
      <h1>로그인</h1>
      <p>신규 사용자이신가요? <Link to="#">계정 만들기</Link></p>
      <label htmlFor="useremail">이메일 주소
        <input type="email" id="useremail" placeholder="이메일 주소를 입력해주세요."/>
      </label>
      <input type="submit" className="btn__submit" value="계속" />
      <div className="divider">
        <hr />
        <span>또는</span>
      </div>
      <button className="btn__google" onClick={handleGoogle}>
        <i className="icon icon-google"></i>Google으로 계속
      </button>
      <button className="btn__facebook">
        <i className="icon icon-facebook"></i>Facebook으로 계속
      </button>
      <p className="captcha__text">
        reCAPTCHA로 보호되며 Google <Link to="#">개인정보보호 정책</Link> 및
        <Link to="#">서비스 약관</Link>의 적용을 받습니다.
      </p>
      </form>
    </>
  )
}

export default LoginPage

 

<회원가입페이지 만들기 - validateLogic.js>

export const validateEmail = (e) => {
  //eslint-disable-next-line
  const re = /^([0-9a-zA-Z_\\.-]+)@([0-9a-zA-Z_-]+)(\\.[0-9a-zA-Z_-]+){1,2}$/;
  const email = e.target.value;
  if (email === "") {
    return " ";
  } else if (!re.test(email)) {
    return "이메일 형식이 잘못되었습니다.";
  } else {
    return "중복확인을 해주세요.";
  }
};

export const validatePassword = (e) => {
  const pw = e.target.value;
  const num = pw.search(/[0-9]/g);
  const eng = pw.search(/[a-z]/gi);
  const spe = pw.search(/[`~!@@#$%^&*|₩₩₩'₩";:₩/?]/gi);
  const hangulcheck = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;

  if (pw.length === 0) {
    return " ";
  } else if (pw.length < 8 || pw.length > 20) {
    return "8자리 ~ 20자리 이내로 입력해주세요.";
  } else if (hangulcheck.test(pw)) {
    return "비밀번호에 한글을 사용 할 수 없습니다.";
  } else if (pw.search(/\\s/) !== -1) {
    return "비밀번호는 공백 없이 입력해주세요.";
  } else if (num < 0 || eng < 0 || spe < 0) {
    return "영문,숫자, 특수문자를 혼합하여 입력해주세요.";
  } else {
    return "";
  }
};

export const checkPassword = (pw, pw2) => {
  if (pw2) {
    if (pw === pw2) {
      return "";
    } else {
      return "비밀번호가 일치하지않습니다.";
    }
  } else {
    return " ";
  }
};

export const validateName = (e) => {
  console.log('validateName : '+e);
  const name = e.target.value;
  const han = /^[가-힣]+$/;
  const eng = /^[a-zA-Z]+$/;
  if (name.length === 0) {
    return " ";
  } else if (han.test(name) || eng.test(name)) {
    return "";
  } else {
    return "이름은 영어또는 한글로만 가능합니다.";
  }
};

export const validateBirthdate = (e) => {
  const birthday = e.target.value;
  const day =
    /^(19[0-9][0-9]|20\\d{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$/;
  if (birthday.length === 0) {
    return " ";
  } else if (day.test(birthday) && birthday.length === 8) {
    return "";
  } else {
    return "생년월일은 YYYYMMDD형식으로 적어주세요.";
  }
};

export const validateHp = (e) => {
  const hp = e.target.value;

  var reghp = /^01([0|1|6|7|8|9]?)([0-9]{4})([0-9]{4})$/;

  if (hp.length === 0) {
    return " ";
  } else if (!reghp.test(hp)) {
    return "'-'를 제외한 번호를 적어주세요.";
  } else {
    return "";
  }
};

export const validateNickname = (e) => {
  const nickname = e.target.value;
  const check = /^[가-힣a-zA-Z0-9]+$/;
  if (nickname.length === 0) {
    return " ";
  } else if (nickname.length < 2 || nickname.length > 10) {
    return "2자리 ~ 10자리 이내로 입력해주세요.";
  } else if (nickname.search(/\\s/) !== -1) {
    return "닉네임은 공백 없이 입력해주세요.";
  } else if (check.test(nickname)) {
    return "중복확인을 해주세요.";
  } else {
    return "해당 닉네임은 사용할 수 없습니다.";
  }
};

export const validateDname = (e) => {
  // 사용자가 입력한 부서명
  const name = e.target.value // input onChange
  const kor = /^[ㄱ-ㅎ가-힣]+$/;
  const eng = /^[a-zA-Z]+$/;
  if(name.length === 0) {
    return " ";
  } else if (kor.test(name) || eng.test(name)) { // test의 리턴타입은 boolean
    return "";
  } else {
    return "부서명은 영어 또는 한글만 가능합니다.";
  }
}

 

<회원가입페이지 만들기 - authLogic.js>

import { createUserWithEmailAndPassword, EmailAuthProvider, getAuth, GoogleAuthProvider, sendEmailVerification, signInWithPopup } from 'firebase/auth'

class AuthLogic {
  constructor() {
    this.auth = getAuth()
    this.googleProvider = new GoogleAuthProvider()
  }
  getUserAuth = () => {
    return this.auth
  }
  getGoogleAuthProvider = () => {
    return this.googleProvider
  }
}
export default AuthLogic

export const onAuthChange = (auth) => {
  return new Promise((resolve) => {
    // 사용자가 바뀌었을 때 콜백함수를 받아서
    auth.onAuthStateChanged((user) => {
    resolve(user)
    });
  }) // end of Promise
} // end of onAuthChange

// 로그아웃 버튼 클릭시 호출하기
export const logout = (auth) => {
  return new Promise((resolve, reject) => {
    auth.signOut().catch(e => reject(e + '로그아웃 오류입니다.'))
    // 로그인 성공시 세션 스토리지에 담아둔 정보를 모두 지운다
    sessionStorage.clear();
    // 서비스를 더 이상 사용하지 않는 경우이므로 돌려줄 값은 없다
    resolve();
  })
} // end of logout

 // 로그인 시도시 구글인증인지 아니면 깃허브 인증인지 문자열로 넘겨받음
  // 구글 인증인 경우 - Google
  // 깃허브 인증인 경우 - Github
  export const loginGoogle = (auth, googleProvider) => {
    return new Promise((resolve, reject) => {
      signInWithPopup(auth, googleProvider).then(
        (result) => {
          const user = result.user; // 구글에 등록되어있는 profile정보가 담겨있음
          console.log(user);
          resolve(user)
        }
      ).catch(e => reject(e))
    })
  }

  export const signupEmail = (auth, user) => {
    return new Promise((resolve, reject) => {
      createUserWithEmailAndPassword(auth, user.email, user.password)
        .then((userCredential) => {
          sendEmail(userCredential.user).then(() => {
            resolve(userCredential.user.uid);
          });
        })
        .catch((e) => reject(e));
    });
  };

  export const linkEmail = (auth, user) => {
    console.log(auth);
    console.log(auth.currentUser);
    console.log(user);
    return new Promise((resolve, reject) => {
      console.log(user.email + "," + user.password);
      const credential = EmailAuthProvider.credential(user.email, user.password);
      console.log(credential);
      console.log(auth.currentUser.uid);
      resolve(auth.currentUser.uid)
      /* 인증정보가 다른 사용자 계정에 이미 연결되어 있다면 아래 코드 에러 발생함
      linkWithCredential(auth.currentUser, credential)
        .then((usercred) => {
          console.log(usercred);
          const user = usercred.user;
          console.log("Account linking success", user.uid);
          resolve(user.uid);
        })
        .catch((e) => reject(e));
      */
    });
  };

  export const sendEmail = (user) => {
    return new Promise((resolve, reject) => {
      sendEmailVerification(user)
        .then(() => {
          resolve("해당 이메일에서 인증메세지를 확인 후 다시 로그인 해주세요.");
        })
        .catch((e) => reject(e + ": 인증메일 오류입니다."));
    });
  };

 

<회원가입페이지 만들기 - RestMemberController.java>

package com.example.demo.controller;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
// import org.slf4j.Logger;
// import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.logic.BoardLogic;
import com.example.demo.logic.MemberLogic;
import com.google.gson.Gson;

@RestController
@RequestMapping("/member/*")
public class RestMemberController {
	// Logger logger = LoggerFactory.getLogger(RestMemberController.class);
	Logger logger = LogManager.getLogger(RestMemberController.class);
	
	@Autowired
	private MemberLogic memberLogic = null;
	
	/**
	 * 회원 목록
	 * <http://localhost:8000/member/memberList?MEM_NICKNAME=&type=overlap>
	 * 리액트 프로젝트에서 닉네임 중복검사시 사용하는 메소드 구현
	 * 리액트에서 넘기는 파라미너는 {MEM_NICKNAME: 'qwerty', type: 'overlap'}
	 * @param pMap
	 * @return
	 */
	@GetMapping("memberList")
	public String memberList(@RequestParam Map<String, Object> pMap) {
		logger.info("memberList 호출");
		logger.info(pMap);
		String temp = null;
		List<Map<String, Object>> mList = new ArrayList<>();
		mList = memberLogic.memberList(pMap);
		logger.info(mList);
		// 파라미터로 넘어온 닉네임이 회원집합에 존재하면 조회결과가 있다 -> mList.size() === 1
		// temp에 문자열이 들어있으면 자바스크립트쪽에서는 true로 판정된다 - 주의!
		if(mList.size() > 0) {
			Gson g = new Gson();
			temp = g.toJson(mList);			
		}
		// mList.size() === 0
		else {
			temp = "0";
		}
		return temp;
	}
	
	/**
	 * 회원 등록
	 * @param pMap
	 * @return
	 */
	@PostMapping("memberInsert")
	public String memberInsert(@RequestBody Map<String, Object> pMap) {
		// 리액트에서 body에 {}객체리터럴로 넘겨준 정보를 Map이나 VO에 담을 수 있다
		logger.info("memberInsert 호출");
		logger.info(pMap);
		int result = 0;
		result = memberLogic.memberInsert(pMap);
		return String.valueOf(result);
	}
	
	/**
	 * 회원 정보 수정
	 * @param pMap
	 * @return
	 */
	@PostMapping("memberUpdate")
	public String memberUpdate(@RequestBody Map<String, Object> pMap) {
		logger.info("memberUpdate 호출");
		logger.info(pMap);
		int result = 0;
		result = memberLogic.memberUpdate(pMap);
		return String.valueOf(result);
	}
	
	/**
	 * 회원 정보 삭제
	 * @param pMap
	 * @return
	 */
	@GetMapping("memberDelete")
	public String memberDelete(@RequestParam Map<String, Object> pMap) {
		logger.info("memberDelete 호출");
		logger.info(pMap);
		int result = 0;
		result = memberLogic.memberDelete(pMap);
		return String.valueOf(result);
	}
}

 

<회원가입페이지 만들기 - member.xml>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo">
	<select id="getToday" resultType="string" parameterType="string">
		SELECT
		to_char(sysdate, 'YYYY-MM-DD') FROM dual
	</select>

	<select id="login" parameterType="map" resultType="string">
		select mem_name from member230324
		<where>
			<if test='mem_uid!=null and mem_uid.length()>0'>
				AND mem_uid = #{mem_uid}
			</if>
			<if test='mem_pw!=null and mem_pw.length()>0'>
				AND mem_pw = #{mem_pw}
			</if>
		</where>
	</select>

	<!-- 회원 목록 - 닉네임 중복확인 -->
	<select id="memberList" parameterType="map" resultType="map">
		select mem_uid, mem_name from member230324
		<where>
			<if test='mem_uid!=null and mem_uid.length()>0'>
				AND mem_uid = #{mem_uid}
			</if>
			<if test='MEM_NICKNAME!=null and MEM_NICKNAME.length()>0'>
				AND mem_nickname = #{MEM_NICKNAME}
			</if>
		</where>
	</select>

	<!-- 회원 정보 입력 -->
	<insert id="memberInsert" parameterType="map">
		INSERT INTO member230324(mem_no
					<if test="mem_uid != null">
						,mem_uid
					</if>
					<if test="mem_pw != null">
						,mem_pw
					</if>
					<if test="mem_name != null">
						,mem_name
					</if>
					<if test="mem_nickname != null">
						,mem_nickname
					</if>
					<if test="mem_email != null">
						,mem_email
					</if>
					<if test="mem_tel != null">
						,mem_tel
					</if>
					<if test="mem_gender != null">
						,mem_gender
					</if>
					<if test="mem_birthday != null">
						,mem_birthday
					</if>
					<if test="mem_zipcode != null">
						,mem_zipcode
					</if>
					<if test="mem_addr != null">
						,mem_addr
					</if>
					<if test="mem_addr_dtl != null">
						,mem_addr_dtl
					</if>
					)
		VALUES (seq_member_no.nextval
					<if test="mem_uid != null">
						,#{mem_uid}
					</if>
					<if test="mem_pw != null">
						,#{mem_pw}
					</if>
					<if test="mem_name != null">
						,#{mem_name}
					</if>
					<if test="mem_nickname != null">
						,#{mem_nickname}
					</if>
					<if test="mem_email != null">
						,#{mem_email}
					</if>
					<if test="mem_tel != null">
						,#{mem_tel}
					</if>
					<if test="mem_gender != null">
						,#{mem_gender}
					</if>
					<if test="mem_birthday != null">
						,#{mem_birthday}
					</if>
					<if test="mem_zipcode != null">
						,#{mem_zipcode}
					</if>
					<if test="mem_addr != null">
						,#{mem_addr}
					</if>
					<if test="mem_addr_dtl != null">
						,#{mem_addr_dtl}
					</if>
					)
	</insert>
</mapper>