관리 메뉴

SIMPLE & UNIQUE

5회차_3강 : 이메일 인증을 통한 비밀번호 재설정 기능을 구현한다. 본문

탈잉 강의 자료/react.js(프론트) + node.js(백앤드) 개발에서 배포까지

5회차_3강 : 이메일 인증을 통한 비밀번호 재설정 기능을 구현한다.

착한코딩 2020. 2. 14. 03:00

5_3 목표 : 이메일 HTML 탬플릿을 작성한다. 사용자 아이디와 토큰을 함께 전송한다. 토큰을 이용해 사용자 인증을 구현한다.

 

1. 비밀번호 재설정 요청을 하면 이메일을 발송하는 기능을 구현한다.

 

1) react 경로 C:\Users\ljung\OneDrive\문서\taling0102\client\src\components에 있는 LoginForm.js를 아래와 같이 수정한다.

 

import React, { Component } from 'react';
import {Redirect, Link} from 'react-router-dom'
import cookie from 'react-cookies';
import $ from 'jquery';
import axios from "axios";
import Swal from 'sweetalert2'

class LoginForm extends Component {
    constructor (props) {
        super(props);

        this.state = {
            prop:props
        }
    }
    submitClick = async e => {
        this.email_val = $('#email_val').val();
        this.pwd_val = $('#pwd_val').val();
        if(this.email_val === '' || this.pwd_val === ''){
            this.sweetalert('이메일과 비밀번호를 확인해주세요.', '', 'info', '닫기')
        }else{
            axios.post('/api/LoginForm?type=signin', {
                is_Email: this.email_val,
                is_Password: this.pwd_val
            })
            .then( response => {
                try {
                    var userid = response.data.json[0].useremail
                    var username = response.data.json[0].username
                    var userflag = response.data.json[0].userflag
                    var upw = response.data.json[0].userpassword
                    
                    if(userid != null && userid != ''){
                        this.sweetalert('로그인 되었습니다.', '', 'info', '닫기')
                        //로그인 id 세션에 저장
                        const expires = new Date()
                        expires.setMinutes(expires.getMinutes() + 60)
                        
                        //userid 와 username을 비밀키로 암호화해 쿠키값에 세팅한다.
                        axios.post('/api/LoginForm?type=SessionState', {
                            is_Email: userid,
                            is_UserName: username,
                        })
                        .then( response => {

                            cookie.save('userid', response.data.token1
                            , {
                                path: '/',
                                expires,
                                // httpOnly: true // 도메인 연결 후 주석해제
                            })
                            cookie.save('username', response.data.token2
                            , {
                                path: '/',
                                expires,
                                // httpOnly: true // 도메인 연결 후 주석해제
                            })

                            cookie.save('userpassword', upw
                            , {
                                path: '/',
                                expires,
                                // httpOnly: true // 도메인 연결 후 주석해제
                            })
                            cookie.save('user_flag', 'Y'
                                , {
                                    path: '/',
                                    expires
                                }
                            );
                        })  
                        .catch( error => {this.sweetalert('작업중 오류가 발생하였습니다.', error, 'error', '닫기'); return false; } );
                        
                        setTimeout(function() {
                            window.location.href = '/';
                        }.bind(this),1000
                        );

                    }else{
                        this.sweetalert('이메일과 비밀번호를 확인해주세요.', '', 'info', '닫기')
                    }
                } catch (error) {
                    this.sweetalert('이메일과 비밀번호를 확인해주세요.', '', 'info', '닫기')
                }
            })
            .catch( response => { alert(response);return false; } );
        }
    }

    // input value state
    handleChange = (e) => {
        this.setState({
            [e.target.name]: e.target.value
        })
    }

    // login form post
    handleSubmit = (e) => {
        // submit 페이지 리로딩 방지
        e.preventDefault();
    }

    // 비밀번호 재설정
    pwdResetClick = () => {
        $('.m_login').hide();
        $('.m_pw').fadeIn();
        $('.m_pw').css('display','table-cell');
    }

    //비밀전호 재설정용 메일 발송
    sendEmail = (email, subject, text, e) => {
        axios.post('/api/message?type=email&roll=resetpw', {
            is_Email : email,
            is_Subject : subject,
            is_Text: text
        })
        .then( response => {
            this.sweetalert('입력하신 이메일로 비밀번호 \n 재설정 메일 보내드렸습니다.', '', 'info', '닫기')
        })
        .catch( error => {this.sweetalert('작업중 오류가 발생하였습니다.', error, 'error', '닫기');return false;});
    }

    // //비밀번호 재설정 확인버튼
    pwdResetConfim = (e) => {
        this.reset_email = $('#reset_email_val').val();
        this.reset_name = $('#reset_name_val').val();
        if(this.reset_email === '' || this.reset_name === ''){
            this.sweetalert('이메일과 성명을 확인해주세요.', '', 'info', '닫기')
        }else{
            axios.post('/api/LoginForm?type=pwreset', {
                is_Email: this.reset_email,
                is_Name: this.reset_name,
            })
            .then( response => {
                var userid = response.data.json[0].useremail
                var username = response.data.json[0].username
                var userpassword = response.data.json[0].userpassword

                if(userid != null && userid != ''){
                    this.sendEmail(userid, 'rtrod 비밀번호 재설정 메일', userpassword)
                }else{
                    this.sweetalert('이메일과 성명을 확인해주세요.', '', 'info', '닫기')
                }
            })
            .catch( response => {this.sweetalert('이메일과 성명을 확인해주세요.', '', 'info', '닫기');return false; } );
        }
    }

    //alert 기본 함수
    sweetalert = (title, contents, icon, confirmButtonText) => {
        Swal.fire({
            title: title,
            text: contents,
            icon: icon,
            confirmButtonText: confirmButtonText
          })
    }

    render () {
        return (
            <section className="main">
            {/* <!-- 로그인폼 --> */}
                <div className="m_login">
                <h3><span><img src={require("../img/main/log_img.png")} alt="" /></span>LOGIN</h3>
                <div className="log_box">
                    <form onSubmit={this.handleSubmit}>
                    <div className="in_ty1">
                        <span><img src={require("../img/main/m_log_i3.png")} alt="" /></span>
                        <input type="text" id="email_val" name="email" placeholder="이메일" onChange={this.handleChange} />
                    </div>
                    <div  className="in_ty1">
                        <span className="ic_2"><img src={require("../img/main/m_log_i2.png")} alt="" /></span>
                        <input type="password" id="pwd_val" name="password" placeholder="비밀번호" onChange={this.handleChange} />
                    </div>
                    <ul className="af">
                        <li><Link to={'/register_check'}>회원가입</Link></li>
                        <li className="pwr_b" onClick={this.pwdResetClick}><a href="#n">비밀번호 재설정</a></li>
                    </ul>
                    {/* <input className="s_bt" type="submit" value="로그인" />	 */}
                    <button className="s_bt" type="submit" onClick={this.submitClick}>로그인</button>
                    </form>
                </div>
                </div>
            {/* <!-- 비밀번호 재설정 --> */}
                <div className="m_login m_pw">
                <h3 className="pw_ls">비밀번호 재설정 <span className="compl1">완료</span></h3>
                <div className="log_box">
            {/* <!-- 1단 --> */}
                    {/* <form method="post"> */}
                    <div className="pw_one">
                        <div className="in_ty1">
                        <span><img src={require("../img/main/m_log_i3.png")} alt="" /></span>
                        <input type="text" id="reset_email_val" name="" placeholder="이메일"/>
                        </div>
                        <div  className="in_ty1">
                        <span className=""><img src={require("../img/main/m_log_i1.png")} alt="" /></span>
                        <input type="text" id="reset_name_val" name="" placeholder="성명"/>
                        </div>
                        <div className="btn_confirm btn_confirm_m">
                        <div className="bt_ty bt_ty_m bt_ty1 cancel_ty1" onClick={this.pwdResetCancleClick}>취소</div>
                        <a href="#n" className="bt_ty bt_ty_m bt_ty2 submit_ty1" onClick={this.pwdResetConfim}>확인</a>
                        </div>
                    </div>
            {/* <!-- 2단 가려둠-->  */}
                    <div className="pw_two">
                        <div className="in_ty1">
                        <span className="ic_2"><img src={require("../img/main/m_log_i2.png")} alt="" /></span>
                        <input type="password" name="" placeholder="새 비밀번호" />
                        </div>
                        <div className="in_ty1">
                        <span className="ic_2"><img src={require("../img/main/m_log_i2.png")} alt="" /></span>
                        <input type="password" name="" placeholder="새 비밀번호 확인" />
                        </div>
                        <div className="btn_confirm btn_confirm_m">
                        <div className="bt_ty bt_ty_m bt_ty1 cancel_ty1">취소</div>
                        <a href="#n" className="bt_ty bt_ty_m bt_ty2 submit_ty1">재설정</a>
                        </div>
                    </div>
            {/* <!-- 3단 가려둠 --> */}
                    <div className="pw_tree">
                        <div className="">
                        <p>
                            '<span>홍길동</span>'
                            님의 비밀번호가 재설정되었습니다.
                        </p>		
                        
                        </div>
                        <input className="s_bt" type="submit" value="로그인 이동" />	
                    </div>
                    {/* </form> */}
                </div>
                </div>
                
            </section>
        );
    }
}

LoginForm.defaultProps = {
}

export default LoginForm;

2) node 경로 C:\Users\ljung\OneDrive\문서\taling0102에 있는 server.js를 아래와 같이 수정한다.

var express = require('express');

var indexRouter = require('./routes/index');
var swtoolRouter = require("./routes/SwtoolRout");
var fileuploadRouter = require("./routes/UploadRout");
var BatchRout = require("./routes/BatchRout");
var usersRouter = require("./routes/UsersRout");
var MessageRoutRouter = require("./routes/MessageRout");

var app = express();

app.use('/', indexRouter);

//sw Tool 조회
app.use("/api/Swtool", swtoolRouter);

//파일 업로드
app.use("/api/upload", fileuploadRouter);

//파일 업로드 경로 설정
app.use(express.static("./uploads"));

//로그인 조회
app.use("/api/LoginForm", usersRouter);

//회원가입 정보 입력
app.use("/api/register", usersRouter);

//시스템 배치
app.use("/api/BatchRout", BatchRout);

//알림, 메일 전송
app.use("/api/message", MessageRoutRouter);

const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Listening on port ${port}`));

 

3) node 경로 C:\Users\ljung\OneDrive\문서\taling0102\routes에 MessageRout.js 파일을 추가하고 아래 내용을 붙여 넣는다.

var express = require('express');

var router = express.Router();
const bodyParser = require('body-parser');

router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true }));

var usersModule = require('../modules/MessageModule');

router.get('/', function(req, res, next){
    router.use('/', usersModule);
    next('route')
});

//post 테스트
router.post('/', (req, res, next) => {
    router.use('/', usersModule);
    next('route')
});

module.exports = router;

4) node 경로 C:\Users\ljung\OneDrive\문서\taling0102\modules MessageModule.js 파일을 추가하고 아래 내용을 붙여 넣는다.

var express = require('express');
var router = express.Router();
const bodyParser = require('body-parser');
const nodemailer = require('nodemailer');
var fs = require('fs')
var ip = require('ip');
const axios = require('axios');

router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true }));

router.post('/', (req, res, next) => {
  var m_typ = req.query.type;

  if(m_typ == 'email'){
    //이메일 발송
    var m_roll = req.query.roll;
    let email = req.body.is_Email;
    let subject = req.body.is_Subject;
    var text = req.body.is_Text;
    var postpone_txt = text
    text = text.substr(0, 20)

    let transporter = nodemailer.createTransport({
      service: 'gmail',
      host: 'smtp.gmail.com',
      port: 465,
      secure: true,
      auth: {
        user: '0103288****a@gmail.com',  // gmail 계정 아이디를 입력
        pass: '0103288****'          // gmail 계정의 비밀번호를 입력
      }
    });

    var home_url = ''
    if(ip.address() == '172.17.0.2'){
      home_url = 'http://15.164.5.237:3000'
    }else{//운영서버 ip가 아닌 경우 모두 로컬처리
      home_url = 'http://localhost:3000'
    }

    var toHtml = ''
    if(m_roll == 'resetpw'){
      // 비밀번호 재설정 메일 발송
      fs.readFile(__dirname+'/template/mail_template_pw.html', function (err, html) {
        toHtml = html.toString()
        toHtml = toHtml.replace('{replacetext}', home_url+'/PwChangeForm/'+ email +'/'+text)
      })
    }else if(m_roll == '2daybefore'){
        // 프로젝트 종료 2일전 메일 발송
        var pjtName = req.body.is_PjtName;
        fs.readFile(__dirname+'/template/mail_template_notice.html', function (err, html) {
          toHtml = html.toString()
          toHtml = toHtml.replace('{replacetext}', pjtName)
        })
    }else if(m_roll == 'basic'){
      // 단순 알림 메일 발송
      fs.readFile(__dirname+'/template/mail_template_basic.html', function (err, html) {
        toHtml = html.toString()
        toHtml = toHtml.replace('{replacetext}', postpone_txt)
      })
     }

    var email_use = true
    axios.post(home_url+'/api/system?type=system', {
      //사이트 이메일 사용유무 체크
    })
    .then( response => {
      if(response.data.json[0].email_ym == 'Y'){
        email_use = true
      }else if(response.data.json[0].email_ym == 'N'){
        email_use =  false
      }
    })
    .catch( response => {console.log('fail1'+response)} ); 

    setTimeout(function() {
      let mailOptions = {
        from: 'rtrodemail@gmail.com',    // 발송 메일 주소 (위에서 작성한 gmail 계정 아이디)
        to: email ,                     // 수신 메일 주소
        subject: subject,   // 제목
        html : toHtml
      };

    if(email_use){
      transporter.sendMail(mailOptions, function(error, info){
        if (error) {
          console.log(error);
        }
        else {
          console.log('Email sent: ' + info.response);
        }
      });
    }else{
      console.log('Email sent: 이메일 사용유무를 Y로 변경하세요 email_use :'+email_use);
    }
      res.redirect("/");
    }.bind(this),1000
    );
  }

});

module.exports = router;

## 참고 ##

  G-mail 자원을 정상적으로 사용하려면 
https://myaccount.google.com/lesssecureapps?pli=1 에서 보안수준이 낮은 엑세스를 허용하고

https://accounts.google.com/DisplayUnlockCaptcha 에서 계정에대한 엑세스를 허용해야한다. 

출처 : https://velog.io/@npcode9194/NodeJS-nodemailer-%EB%AA%A8%EB%93%88%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-Gmail-API-%EC%82%AC%EC%9A%A9-hhjwgcmhsh

 

5) node 경로 C:\Users\ljung\OneDrive\문서\taling0102\modules  template 폴더를 추가하고 mail_template_pw.html 파일을 만들어 아래 내용을 붙여 넣는다.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<!--[if gte mso 9]>
<xml>
  <o:OfficeDocumentSettings>
    <o:AllowPNG/>
    <o:PixelsPerInch>96</o:PixelsPerInch>
  </o:OfficeDocumentSettings>
</xml>
<![endif]-->
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="x-apple-disable-message-reformatting">
  <!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
  <title></title>
  <style type="text/css">
   body {
  margin: 0;
  padding: 0;
}

table, tr, td {
  vertical-align: top;
  border-collapse: collapse;
}

p, ul {
  margin: 0;
}

.ie-container table, .mso-container table {
  table-layout: fixed;
}

* {
  line-height: inherit;
}

a[x-apple-data-detectors=true] {
  color: inherit !important;
  text-decoration: none !important;
}

.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
  line-height: 100%;
}

[owa] .email-row .email-col {
  display: table-cell;
  float: none !important;
  vertical-align: top;
}

.ie-container .email-col-100, .ie-container .email-row, [owa] .email-col-100, [owa] .email-row { width: 650px !important; }
.ie-container .email-col-17, [owa] .email-col-17 { width: 110.50000000000001px !important; }
.ie-container .email-col-25, [owa] .email-col-25 { width: 162.5px !important; }
.ie-container .email-col-33, [owa] .email-col-33 { width: 214.5px !important; }
.ie-container .email-col-50, [owa] .email-col-50 { width: 325px !important; }
.ie-container .email-col-67, [owa] .email-col-67 { width: 435.5px !important; }

@media only screen and (min-width: 670px) {
  .email-row { width: 650px !important; }
  .email-row .email-col { vertical-align: top; }
  .email-row .email-col-100 { width: 650px !important; }
  .email-row .email-col-67 { width: 435.5px !important; }
  .email-row .email-col-50 { width: 325px !important; }
  .email-row .email-col-33 { width: 214.5px !important; }
  .email-row .email-col-25 { width: 162.5px !important; }
  .email-row .email-col-17 { width: 110.50000000000001px !important; }
}

@media (max-width: 670px) {
  .hide-mobile { display: none !important; }
  .email-row-container {
    padding-left: 0px !important;
    padding-right: 0px !important;
  }
  .email-row .email-col {
    min-width: 320px !important;
    max-width: 100% !important;
    display: block !important;
  }
  .email-row { width: calc(100% - 40px) !important; }
  .email-col { width: 100% !important; }
  .email-col > div { margin: 0 auto; }
  .no-stack .email-col { min-width: 0 !important; display: table-cell !important; }
  .no-stack .email-col-50 { width: 50% !important; }
  .no-stack .email-col-33 { width: 33% !important; }
  .no-stack .email-col-67 { width: 67% !important; }
  .no-stack .email-col-25 { width: 25% !important; }
  .no-stack .email-col-17 { width: 17% !important; }
} 

  </style>

<!--[if mso]>
<style type="text/css">
  ul li {
    list-style:disc inside;
    mso-special-format:bullet;
  }
</style>
<![endif]-->

<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css">
<style type="text/css">
  @import url('https://fonts.googleapis.com/css?family=Montserrat:400,700');
</style><!--<![endif]-->

</head>

<body class="clean-body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #f2f2f2">
  <!--[if IE]><div class="ie-container"><![endif]-->
  <!--[if mso]><div class="mso-container"><![endif]-->
  <table class="nl-container" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #f2f2f2;width:100%" cellpadding="0" cellspacing="0">
  <tbody>
  <tr style="vertical-align: top">
    <td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
    <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #f2f2f2;"><![endif]-->

<div class="email-row-container" style="padding: 25px 10px 0px;background-color: rgba(255,255,255,0)">
  <div style="Margin: 0 auto;min-width: 320px;max-width: 650px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;" class="email-row">
    <div style="border-collapse: collapse;display: table;width: 100%;background-color: #ffffff;">
      <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 25px 10px 0px;background-color: rgba(255,255,255,0);" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:650px;"><tr style="background-color: #ffffff;"><![endif]-->
      <!--[if (mso)|(IE)]></td><![endif]-->
      <!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
    </div>
  </div>
</div>

<div class="email-row-container" style="padding: 0px 10px;background-color: rgba(255,255,255,0)">
  <div style="Margin: 0 auto;min-width: 320px;max-width: 650px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;" class="email-row">
    <div style="border-collapse: collapse;display: table;width: 100%;background-color: #ffffff;">
      <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px 10px;background-color: rgba(255,255,255,0);" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:650px;"><tr style="background-color: #ffffff;"><![endif]-->

<!--[if (mso)|(IE)]><td align="center" width="650" style="width: 650px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="email-col email-col-100" style="max-width: 320px;min-width: 650px;display: table-cell;vertical-align: top;">
  <div style="width: 100% !important;">
  <!--[if (!mso)&(!IE)]><!--><div style="padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->

<table id="u_content_text_3" class="u_content_text" style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
  <tbody>
    <tr>
      <td style="overflow-wrap:break-word;word-break:break-word;padding:40px 20px 10px;font-family:arial,helvetica,sans-serif;" align="left">

  <div style="color: #000; line-height: 150%; text-align: center; word-wrap: break-word;">
    <p style="font-size: 14px; line-height: 150%;"><span style="font-size: 20px; line-height: 30px;">Distributed biohealth integrated analysis platform</span></p>
<p style="font-size: 14px; line-height: 150%;"><span style="font-size: 20px; line-height: 30px;"><strong>centered on immune disease research</strong>.</span></p>
  </div>

      </td>
    </tr>
  </tbody>
</table>

<table id="u_content_text_4" class="u_content_text" style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
  <tbody>
    <tr>
      <td style="overflow-wrap:break-word;word-break:break-word;padding:10px 20px;font-family:arial,helvetica,sans-serif;" align="left">

  <div style="color: #000; line-height: 150%; text-align: center; word-wrap: break-word;">
    <p style="font-size: 14px; line-height: 150%;"><span style="font-size: 20px; line-height: 30px;">Please feel free to contact us if you have any questions.</span></p>
  </div>

      </td>
    </tr>
  </tbody>
</table>

<table id="u_content_divider_1" class="u_content_divider" style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
  <tbody>
    <tr>
      <td style="overflow-wrap:break-word;word-break:break-word;padding:10px 20px;font-family:arial,helvetica,sans-serif;" align="left">

  <table height="0px" align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;border-top: 1px solid #CCC;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
    <tbody>
      <tr style="vertical-align: top">
        <td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;font-size: 0px;line-height: 0px;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
          <span>&#160;</span>
        </td>
      </tr>
    </tbody>
  </table>

      </td>
    </tr>
  </tbody>
</table>

<table id="u_content_text_5" class="u_content_text" style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
  <tbody>
    <tr>
      <td style="overflow-wrap:break-word;word-break:break-word;padding:10px 20px;font-family:arial,helvetica,sans-serif;" align="left">

  <div style="color: #07a88b; line-height: 150%; text-align: center; word-wrap: break-word;">
    <p style="font-size: 14px; line-height: 150%;"><strong><span style="font-size: 30px; line-height: 45px;">Real Time-Research On Demand</span></strong></p>
<p style="font-size: 14px; line-height: 150%;"><span style="font-size: 16px; line-height: 24px; color: #000000;"><span style="line-height: 24px; font-size: 16px;">Manage Your Project Easily</span></span></p>
  </div>

      </td>
    </tr>
  </tbody>
</table>

<table id="u_content_button_1" class="u_content_button" style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
  <tbody>
    <tr>
      <td style="overflow-wrap:break-word;word-break:break-word;padding:20px 20px 50px;font-family:arial,helvetica,sans-serif;" align="left">

<div align="center">
  <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:85px; v-text-anchor:middle; width:304px;" arcsize="59%" stroke="f" fillcolor="#28e0bf"><w:anchorlock/><center style="color:#FFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
    <a href="{replacetext}" target="_blank" style="display: inline-block;font-family:arial,helvetica,sans-serif;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFF; background-color: #28e0bf; border-radius: 50px; -webkit-border-radius: 50px; -moz-border-radius: 50px; width: auto; padding: 20px 50px; mso-border-alt: none;">
      <span style="line-height:150%;"><strong><span style="font-size: 30px; line-height: 45px;">비밀번호 변경하기</span></strong></span></a>
    </a>
  <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>

      </td>
    </tr>
  </tbody>
</table>

  <!--[if (!mso)&(!IE)]><!--></div><!--<![endif]-->
  </div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
      <!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
    </div>
  </div>
</div>

<div class="email-row-container" style="padding: 10px;background-color: rgba(255,255,255,0)">
  <div style="Margin: 0 auto;min-width: 320px;max-width: 650px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;" class="email-row">
    <div style="border-collapse: collapse;display: table;width: 100%;background-color: transparent;">
      <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 10px;background-color: rgba(255,255,255,0);" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:650px;"><tr style="background-color: transparent;"><![endif]-->

<!--[if (mso)|(IE)]><td align="center" width="650" style="width: 650px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="email-col email-col-100" style="max-width: 320px;min-width: 650px;display: table-cell;vertical-align: top;">
  <div style="width: 100% !important;">
  <!--[if (!mso)&(!IE)]><!--><div style="padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->

<table id="u_content_text_6" class="u_content_text" style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
  <tbody>
    <tr>
      <td style="overflow-wrap:break-word;word-break:break-word;padding:20px;font-family:arial,helvetica,sans-serif;" align="left">

  <div style="color: #999999; line-height: 120%; text-align: left; word-wrap: break-word;">
    <p style="font-size: 14px; line-height: 120%;">주소 : [16499] 경기도 수원시 영통구 월드컵로 164 홍재관 503호Tel : 031-219-4471.</p>
<p style="font-size: 14px; line-height: 120%;">&nbsp;</p>
<p style="font-size: 14px; line-height: 120%;">COPYRIGHT © 2019 RT-ROD, ALL RIGHTS RESERVED.</p>
  </div>

      </td>
    </tr>
  </tbody>
</table>

  <!--[if (!mso)&(!IE)]><!--></div><!--<![endif]-->
  </div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
      <!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
    </div>
  </div>
</div>

    <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
    </td>
  </tr>
  </tbody>
  </table>
  <!--[if (mso)|(IE)]></div><![endif]-->
</body>

</html>

 

node 경로에서 필요한 패키지들을 설치해준다.

npm install nodemailer -- save
npm install axios -- save
npm install fs -- save

 

2. 이메일에서 [비밀번호 변경하기]를 누르면 비밀번호 변경 가능 화면으로 이동시킨다.

 

1) react 경로 C:\Users\ljung\OneDrive\문서\taling0102\client\src\components 에 App.js 를 아래와 같이 수정한다.

import React, { Component } from 'react';
import { Router, Route, Switch } from "react-router";
import {Redirect} from 'react-router-dom'

import Api_test from './Api_test'
import cookie from 'react-cookies';
import axios from "axios";

// css
import '../css/new.css';
import '../css/owl.carousel.min.css';
import '../css/owl.theme.default.min.css';

// header
import HeaderAdmin from './Header/Header admin';

// footer
import Footer from './Footer/Footer';

// login
import LoginForm from './LoginForm';
import PwChangeForm from './PwChangeForm';

// admin floatingPopulationList
import floatingPopulationList from './Floating_population/floatingPopulationList';

// admin softwareinfo
import AdminSoftwareList from './SoftwareToolsManage/AdminSoftwareList';
import AdminSoftwareView from './SoftwareToolsManage/AdminSoftwareView';

// register
import Register from './Register/Register';
import RegisterCheck from './Register/RegisterCheck';

class App extends Component {
  constructor (props) {
    super(props);
    
    this.state = {
    }
}

  componentDidMount() {
      
    // 비밀번호 재설정 패이지를 제외하고, 세션이 유효하지 않으면 home url로 이동.
    if(window.location.pathname.indexOf('/PwChangeForm') == -1){

      //쿠키에서 userid, username을 가져와 복호화한다.
      axios.post('/api/LoginForm?type=SessionConfirm', {
        token1 : cookie.load('userid') 
        , token2 : cookie.load('username') 
      })
      .then( response => {
          this.state.userid = response.data.token1
          let password = cookie.load('userpassword')
          if(password == undefined){
            password = ''
          }
          axios.post('/api/LoginForm?type=pwemail', {
            is_Email: this.state.userid,
            is_Token : password
          })
          .then( response => {
            if(response.data.json[0] !== undefined){
              var userid = response.data.json[0].useremail
                
              if(userid == undefined || password == undefined){
                window.location.href = '/admin';
              }
            }else{
              // 계정정보가 유효하지 않다면 세션값 삭제후, 홈으로 이동
              if(window.location.hash != 'nocookie'){
                this.remove_cookie();
                window.location.href = '/nocookie/#nocookie';
              }  
            }
          })
          .catch( response => {
            this.remove_cookie()
            window.location.href = '/nocookie/#nocookie';
          } );
      })
      .catch( response => {window.location.href = '/nocookie/#nocookie';} );
    }
  }

  //쿠키 초기화
  remove_cookie = (e) => {
    cookie.remove('userid', { path: '/'});
    cookie.remove('username', { path: '/'});
    cookie.remove('user_flag', { path: '/'});
    cookie.remove('userpassword', { path: '/'});
  }

  render () {
    return (
      <div className="App">
          <HeaderAdmin/> 
          <Switch>
          {
              (cookie.load('userid') !== undefined) ? (
                <Route exact path='/' component={AdminSoftwareList} />
              ):(
                  <Route exact path='/admin' component={LoginForm} />
              )
            }
            <Route exact path='/' component={LoginForm} />
            <Route path='/nocookie' component={LoginForm} />
            <Route path='/Api_test' component={Api_test} />
            <Route path='/floatPopulationList' component={floatingPopulationList} />
            <Route path='/AdminSoftwareList' component={AdminSoftwareList} />
            <Route path='/AdminSoftwareView/:swtcode' component={AdminSoftwareView} />
            <Route path='/register' component={Register} />
            <Route path='/register_check' component={RegisterCheck} />
            <Route path='/PwChangeForm/:email/:token' component={PwChangeForm} />
          </Switch>
          <Footer 
            footer_address={this.props.footer_address} 
            footer_tel={this.props.footer_tel}  
            footer_email={this.props.footer_email} 
            footer_mobile={this.props.footer_mobile} 
          />
      </div>
    );
  }
}

App.defaultProps = {
  // footer value
  footer_address: '[34234] 서울특별시 강남구 삼성동 111-114',
  footer_tel: '02-1234-5678',
  footer_email: 'ljung5@naver.com',
  footer_mobile: '010-3288-3398',
};

export default App

2) react 경로 C:\Users\ljung\OneDrive\문서\taling0102\client\src\components 에 PwChangeForm.js 파일을 생성하고 아래코드를 붙여넣는다.

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import Swal from 'sweetalert2'
import $ from 'jquery';
import axios from "axios";

class LoginForm extends Component {
    constructor (props) {
    super(props);
        this.state = {
            email: props.match.params.email, //이메일
            token: props.match.params.token, //이메일 token
        }
    }

    componentDidMount() {
        axios.post('/api/LoginForm?type=pwemail', {
            is_Email : this.state.email,
            is_Token : this.state.token,
        })
        .then( response => {
            if(response.data.json[0].usercode == undefined){
                window.location.replace('about:blank')
            }
        })
        .catch( error => {
            this.sweetalert('유효한 접속이 아닙니다.', error, 'error', '닫기')
            setTimeout(function() {
                window.location.replace('about:blank')    
                }.bind(this),1000
            );
        });
    }

    // 회원가입 버튼 클릭시 validate check
    submitClick = async (type, e) => {

        this.pwd_val_checker = $('#pwd_val').val();
        this.pwd_cnf_val_checker = $('#pwd_cnf_val').val();

        this.fnValidate = (e) => {
            var pattern1 = /[0-9]/;	// 숫자
            var pattern2 = /[a-zA-Z]/;	// 문자
            var pattern3 = /[~!@#$%^&*()_+|<>?:{}]/;	// 특수문자

            // ## pwd check start 
            if(this.pwd_val_checker ==='') {
                $('#pwd_val').addClass('border_validate_err');
                this.sweetalert('비밀번호를 입력해주세요.', '', 'info', '닫기')
                return false;
            }
            if(this.pwd_val_checker !='') {
                var str = this.pwd_val_checker;
                if(str.search(/\s/) != -1) {
                        $('#pwd_val').addClass('border_validate_err');
                        this.sweetalert('비밀번호 공백을 제거해 주세요.', '', 'info', '닫기')
                        return false;
                } 
                if(!pattern1.test(str) || !pattern2.test(str) || !pattern3.test(str) || str.length < 8 || str.length > 16) {
                        $('#pwd_val').addClass('border_validate_err');
                        this.sweetalert('8~16자 영문 대 소문자, 숫자\n 특수문자를 사용하세요.', '', 'info', '닫기')
                        return false; 
                } 
            }
            $('#pwd_val').removeClass('border_validate_err');

            if(this.pwd_cnf_val_checker ==='') {
                $('#pwd_cnf_val').addClass('border_validate_err');
                this.sweetalert('비밀번호 확인을 입력해주세요.', '', 'info', '닫기')
                return false;
            }
            if(this.pwd_val_checker != this.pwd_cnf_val_checker) {
                $('#pwd_val').addClass('border_validate_err');
                $('#pwd_cnf_val').addClass('border_validate_err');
                this.sweetalert('비밀번호가 일치하지 않습니다.', '', 'info', '닫기')
                return false;
            }
            $('#pwd_cnf_val').removeClass('border_validate_err');
            // ## pwd check end 

            return true;
        }

        //회원 정보 유효성 체크
        if(this.fnValidate()){
            //form type To Json
            var type = 'pwdmodify'
            var jsonstr = $("form[name='frm']").serialize();
            //특수문자 깨짐 해결
            jsonstr = decodeURIComponent(jsonstr);
            var Json_form = JSON.stringify(jsonstr).replace(/\"/gi,'')
            Json_form = "{\"" +Json_form.replace(/\&/g,'\",\"').replace(/=/gi,'\":"')+"\"}";
            
            try {
                const response = await fetch('/api/register?type='+type, {
                    method: 'POST',
                    headers: {
                    'Content-Type': 'application/json',
                    },
                    //한글 디코딩
                    body: Json_form,
                });
                const body = await response.text();
                if(body == "succ"){
                    this.sweetalertSucc('비밀번호 수정이 완료되었습니다.', false)

                    //home url 호출
                    setTimeout(function() {
                        this.props.history.push('/');
                        }.bind(this),1500
                    );

                }else{
                    this.sweetalert('작업 중 오류가 발생하였습니다.', '', 'error', '닫기')
                }  
            } catch (error) {
                this.sweetalert('작업 중 오류가 발생하였습니다.', error, 'error', '닫기')
            }
        }//fnValidate end
    };

    //alert 기본 함수
    sweetalert = (title, contents, icon, confirmButtonText) => {
        Swal.fire({
            title: title,
            text: contents,
            icon: icon,
            confirmButtonText: confirmButtonText
          })
    }

    //alert 성공 함수
    sweetalertSucc = (title, showConfirmButton) => {
        Swal.fire({
            position: 'bottom-end',
            icon: 'success',
            title: title,
            showConfirmButton: showConfirmButton,
            timer: 1000
        })
    }

    render () {
        return (
            <section className="main">
                <div className="m_login">
                <h3 className="pw_ls">비밀번호 재설정 <span className="compl1">완료</span></h3>
                    <form method="post" name="frm" action="">
                        <input type="hidden" id="is_Useremail" name="is_Useremail" value={this.state.email}/>
                        <div className="log_box">
                            <div className="in_ty1">
                            <span className="ic_2"><img src={require("../img/main/m_log_i2.png")} alt="" /></span>
                            <input type="password" id="pwd_val" name="is_Password" placeholder="새 비밀번호" />
                            </div>
                            <div className="in_ty1">
                            <span className="ic_2"><img src={require("../img/main/m_log_i2.png")} alt="" /></span>
                            <input type="password" id="pwd_cnf_val" name="is_Password" placeholder="새 비밀번호 확인" />
                            </div>
                            <div className="btn_confirm btn_confirm_m">
                            <Link to={'/'}><div className="bt_ty bt_ty_m bt_ty1 cancel_ty1">취소</div></Link>
                            <a href="#n" className="bt_ty bt_ty_m bt_ty2 submit_ty1" onClick={(e) => this.submitClick('modify', e)}>재설정</a>
                            </div>
                        </div>
                    </form>
                </div>
            </section>
        );
    }
}

LoginForm.defaultProps = {
}

export default LoginForm;

 

3) 비밀번호 재설정을 요청하면 id와 동일한 메일로 아래와 같은 템플릿으로 발송된다. 
    여기서 변경하기를 누르면, 사이트 url로 이동해 비밀번호 초기화를 할 수 있다.

 

https://taling.me/Talent/Detail/19341

 

[3월/주말] REACT, NODE, MYSQL, AWS 개발부터 배포까지/ 따라하면 완성되는 웹프로젝트. | 탈잉

✔ 수업 목표 ⊙ Font-end(react), back-end(node), mysql 구조를 프레임워크화 한다. ⊙ SELECT, UPDATE, DELETE, INSERT를 각각 1세트씩 구현한다. (CRUD 세트를 참고해서 만들고 싶은 기능을 혼자 개발할 수 있어요) ⊙ AWS EC2 인스턴스를 생성하고, 서버에 접근하여 파일을 컨트롤 할 수 있다. ⊙ AWS RDS(mysql) 인스턴스를 생성하고, 필요한 테이블들을 관리할 수 있다. ✔ 제 수업만의

taling.me

 

Comments