Almon Dev

게시판 만들기 #8 (마이페이지 수정) 본문

카테고리 없음

게시판 만들기 #8 (마이페이지 수정)

Almon 2025. 1. 23. 17:37

 마이페이지 수정 기능

마이페이지 생성에서 만들었던 마이페이지에 수정기능이 빠져있어 추가했습니다.

 

마이페이지 수정 버튼

// login_successful.php
// 수정 전
<button class="mypageset-btn">수정하기</button>

// 수정 후
<button class="mypageset-btn" onclick="location.href='/mypage_update.php'">수정하기</button>

마이페이지의 수정 버튼을 클릭했을 때 mypage_update.php로 리다이렉트 하도록 변경했습니다.

 

mypage_update.php

<?php
require_once("jwt_auth.php");
require_once("mysql.php");

if ($token = Jwt_auth::auth()) {
    $user_id = $token->sub;
    $sql = "select profile_img_path, nickname, email, password, name from users where user_id='$user_id'";
    try {
        $result = runSQL($sql)->fetch_array();
        $db_pass = $result['password'];
        $nickname = $result['nickname'];
        $email = $result['email'];
        $img_path = $result['profile_img_path'] ?? "almond-profile.jpg";
        $name = $result['name'];
    } catch(Exception $e) {
        // sql 실패
        die("계정 정보를 확인할 수 없습니다.")
    }

}else {
    header("Location: login2.php");
    exit;
}

?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>내 정보 변경</title>
    <link rel="stylesheet" href="/css/login_successful.css">
    <link rel="stylesheet" href="/css/mypage_update.css">
    <script src="./script/mypage_update.js"></script>
    <script src="./script/chang_img.js" defer></script>
</head>
<body>
    <container class="profile-container">
        <form action="" class="mypage-form" enctype="multipart/form-data">
            <p class="mypage-row update-mypage mypage profile">
                <label for="profile_img">프로필 : </label>
                <input type="file" id="profile_img_input" name="profile_img" style="display: none;" accept=".jpg, .png, .gif, .jpeg">
                <img src="/upload/profile_img/<?=$img_path ?>" class="profile_img" id="profile_img" onclick="profileImgUpload()">
            </p>
            <p class="mypage-row update-mypage mypage name">
                <label for="nickname">이름 : </label>
                <input type="text" id="name" class="mypage update name" name="name" value=<?=$name?> required >
            </p>
            <p class="mypage-row update-mypage mypage nickname">
                <label for="nickname">닉네임 : </label>
                <input type="text" id="nickname" class="mypage update nickname" name="nickname" value=<?=$nickname?> required >
            </p>
            <p class="mypage-row update-mypage mypage email">
                <label for="email">이메일 : </label>
                <input type="email" id="email" class="mypage update email" name="email" value=<?=$email?> required>
            </p>
            <p class="mypage-row update-mypage mypage pass-btn">
                <label for="pass">비밀번호 : </label>
                <button id="pass" onclick="updatePass(event)">수정</button class="mypage update pass-btn">
            </p>
            <p class="mypage-row update-mypage mypage">
                <button class="mypageset-btn on" onclick="updateSubmit(event)">수정완료</button>
            </p>
        </form>
        <p class="fail">실패</p>
    </container>
</body>
</html>

jwt token을 이용해서 유저를 식별한 뒤 해당 유저의 정보를 가져와 input태그에 넣어서 출력하는 코드입니다.

 

<?php
require_once("jwt_auth.php");
require_once("mysql.php");

if ($token = Jwt_auth::auth()) {
    $user_id = $token->sub;

Jwt_auth의 auth 메서드를 이용해서 토큰을 추출해서 $token에 저장합니다.

$user_id에 jwt token의 sub에 저장된 user_id를 저장합니다.

JWT에 대한 정리 : https://almon.tistory.com/21#toc5

 

    $sql = "select profile_img_path, nickname, email, password, name from users where user_id='$user_id'";
    try {
        $result = runSQL($sql)->fetch_array();
        $db_pass = $result['password'];
        $nickname = $result['nickname'];
        $email = $result['email'];
        $img_path = $result['profile_img_path'] ?? "almond-profile.jpg";
        $name = $result['name'];
    } catch(Exception $e) {
        // sql 실패
        die("계정 정보를 확인할 수 없습니다.")
    }

토큰에서 가져온 user_id를 이용해서 db에서 정보를 가져오고 각 변수에 저장합니다.

try 함수를 사용해서 에러가 발생하는 경우를 처리합니다.

 

    <p class="mypage-row update-mypage mypage profile">
        <label for="profile_img">프로필 : </label>
        <input type="file" id="profile_img_input" name="profile_img" style="display: none;" accept=".jpg, .png, .gif, .jpeg">
        <img src="/upload/profile_img/<?=$img_path ?>" class="profile_img" id="profile_img" onclick="profileImgUpload()">
    </p>
function profileImgUpload() {
  const uploadTag = document.querySelector('#profile_img_input');
  uploadTag.click();
}

input 태그의 file 타입을 이용해서 파일을 업로드할 수 있는 태그를 생성합니다.

img 태그의 onclick 핸들러를 이용해서 이미지를 클릭했을 때 input 태그를 클릭하는 JS를 실행합니다.

style="display:none" : 화면에 input 태그를 숨깁니다.
accept=".jpg .jpeg .png .gif" : accept를 이용해서 파일 확장자를 제한합니다.

 

    <p class="mypage-row update-mypage mypage name">
        <label for="nickname">이름 : </label>
        <input type="text" id="name" class="mypage update name" name="name" value=<?=$name?> required >
    </p>
    <p class="mypage-row update-mypage mypage nickname">
        <label for="nickname">닉네임 : </label>
        <input type="text" id="nickname" class="mypage update nickname" name="nickname" value=<?=$nickname?> required >
    </p>
    <p class="mypage-row update-mypage mypage email">
        <label for="email">이메일 : </label>
        <input type="email" id="email" class="mypage update email" name="email" value=<?=$email?> required>
    </p>
    <p class="mypage-row update-mypage mypage pass-btn">
        <label for="pass">비밀번호 : </label>
        <button id="pass" onclick="updatePass(event)">수정</button class="mypage update pass-btn">
    </p>
    <p class="mypage-row update-mypage mypage">
        <button class="mypageset-btn on" onclick="updateSubmit(event)">수정완료</button>
    </p>

<?=변수명 ?>을 이용해서 db에서 가져온 정보를 input태그의 value에 추가합니다.

정보 수정 페이지


function updatePass(e) {
  e.preventDefault();
  const passBtnRow = document.querySelector('.mypage-row.pass-btn');
  const emailRow = document.querySelector('.mypage-row.email');
  const container = document.querySelector('.profile-container');

  const passTag = `
  <p class="mypage-row update-mypage mypage pass">
    <label for="pass">비밀번호 : </label>
    <input type="password" class="mypage update pass" name="password" id="pass"/>
  </p>
  <p class="mypage-row update-mypage mypage pass-check">
    <label for="pass-check">비밀번호 확인 : </label>
    <input type="password" class="mypage update pass-check" name="password-check" id="pass-check"/>
  </p>
  `;

  passBtnRow.remove();
  emailRow.insertAdjacentHTML('afterend', passTag);
  container.classList.add('pass-update');
}

비밀번호 수정 버튼을 클릭하면 updatePass 함수가 실행됩니다.

 

e.preventDefault();

이벤트가 발생했을 때 기본 동작을 하지 않도록 합니다.

ex) submit 이벤트시 리다이렉트를 하지 않도록 합니다.

 

  const passBtnRow = document.querySelector('.mypage-row.pass-btn');
  const emailRow = document.querySelector('.mypage-row.email');
  const container = document.querySelector('.profile-container');

비밀번호 수정 버튼 태그와 이메일 수정 태그를 가져옵니다.

컨테이너도 크기 수정을 위해 가져옵니다. 

 

  const passTag = `
  <p class="mypage-row update-mypage mypage pass">
    <label for="pass">비밀번호 : </label>
    <input type="password" class="mypage update pass" name="password" id="pass"/>
  </p>
  <p class="mypage-row update-mypage mypage pass-check">
    <label for="pass-check">비밀번호 확인 : </label>
    <input type="password" class="mypage update pass-check" name="password-check" id="pass-check"/>
  </p>
  `;

비밀번호를 입력할 태그를 생성하고 passTag에 저장합니다.

 

  passBtnRow.remove();

비밀번호 수정 버튼 태그를 제거합니다.

 

  emailRow.insertAdjacentHTML('afterend', passTag);

이메일 태그의 뒷부분에 생성한 비밀번호를 입력할 input 태그들을 삽입합니다.

 

  container.classList.add('pass-update');

컨테이너 태그에 pass-update 클래스를 추가합니다.

pass-update 클래스는 height를 600px로 키워줍니다.

비밀번호 수정 버튼 클릭 전
비밀번호 수정 버튼 클릭 후


const imgInput = document.querySelector('#profile_img_input');
const imgTag = document.querySelector('#profile_img');
imgInput.addEventListener('change', (e) => {
  const imgFile = e.target.files[0];

  if (imgFile) {
    const fileReader = new FileReader();
    fileReader.onload = (e) => {
      imgTag.src = e.target.result;
    };
    fileReader.readAsDataURL(imgFile);
  }
});

input 태그에서 수정할 프로필 사진을 등록했을 때 img 태그의 사진이 변경되는 js 코드입니다.

 

const imgInput = document.querySelector('#profile_img_input');
const imgTag = document.querySelector('#profile_img');

이미지 파일을 선택하는 input 태그와 가져온 이미지를 출력할 img 태그를 가져옵니다.

 

imgInput.addEventListener('change', (e) => {

change 이벤트가 발생할 경우 함수를 실행합니다.

change 이벤트는 태그에 변화가 생겼을 때 발생합니다.
ex) value 변경, 파일 선택 (같은 파일 X)

 

  const imgFile = e.target.files[0];

e.target인 input 태그로부터 선택한 파일을 가져와 imgFile에 저장합니다.

 

  if (imgFile) {
    const fileReader = new FileReader();

imgFile을 성공적으로 가져왔다면 FileReader 객체를 생성합니다.

FileReader 객체는 Data URL형식으로 파일을 가져올 수 있습니다.
이걸 img 태그의 src에 삽입할 것입니다.

 

    fileReader.onload = (e) => {
      imgTag.src = e.target.result;
    };

fileReader가 로드가 되면 img 태그의 src에 읽어온 파일을 삽입합니다.

onload 역시 addEventListener처럼 등록을 해두면 이벤트 발생 시 콜백 함수가 실행됩니다.

 

    fileReader.readAsDataURL(imgFile);

FileReader를 이용해 DataURL 타입으로 선택한 파일을 읽어옵니다.

그리고 모두 읽어오면 결과가 img 태그의 src로 삽입됩니다.

Data URL
Data URL은 데이터를 텍스트로 인코딩한 것입니다. 별도로 파일을 요청할 필요 없이 파일을 처리할 수 있습니다.


Data URL 구조
data:[<mediatype>][;base64],<data>
[<mediatype>] : MIME 타입입니다. ex) jpeg 파일 => 'image/jpeg'
[;base64] : 이 부분이 있으면 base64로 인코딩 되었음을 의미합니다. => text일 경우 base64를 안 쓰기도 합니다.
<data> : 실제 데이터

정보 변경 페이지
사진 선택
프로필 이미지 변경


function updateSubmit(e) {
  e.preventDefault();
  const updateForm = document.querySelector('.mypage-form');
  const formData = new FormData(updateForm);

  fetch('/mypage_update_proc.php', {
    method: 'POST',
    body: formData,
  })
    // .then((res) => res.text())
    // .then((res) => console.log(res));
    .then((res) => res.json())
    .then((res) => {
      const fail = document.querySelector('.fail');
      if (res.result) {
        if (res.password) {
          location.href = 'logout.php';
        } else {
          fail.classList.remove('on');
          alert('회원정보가 수정되었습니다.');
          location.href = 'login_successful.php';
        }
      } else {
        fail.classList.add('on');
      }
    });
}

수정완료 버튼을 클릭하면 updateSubmit() 함수가 실행됩니다.

이 함수는 form 태그의 데이터를 mypage_update_proc.php에 전송하고 결과를 처리하는 역할입니다.

 

function updateSubmit(e) {
  e.preventDefault();
  const updateForm = document.querySelector('.mypage-form');
  const formData = new FormData(updateForm);

submit 버튼을 클릭 시 기본 동작을 막고 form의 데이터를 가져옵니다.

FormData는 multipart/form-data MIME 타입이기 때문에 파일 전송에 사용할 수 있습니다.

 

  fetch('/mypage_update_proc.php', {
    method: 'POST',
    body: formData,
  })

mypage_update_proc.php에 formData를 포함한 POST 요청을 보냅니다.

 

    .then((res) => res.json())

응답이 오면 json을 파싱해 javascript 객체로 변환합니다.

 

.then((res) => {
      const fail = document.querySelector('.fail');
      if (res.result) {
          ~~~
      } else {
        fail.classList.add('on');
      }

응답에 담긴 결과가 실패일 경우 실패 메시지를 출력합니다.

fail 클래스를 가진 태그에 on 클래스를 추가하면 화면에 출력됩니다.

 

      if (res.result) {
        if (res.password) {
          location.href = 'logout.php';
        } else {
          fail.classList.remove('on');
          alert('회원정보가 수정되었습니다.');
          location.href = 'login_successful.php';
        }
      }

응답이 성공일 경우 비밀번호를 변경했다면 로그아웃을 시키고, 그렇지 않은 경우 성공 메시지를 띄우고 마이페이지로 돌아갑니다.


mypage_update_proc.php

<?php
require_once("jwt_auth.php");
require_once("mysql.php");

$result = false;
$pass_result = false;

// GET 메서드로 요청을 보내면 모두 null이 되어버림 (수정필요)
if(isset($_POST['name']) or isset($_POST['email']) or isset($_POST['password']) or isset($_FILES['profile_img'])) {
    if($token = Jwt_auth::auth()) {
        $user_id = $token->sub;
        $name = $_POST['name'] ?? null;
        $password = $_POST['password'] ?? null;
        $nickname = $_POST['nickname'] ?? null;
        $email = $_POST['email'] ?? null;
        $password_filtered = preg_replace("/\s+/", "", $password);
    
        $file = $_FILES['profile_img'];
        // var_dump($file);
        $upload_dir = './upload/profile_img/';
        $extension = ['jpg', 'png', 'gif', 'jpeg'];
    
        $sql = "update users set";
        $img_result = false;
        $pass_result = false;
    
        if($name != null) {
            $sql = $sql . " name='$name'";
        }
    
        if ($nickname != null) {
            $sql = $sql . ", nickname='$nickname'";
        }
    
        if ($email != null) {
            $sql = $sql . ", email='$email'";
        }
    
        if($password != null && $password === $password_filtered) {
            $pass_hash = password_hash($password, PASSWORD_ARGON2ID);
            $sql = $sql . ", password='$pass_hash'";
            $pass_result = true;
        }
    
        if($file['error'] == UPLOAD_ERR_OK) {
            $file_name = basename($file['name']);
            $path_info = pathinfo($file_name);
            $file_extension = strtolower($path_info['extension']);
            
            if (!in_array($file_extension, $extension)) {
                die("파일 확장자를 확인해 주세요. \n'jpg', 'jpeg', 'png', 'gif'만 사용이 가능합니다.");
            }
    
            $sql = $sql . ", profile_img_path='$file_name'";
            $upload_path = $upload_dir . $file_name;
    
            if(move_uploaded_file($file['tmp_name'], $upload_path)) {
                $img_result = true;
            }
        }
    
        $sql = $sql . " where user_id='$user_id'";
        
        // echo "$sql";
        $result = runSQL($sql);
    
    }else {
        header("Location: login2.php");
    }
}else {
    echo('요청을 확인해주세요');
    header("Location: login2.php");
    exit;
}

echo json_encode(["result" => $result, "password" => $pass_result, "img" => $img_result]);

입력받은 값이 있는지 검사한 후에 sql문에 포함시켜 db를 변경하는 코드입니다.

 

if(isset($_POST['name']) or isset($_POST['email']) or isset($_POST['password']) or isset($_FILES['profile_img'])) {
    if($token = Jwt_auth::auth()) {

이메일, 이름, 비밀번호, 프로필 사진 중 하나라도 변경점이 있는지 확인하고 토큰까지 확인한 뒤에 if문을 실행합니다.

검사하는 과정이 없을 때 GET 메서드로 요청을 보내면 변수 선언 부분에서 모두 null로 처리돼서
이름, 이메일, 사진이 null로 변경되는 점을 방지하기 위해서 추가했습니다.

 

        $user_id = $token->sub;
        $name = $_POST['name'] ?? null;
        $password = $_POST['password'] ?? null;
        $nickname = $_POST['nickname'] ?? null;
        $email = $_POST['email'] ?? null;
        $password_filtered = preg_replace("/\s+/", "", $password);

각 정보들을 변수에 저장합니다. 비밀번호의 경우 정규식으로 공백을 검사한 뒤 삭제한 변수를 생성합니다.

삼항 연산자를 이용해서 POST 요청이 없는 값을 null로 처리합니다.
나중에 if문을 통해서 sql문에 추가할지 말지를 결정하기 위해서입니다.

 

        $file = $_FILES['profile_img'];
        // var_dump($file);
        $upload_dir = './upload/profile_img/';
        $extension = ['jpg', 'png', 'gif', 'jpeg'];

업로드한 프로필 사진을 $file에 저장합니다. 그리고 업로드 파일을 저장할 경로와 허용할 확장자를 각 변수에 저장합니다.

확장자를 검사하는 이유는 파일 업로드 취약점을 방지하기 위합니다.
php와 같은 서버 실행 파일을 삽입하는 공격을 막기 위해 jpg, png, gif, jpeg 같은 사진 파일만 업로드할 수 있도록 화이트 리스트 기반 필터링을 진행합니다.

 

        $sql = "update users set";
        $img_result = false;
        $pass_result = false;
    
        if($name != null) {
            $sql = $sql . " name='$name'";
        }
    
        if ($nickname != null) {
            $sql = $sql . ", nickname='$nickname'";
        }
    
        if ($email != null) {
            $sql = $sql . ", email='$email'";
        }
    
        if($password != null && $password === $password_filtered) {
            $pass_hash = password_hash($password, PASSWORD_ARGON2ID);
            $sql = $sql . ", password='$pass_hash'";
            $pass_result = true;
        }

$sql에 update문의 기본 형식을 작성하고 if문을 통해 정보를 변경하는 경우에만 update문을 추가합니다. 비밀번호의 경우 공백이 포함되지 않은 것 역시 검사를 하고 해시화를 거쳐 sql문에 추가합니다.

$password === $password_filtered에서 ===를 사용하는 이유는 ==의 경우 숫자로 이루어진 문자열을 숫자로 변환해서 비교하기 때문입니다. 예를 들어 "1234"와 "1234 "처럼 공백이 포함되었음에도 같은 값으로 취급하여 참이 반환됩니다.

타입 변환
== 의 경우 비교를 할 때 문자열이 숫자로 이루어진 경우 숫자로 변경해서 비교합니다.

ex)
"1234" == '1234 '
'1234 '중 1234만 숫자로 변경
1234 == 1234  true

 

    if($file['error'] == UPLOAD_ERR_OK) {
            $file_name = basename($file['name']);

$file ['error'] == UPLOAD_ERR_OK의 경우 정상적으로 업로드된 경우만 true를 반환합니다.

정상적으로 업로드된 경우 파일의 이름을 $file_name에 삽입합니다.

 

            $path_info = pathinfo($file_name);

pathinfo 함수를 이용해 파일의 경로, 확장자, 이름을 분리합니다.

 

            $file_extension = strtolower($path_info['extension']);

파일의 확장자를 소문자로 변환해서 $file_extension 변수에 저장합니다.

소문자로 변환하는 이유는 PHp, PhP처럼 대문자와 소문자를 섞어 우회하는 경우를 방지하기 위함입니다.

여기서는 jpg 등 이미지 파일만 통과시키기 때문에 의미는 없지만 소문자로 변환해서 검사하면 Jpg PnG 등의 파일도 검사를 통과합니다.

 

            if (!in_array($file_extension, $extension)) {
                die("파일 확장자를 확인해 주세요. \n'jpg', 'jpeg', 'png', 'gif'만 사용이 가능합니다.");
            }

파일 확장자 검사를 실패하면 실패 메시지를 출력합니다.

 

            $sql = $sql . ", profile_img_path='$file_name'";
            $upload_path = $upload_dir . $file_name;

확장자 검사를 통과하면 sql문에 프로필 파일 이름을 수정하는 구문을 추가하고, 프로필 사진을 업로드할 경로를 $upload_path에 설정합니다.

 

            if(move_uploaded_file($file['tmp_name'], $upload_path)) {
                $img_result = true;
            }

move_uploaded_file 함수를 이용해 임시 폴더에 저장되었던 프로필 사진을 우리가 설정한 경로로 이동시킵니다.

그리고 이동에 성공하면 $img_result를 true로 설정합니다.

 

        $sql = $sql . " where user_id='$user_id'";
        
        // echo "$sql";
        $result = runSQL($sql);

모든 업데이트문을 완성하면 $sql에 유저를 식별하는 where문을 추가하고 update문을 실행합니다.

 

echo json_encode(["result" => $result, "password" => $pass_result, "img" => $img_result]);

echo를 이용해 js의 updateSubmit 함수로 결과를 담아 응답을 보냅니다.

 

정보 수정 페이지
이름, 닉네임, 이메일 수정
수정사항 확인