PHP & JavaScript 게시판 사이트 제작 1

약 2년 전에 만들었던 게시판 사이트 소스 코드를 살펴보았는데, 소스 코드가 뒤죽박죽 섞여있고 기초적인 부분에서도 부족한 점이 많이 보여 이참에 다시 작성하기로 했다.

이번 글에서는 회원가입 및 로그인 기능을 구현하는 코드에 대하여 설명하겠다. 모든 디자인은 부트스트랩을 사용했다.


1. 부트스트랩 및 jQuery 스크립트 태그 추가

파일 최상단 또는 Head 파일에 다음과 같이 작성하여 부트스트랩 및 jQuery를 사용하기 위해 추가한다.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.1.js" integrity="sha256-3zlB5s2uwoUzrXK3BT7AX3FyvojsraNFxCc2vC/7pNI=" crossorigin="anonymous"></script>

2. 로그인 폼 작성

Index 파일은 로그인 폼으로 작성한다. 로그인 폼 이외에도 비밀번호 찾기와 회원가입으로 이동할 수 있는 <a>태그를 작성한다.

<form name="login_form" action="로그인 처리 파일 경로" method="post">
    <div class="loginwrap">
        <img src="로고 이미지 경로" class="img-responsive" ><br><br>
        <div class="form-floating mb-3">
            <input type="text" name="id" class="form-control" id="floatingID" autofocus="true" required="true" placeholder="ID" autocomplete="off">
            <label for="floatingID">아이디</label>
        </div>
        <div class="form-floating">
            <input type="password" name="password" class="form-control" id="floatingPassword" required="true" placeholder="Password">
            <label for="floatingPassword">비밀번호</label>
        </div><br>
        <button class="btn btn-primary form-control" id="signup_btn">로그인</button><br><br>
        <div style="text-align:right;">
            <a href="비밀번호 찾기 폼 경로">비밀번호 찾기</a> | <a href="회원가입 폼 경로">회원가입</a>
        </div>
    </div>
</form>

<script type="text/javascript" src="동작 구현 파일 경로"></script>

3. 회원가입 폼 작성

회원가입 폼에 각 회원 정보들을 입력할 수 있게 작성한다. 아이디에 한글 입력 시, 생년월일과 전화번호에 숫자 이외 입력 시 스크립트로 문자를 제거한다. 아이디 중복을 방지하기 위해 아이디 중복 확인 버튼을 작성하고 주소 검색 버튼 클릭 시 카카오 주소 검색 API를 사용하기 위해 스크립트 코드도 추가한다.

<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<form id="member_register_frm" method="post" action="회원가입 처리 파일 경로" enctype="multipart/form-data">
    <div class="signwrap"><br>
        <img src="로고 이미지 경로" class="img-responsive" ><br><hr><br>
        <table class="table table-borderless rounded-3 overflow-hidden">
            <tbody>
                <tr>
                    <th>아이디 <span style="color:red;">*</span></th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="text" id="member_id" name="member_id" placeholder="2~12자 이내 영문과 숫자" class="form-control" oninput="this.value=this.value.replace(/[^a-zA-Z-_0-9]/g,'');">&nbsp;
                        <button type="button" id="id_check_btn" class="btn btn-secondary">아이디 중복 확인</button>
                    </td>
                </tr>
                <tr>
                    <th>비밀번호 <span style="color:red;">*</span></th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="password" id="member_password" name="member_password" placeholder="4자 이상 영문과 숫자" class="form-control">								
                    </td>
                </tr>
                <tr>
                    <th>이름 <span style="color:red;">*</span></th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="text" id="member_first_name" name="member_first_name" placeholder="이름" class="form-control">&nbsp;
                        <input type="text" id="member_last_name" name="member_last_name" placeholder="성" class="form-control">
                    </td>
                </tr>
                <tr>
                    <th>생년월일<span style="color:red;">*</span></th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="text" id="member_birthY" name="member_birthY" placeholder="년(4자)" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');">&nbsp;
                        <select name="member_birthM" class="form-select">
                            <option value="">월</option>
                            <?php 
                                for($i=1; $i<13; $i++){
                                    if($i < 10) $ival = "0".$i;
                            ?>
                                    <option value="<?=$ival?>"><?=$i?></option>
                            <?php 
                                }
                            ?>
                        </select>&nbsp;
                        <input type="text" id="member_birthD" name="member_birthD" placeholder="일" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');">
                    </td>
                </tr>
                <tr>
                    <th>전화번호 <span style="color:red;">*</span></th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="text" id="member_phone" name="member_phone" placeholder="전화번호 입력" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');">
                    </td>
                </tr>
                <tr>
                    <th>주소 / 상세주소 *</th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="text" name="member_address" id="member_address" placeholder="도로명 또는 지번" class="form-control">&nbsp;
                        <input type="text" name="member_detail_address" id="member_detail_address" placeholder="ex) 101동 1001호" class="form-control">&nbsp;
                        <button type="button" id="postBtn" class="btn btn-secondary">검색</button>
                    </td>
                </tr>
                <tr>
                    <th>이메일 *</th>
                    <td class="form-group row">&nbsp;&nbsp;
                        <input type="text" id="member_email" name="member_email" placeholder="이메일" class="form-control">&nbsp;
                        <select name="member_email_select" class="form-select">
                            <option value="N">직접 입력</option>
                            <option value="naver.com">@naver.com</option>
                            <option value="hanmail.net">@hanmail.net</option>
                            <option value="daum.net">@daum.net</option>
                            <option value="nate.com">@nate.com</option>
                            <option value="gmail.com">@gmail.com</option>
                            <option value="yahoo.com">@yahoo.com</option>
                        </select>
                    </td>
                </tr>
            </tbody>
        </table><br>
        <div class="pagination justify-content-center">
            <button type="button" id="member_save" class="btn btn-primary">회원가입</button>
        </div>
    </div>
</form>

<script type="text/javascript" src="동작 구현 파일 경로"></script>

4. 회원 DB 생성 및 설정

회원가입 시 회원 정보가 저장되는 DB를 생성한다.

CREATE TABLE `member` (
	`no` INT(11) NOT NULL AUTO_INCREMENT,
	`member_id` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_password` VARCHAR(300) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_first_name` VARCHAR(20) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_last_name` VARCHAR(20) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_birthY` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_birthM` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_birthD` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_phone` VARCHAR(15) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_address` VARCHAR(200) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_detail_address` VARCHAR(200) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_email` VARCHAR(80) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_email_select` VARCHAR(30) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`member_joindate` DATETIME NULL DEFAULT NULL,
	PRIMARY KEY (`no`) USING BTREE,
	UNIQUE INDEX `member_id` (`member_id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

5. 비밀번호 찾기 폼 작성

비밀번호 찾기 폼은 간단하게 회원가입 시 입력했던 아이디와 전화번호로 본인확인 후 비밀번호를 초기화하고 재설정할 수 있도록 작성한다. 회원가입 폼과 같이 아이디와 전화번호는 스크립트로 문자 형식에 제한을 둔다.

본인확인이 완료되었다면 비밀번호 찾기 폼은 비밀번호 재설정 폼으로 변경된다.

<form id="member_find_frm" method="post" action="비밀번호 찾기 처리 파일 경로" enctype="multipart/form-data">
    <div class="findwrap"><br>
        <img src="로고 이미지 경로" class="img-responsive" ><br>
        <?php
            if($_GET["find_status"] == "confirmed" && !empty($_GET["member_no"])){
                echo "새로운 비밀번호를 설정하세요.";
            }else{
                echo "가입 시 입력한 아이디와 전화번호로 본인확인 후 비밀번호를 다시 설정할 수 있습니다.";
            }
        ?>
        <br><hr><br>
        <table class="table table-borderless rounded-3 overflow-hidden">
            <tbody>
                <?php
                    if($_GET["find_status"] == "confirmed" && !empty($_GET["member_no"])){
                ?>
                        <tr>
                            <th>비밀번호 <span style="color:red;">*</span></th>
                                <td class="form-group row">&nbsp;&nbsp;
                                    <input type="password" id="member_password_find" name="member_password_find" placeholder="4자 이상 영문과 숫자" class="form-control">
                                    <input type = "hidden" id="member_no" name = "member_no" value="<?=$_GET["member_no"]?>">
                                </td>
                        </tr>
                <?php
                    }else{
                ?>
                        <tr>
                            <th>아이디 <span style="color:red;">*</span></th>
                            <td class="form-group row">&nbsp;&nbsp;
                                <input type="text" id="member_id_find" name="member_id_find" placeholder="아이디 입력" class="form-control" oninput="this.value=this.value.replace(/[^a-zA-Z-_0-9]/g,'');">
                            </td>
                        </tr>
                        <tr>
                            <th>전화번호 <span style="color:red;">*</span></th>
                            <td class="form-group row">&nbsp;&nbsp;
                                <input type="text" id="member_phone_find" name="member_phone_find" placeholder="전화번호 입력" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');">
                            </td>
                        </tr>
                <?php
                    }
                ?>
            </tbody>
        </table><br>
        <div class="pagination justify-content-center">
            <?php
                if($_GET["find_status"] == "confirmed" && !empty($_GET["member_no"])){
            ?>
                    <button type="button" id="member_reset_pwd" class="btn btn-danger">비밀번호 재설정</button>
            <?php
                }else{
            ?>
                    <button type="button" id="member_find" class="btn btn-primary">비밀번호 찾기</button>
            <?php
                }
            ?>
        </div>
    </div>
</form>

<script type="text/javascript" src="동작 구현 파일 경로"></script>

6. 동작 구현 파일 작성

아이디 중복 유무를 확인해서 AJAX로 중복 유무를 받아오고 사용 가능한 아이디라면 중복 확인이 완료된 상태로 변경한다.

주소 검색 버튼을 클릭하면 카카오 주소 검색창을 띄운 후 검색한 주소가 칸에 입력되게 구현하고, 폼에서 전송되는 모든 값에 대한 빈값 검사를 한다.

$("#id_check_btn").click(function(){
	var id = $("#member_id").val();
	if(id.length > 0){
    if(id.length < 2 || id.length > 12){
      alert("아이디는 2자 이상 12자 이하로 입력해 주세요.");
      return false;
    }

		$.ajax({
			url: "아이디 중복 확인 처리 파일 경로", 
			type: "POST",
			data: "id=" + id,
		  success: function(data){
        data = data.trim();
        if(data == 0){
          alert("사용가능한 아이디입니다.");
          $("#member_id").attr("state", "Y");
        }else{
          alert("이미 사용중인 아이디입니다.");
          $("#member_id").attr("state", "N");
        }
      }
		});
	}else{
		alert("아이디를 입력해 주세요.");
    return false;
	}
});

$("#postBtn").click(function(){
  new daum.Postcode({
    oncomplete: function(data){
      $("#member_address").val(data.roadAddress);
      $("#member_detail_address").focus();
    }
  }).open();
});

$("#member_save").click(function(){

  if($("#member_id").val() == ""){
    alert("아이디를 입력해 주세요.");
    return false;
  }

  if($("#member_id").attr("state") != "Y"){
    alert("아이디 중복 확인을 해주세요.");
    return false;
  }

  if($("#member_password").val() == ""){
    alert("비밀번호를 입력해 주세요.");
    return false;
  }

  if($("#member_first_name").val() == ""){
    alert("이름을 입력해 주세요.");
    return false;
  }

  if($("#member_last_name").val() == ""){
    alert("성을 입력해 주세요.");
    return false;
  }

  var now = new Date();
  var year= now.getFullYear();

  if($("#member_birthY").val() == "" || $("#member_birthY").val() > year || $("#member_birthY").val() < 1900){
    alert("생년월일을 정확히 입력해 주세요.");
    return false;
  }

  if($("#member_birthM option:selected").val() == ""){
    alert("생년월일을 정확히 입력해 주세요.");
    return false;
  }

  if($("#member_birthD").val() == "" || $("#member_birthD").val() > 31){
    alert("생년월일을 정확히 입력해 주세요.");
    return false;
  }

  if($("#member_phone").val() == ""){
    alert("전화번호를 입력해 주세요.");
    return false;
  }
  
  $("#member_register_frm").submit();
});

$("#member_find").click(function(){

  if($("#member_id_find").val() == ""){
    alert("아이디를 입력해 주세요.");
    return false;
  }

  if($("#member_phone_find").val() == ""){
    alert("전화번호를 입력해 주세요.");
    return false;
  }
  
  $("#member_find_frm").submit();
});

$("#member_reset_pwd").click(function(){

  if($("#member_password_find").val() == ""){
    alert("비밀번호를 입력해 주세요.");
    return false;
  }
  
  $("#member_find_frm").submit();
});

7. 아이디 중복 확인 처리 파일 작성

회원이 입력한 아이디가 DB에 이미 존재하는지 확인해서 아이디 중복 유무를 확인한다.

<?php
    global $mysqli;
    $sql = "select no from member where member_id = '".$_POST["id"]."'";
    $result = $mysqli->query($sql);
    echo $result->num_rows;
?>

8. 회원가입 및 비밀번호 찾기 처리 파일 작성

회원가입 시 값이 모두 잘 들어왔는지 확인 후 비밀번호는 PHP 내장 함수 password_hash()를 사용해서 암호화하고 DB에 데이터를 저장한다. 회원가입이 완료되었다면 로그인 페이지로 이동한다.

비밀번호 찾기 페이지에서 본인인증 시 아이디와 전화번호 값을 확인하고 일치한다면 비밀번호 재설정 페이지로 이동한다. 이동 후 회원이 비밀번호를 재설정하면 입력한 비밀번호를 다시 암호화해서 비밀번호 데이터를 업데이트한다.

<?php
    global $mysqli;
    if(!empty($_POST["member_id"]) && !empty($_POST["member_password"]) && !empty($_POST["member_first_name"]) && !empty($_POST["member_last_name"]) && !empty($_POST["member_birthY"]) && !empty($_POST["member_birthM"]) && !empty($_POST["member_birthD"]) && !empty($_POST["member_phone"])){   
        $_POST["member_password"] = password_hash($_POST["member_password"], PASSWORD_DEFAULT);
        if((int)$_POST["member_birthD"] < 10){
            $_POST["member_birthD"] = "0".$_POST["member_birthD"];
        }

        $column = "";
        $values = "";

        foreach($_POST as $key => $value){
            $column .= $key.", ";
            $values .= "'".$value."', ";
        }

        $sql = "insert into member (".$column." member_joindate) values (".$values." now())";
        $result = $mysqli->query($sql);
        if($result){
            echo "<script>alert('회원가입에 성공하였습니다.');location.replace('로그인 페이지 경로');</script>";
        }else{
            echo "<script>alert('문제가 발생했습니다. 관리자에게 문의해 주세요.');</script>";
        }
    }

    if(!empty($_POST["member_id_find"]) && !empty($_POST["member_phone_find"])){
        $sql = "select no from member where member_id = '".$_POST["member_id_find"]."' and member_phone = '".$_POST["member_phone_find"]."'";
        $result = $mysqli->query($sql);
        if($result){
            $row = $result->fetch_array();
            echo "<script>alert('본인확인에 성공하였습니다.');location.replace('비밀번호 찾기 페이지 경로?find_status=confirmed&member_no=".$row["no"]."');</script>";
        }else{
            echo "<script>alert('아이디 또는 전화번호를 다시 확인해 주세요.');</script>";
        }
    }

    if(!empty($_POST["member_password_find"]) && !empty($_POST["member_no"])){
        $_POST["member_password_find"] = password_hash($_POST["member_password_find"], PASSWORD_DEFAULT);
        $sql = "update member set member_password = '".$_POST["member_password_find"]."' where no = '".$_POST["member_no"]."'";
        $result = $mysqli->query($sql);
        if($result){
            echo "<script>alert('비밀번호 재설정에 성공하였습니다.');location.replace('로그인 페이지 경로');</script>";
        }else{
            echo "<script>alert('문제가 발생했습니다. 관리자에게 문의해 주세요.');</script>";
        }
    }
?>

회원가입과 비밀번호 찾기가 잘 동작하는지 확인한다.

9. 로그인 처리 파일 작성

로그인 시 DB에 저장된 아이디와 암호화된 비밀번호가 입력한 정보와 같으면 커뮤니티 메인으로 이동한다. 아이디가 존재하지 않거나 비밀번호가 틀렸을 경우 예외 처리한다. 로그인이 완료되었다면 SESSION에 아이디와 이름을 저장한다.

<?php
	try{
		if(isset($_POST["id"]) && isset($_POST["password"])){
			global $mysqli;
			$sql = "select * from member where member_id = '".$_POST["id"]."'";
			if(!$result = $mysqli->query($sql)){throw new Exception("Not permitted to log in/connect.");}
			if($result->num_rows == 0){
				throw new Exception("등록되지 않은 아이디입니다.");
			}else if($result->num_rows == 1){
				$row = $result->fetch_assoc();
				if(password_verify($_POST["password"], $row["member_password"])){
					session_start(); 
					$_SESSION["id"] = $row["member_id"];
					$_SESSION["name"] = $row["member_last_name"].$row["member_first_name"];

					echo "<script>location.replace('로그인 후 이동 경로');</script>";
					exit;
				}else{
					throw new Exception("아이디 또는 비밀번호가 맞지 않습니다.");
				}
			}else{
				exit;
			}
		}
	}catch(Exception $e){
		echo "<script>alert('".$e->getMessage()."');history.back();</script>";
	}
?>

10. 로그아웃 처리 및 로그인 확인 파일 작성

로그인이 잘 되었는지 확인하기 위해 로그인 후 이동하는 페이지에 저장했던 SESSION 값을 출력한다. 이 페이지는 후에 게시판 메인 페이지로 쓰일 예정이다. 여기에 로그아웃 버튼도 간단하게 작성한다.

<?php 
    echo $_SESSION['id']."[".$_SESSION['name']."]";
?>
<form method="post" action="로그아웃 처리 파일 경로" id="logoutfrm">
    <button class="btn btn-light">로그아웃 <i class="bi bi-box-arrow-right"></i></button>
</form>
<?php
    session_destroy();
    echo "<script>alert('로그인 화면으로 돌아갑니다.');location.replace('로그인 페이지 경로');</script>";
    if($mysqli){$mysqli->close();}
?>

로그인 후 페이지 이동, SESSION 값 출력, 로그아웃 모두 잘 되는지 확인한다.


위로 스크롤