728x90

ajax(asynchronous Javascript and XML) 회원가입 처리를 하면서 에러가 발생하면 디버깅을 해야 한다.

ajax를 이용해서 클라이언트와 서버가 사용자에게 영향을 미치지 않고 백그라운드에서 정보를 주고 받을 수 있다.

입력변수로 넘어가는 부분에서 에러가 발생하는지 여부는 alert 창으로 확인하면 된다.


echo '{"result":"0"}'; // echo json_encode(array('result' => '0')); 과 동일

echo json_encode(array('result' => '1')); // 회원 등록 성공

echo json_encode(array('result' => '-1')); // 회원 등록 실패

echo json_encode(array('result' => '-2')); // 입력받은 데이터에 문제가 있는 경우


처리된 결과를 return 받은 곳에서 에러가 발생하는지 파악할 때

dataType 이 json 으로 되어 있으면 alert(response); 를 해도 Object.object 라서 에러 메시지가 무엇인지 내용을 알 수 없다.

이럴 경우에는 dataType 을 text 로 변경한 다음에 alert(response); 를 해보면 경고메시지 등이 반환된 것을 알 수 있다.

경고메시지 발생부분을 모두 수정하고 나서, 다시 dataType 을 json 으로 변경하고, alert(response); 를 주석처리해주면 된다.

간단한 것인데도 이 부분에서 자꾸 순간적인 실수를 해서 그냥 적어둔다.


문제가 발생하는 부분은

1. 넘겨주는 url 경로변수가 잘못된 경우

2. dataType 을 json 으로 했는데 respone == 1 이라고 잘못 처리하는 경우

   response.result == 1로 처리해야 한다.

   만약 넘겨주는 값이 {"status":"true","result":"0"} 라고 한다면

   respone.status == true

   response.result == 0

   와 같이 if 조건문을 설정해야 한다.

3. 넘겨주는 data 값이 잘못되었는지 여부도 2번에서 text 로 받아서 메시지를 확인하면 된다.

    http://link2me.tistory.com/1130 참조

4. 파일 인코딩 모드가 UTF-8 인지 확인한다.


var loginpath =$("#ajaxPath").attr('data-path');

PHP 변수를 ajax에서 인식 못해서 아래와 같이 html 코드를 추가해서 경로변수를 자동인식하도록 처리했다.

<div id="ajaxPath" data-path="<?php echo $g['path_page'].'process/'; ?>" ></div>


$('#join-submit').click(function(e){
    e.preventDefault();
    // 입력값 체크는 생략

    var loginpath =$("#ajaxPath").attr('data-path');
    $.ajax({
        url: loginpath+'registerChk.php',
        type: 'POST',
        data: {
            userNM:$("#inputName").val(),
            userID:$('#inputEmail').val(),
            password:$('#inputPassword').val(),
            mobileNO:$("#inputMobile").val()
        },
        dataType: "json", // json, text
        success: function (response) {
            //alert(response);
            if(response.result == 1){
                alert('가입 완료');
                location.replace('index.php'); // 화면 갱신
            } else if(response.result == 0){
                alert('이미 가입된 아이디입니다');
            } else if(response.result == -2){
                alert('입력 데이터를 확인하세요');
            } else {
                alert('등록중에 에러가 발생했습니다');
            }
        },
        error: function(jqXHR, textStatus, errorThrown){
            alert("arjax error : " + textStatus + "\n" + errorThrown);
        }
    });       
   
});



ajax 대신에 post 를 사용할 경우에는 text 데이터만 처리가 되더라.

체크박스로 체크한 데이터만 추가하는 것이다.

// 전체 선택, 전체 해제
$("#checkall").change(function() {
    $("input:checkbox").prop('checked', $(this).prop("checked"));
});

$('#Favorite').click(function() {
    var chkdata = new Array();
    $('input:checkbox[name="uid[]"]:checked').each(function() {
        chkdata.push($(this).val()); // 체크한 아이템 배열로 저장
    });
    if (chkdata.length == 0) {// 배열의 길이가 0 이 아니면
        alert('체크한 항목이 없습니다.');
    }

    $.post(datapath+"favoriteChk.php", {
        userID:$("input[name=userID]").val(), chkdata:chkdata
    }, function(msg) {
        if(msg == 1){
            alert('관심 항목을 추가했습니다.');
            $("input:checkbox").prop('checked', false);
        } else if(msg == 0) {
            alert('수정할 데이터가 없습니다.');
            $("input:checkbox").prop('checked', false);
        } else {
            alert('데이터를 확인하세요\n'+msg);
        }
    });
});

<?php
if(!isset($_SESSION)){
    session_start();
}
//echo '<pre>';print_r($_POST);echo '</pre>';

if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){
    @extract($_POST);
    require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
    require_once $g['path_config'].'dbconnect.php';
    require_once $g['path_class'].'adminClass.php';
    $a = new adminClass();

    if(isset($chkdata)){
        $rs = $a->putFavoriteData($userID,$chkdata);
        echo $rs;
    }
} else {
    echo -1;
}
?>



블로그 이미지

Link2Me

,
728x90

Bootstrap 메인 화면을 Layout 할 때 Carousel (이미지 회전 화면) 기능을 쉽게 제공한다.


https://www.w3schools.com/bootstrap/tryit.asp?filename=trybs_carousel&stacked=h 는 하나의 파일로 Carousel 을 제공하는 예제다.

그런데 직접 파일명을 갯수만큼 적어주는 방법이다.


그래서 지정한 폴더의 파일명을 자동으로 읽어서 화면에 보여주도록 코드를 보완했다.


보완 전 코드

<div id="myCarousel" class="carousel slide" data-ride="carousel">
    <!-- Indicators -->
    <ol class="carousel-indicators">
        <li data-target="#myCarousel" data-slide-to="0" class="active"></li>
        <li data-target="#myCarousel" data-slide-to="1"></li>
        <li data-target="#myCarousel" data-slide-to="2"></li>
        <li data-target="#myCarousel" data-slide-to="3"></li>
    </ol>

    <!-- Wrapper for slides -->
    <div class="carousel-inner">
        <div class="item active">
            <img src="<?php echo $g['path_image']?>carousel/01.jpg" style="width:100%;">
        </div>

        <div class="item">
            <img src="<?php echo $g['path_image']?>carousel/02.jpg" style="width:100%;">
        </div>

        <div class="item">
            <img src="<?php echo $g['path_image']?>carousel/03.jpg" style="width:100%;">
        </div>

        <div class="item">
            <img src="<?php echo $g['path_image']?>carousel/04.jpg" style="width:100%;">
        </div>
    </div>

    <!-- Left and right controls -->
    <a class="left carousel-control" href="#myCarousel" data-slide="prev"> <span class="glyphicon glyphicon-chevron-left"></span> <span class="sr-only">Previous</span> </a>
    <a class="right carousel-control" href="#myCarousel" data-slide="next"> <span class="glyphicon glyphicon-chevron-right"></span> <span class="sr-only">Next</span> </a>
</div>


보완 후 코드

<?php
$path = $g['path_image'] . 'carousel';
$R = getFileNames($path); // 오픈하고자 하는 폴더
function getFileNames($directory) {
    $valid_formats = array("jpg", "png", "gif");// 그림 확장자 지정
    $results = array();
    $handler = opendir($directory);
    while ($file = readdir($handler)) {
        if ($file === "." || $file === "..") continue; // file명이 ".", ".." 이면 무시함
        $getExt = pathinfo($file, PATHINFO_EXTENSION); // 파일의 확장자를 구함

        if (!empty($getExt)) {
            if (in_array($getExt, $valid_formats)) {
                $results[] = $file;
            }
        }
    }
    closedir($handler);
    return $results;
}
?>
<div id="myCarousel" class="carousel slide" data-ride="carousel">
    <!-- Indicators -->
    <ol class="carousel-indicators">
        <?php
        for ($i = 0; $i < count($R); $i++) {
            if ($i == 0){
                echo '<li data-target="#myCarousel" data-slide-to="$i" class="active"></li>';
            } else {
                echo '<li data-target="#myCarousel" data-slide-to="$i"></li>';
            }
        }
        ?>
    </ol>

    <!-- Wrapper for slides -->
    <div class="carousel-inner">
        <?php
        foreach($R as $key=>$value){
            if($key == 0){
                echo '<div class="item active">';
            } else {
                echo '<div class="item">';
            }
            echo '<img src="'.$g['path_image'].'carousel/'.$value.'" style="width:100%;">';
            echo '</div>';
        }
        ?>
    </div>

    <!-- Left and right controls -->
    <a class="left carousel-control" href="#myCarousel" data-slide="prev"> <span class="glyphicon glyphicon-chevron-left"></span> <span class="sr-only">Previous</span> </a>
    <a class="right carousel-control" href="#myCarousel" data-slide="next"> <span class="glyphicon glyphicon-chevron-right"></span> <span class="sr-only">Next</span> </a>
</div>


부트스트랩이 아닌 경우에는 bxslider 코드에 이미지 자동 읽어오기를 추가한 걸 이용하면 편하다.

http://link2me.tistory.com/1259 참조


bxslider.zip



블로그 이미지

Link2Me

,
728x90

PHP 에서 JSON 데이터를 파싱하는 방법이다.

JSON 데이터는 Local File 을 읽어오는 것과 Web 사이트에서 해당 URL 을 읽어오는 방법이 있다.

가장 먼저 파싱해야 할 데이터 형태 파악을 하는 코드부터 살펴보고자 구글링을 했더니 관련 코드가 있어서 주석을 좀 더 추가하고 이해를 돕는 걸 첨가하여 적어둔다.


 <?php
// Web JSON 파일 읽어오기
$url = 'http://ip주소/getFileList.php';
$json_string = file_get_contents($url);

// Local JSON 파일 읽어오기
//$json_string = file_get_contents('weather.json');
// 다차원 배열 반복처리
$R = new RecursiveIteratorIterator(
    new RecursiveArrayIterator(json_decode($json_string, TRUE)),
    RecursiveIteratorIterator::SELF_FIRST);
// $R : array data
// json_decode : JSON 문자열을 PHP 배열로 바꾼다
// json_decode 함수의 두번째 인자를 true 로 설정하면 무조건 array로 변환된다.

foreach ($R as $key => $val) {
    if(is_array($val)) { // val 이 배열이면
        echo "$key:<br/>";
        //echo $key.' (key), value : (array)<br />';
    } else { // 배열이 아니면
        echo "$key => $val <br />";
    }
}
?>


위 코드로 형태파악을 한 다음에 필요한 것을 파싱처리하면 된다.


Local JSON 파일을 읽어서 처리하는 걸 예제로 보자.

 [
    {
        "firstName": "길동",
        "lastName": "홍",
        "email": "jdhongv@gmail.com",
        "mobile": "010-1234-1111"
    },
    {
        "firstName": "민아",
        "lastName": "김",
        "email": "minakim@gmail.com",
        "mobile": "010-1234-3333"
    },
    {
        "firstName": "진주",
        "lastName": "마",
        "email": "jjmah@gmail.com",
        "mobile": "010-1234-5555"
    },
    {
        "firstName": "서영",
        "lastName": "이",
        "email": "sylee@gmail.com",
        "mobile": "010-1234-7777"
    }
]


<?php
// Local JSON 파일 읽어오기
$json_string = file_get_contents('data.json');
$R = json_decode($json_string, true);
// json_decode : JSON 문자열을 PHP 배열로 바꾼다
// json_decode 함수의 두번째 인자를 true 로 설정하면 무조건 array로 변환된다.
// $R : array data


foreach ($R as $row) {
    print $row['lastName'];
    print $row['firstName'];
    print ' , ';
    print $row['email'];
    print ' , ';
    print $row['mobile'];
    print '<br />';
}
?> 


결과

홍길동 , jdhongv@gmail.com , 010-1234-1111
김민아 , minakim@gmail.com , 010-1234-3333
마진주 , jjmah@gmail.com , 010-1234-5555
이서영 , sylee@gmail.com , 010-1234-7777


조금 더 복잡한 JSON 파일을 검색한 걸 테스트한다.

{
    "name": "홍길동",
    "alias": "LInk",
    "members": [
        "소원",
        "예린",
        "은하",
        "유주",
        "신비",
        "엄지"
    ],
    "albums": {
        "EP 1집": "Season of Glass",
        "EP 2집": "Flower Bud",
        "EP 3집": "Snowflake",
        "EP 4집": "THE AWAKENING"
    }
}


파싱하는 코드를 두가지로 테스트해보면 print_r 에서 결과를 다르게 보여준다.

<?php
// JSON 파일 읽어오기
$json_string = file_get_contents('weather.json');
// 다차원 배열 반복처리
$R = new RecursiveIteratorIterator(
    new RecursiveArrayIterator(json_decode($json_string, TRUE)),
    RecursiveIteratorIterator::SELF_FIRST);
// $R : array data
// json_decode : JSON 문자열을 PHP 배열로 바꾼다
// json_decode 함수의 두번째 인자를 true 로 설정하면 무조건 array로 변환된다.

print_r($R);
echo '<br />';

foreach ($R as $key => $val) {
    if(is_array($val)) {
        echo "$key:<br/>";
    } else {
        echo "$key => $val<br/>";
    }
}
?>

<?php
// Local JSON 파일 읽어오기
$json_string = file_get_contents('weather.json');
$R = json_decode($json_string, true); //
// $R : array data
// json_decode : JSON 문자열을 PHP 배열로 바꾼다
// json_decode 함수의 두번째 인자를 true 로 설정하면 무조건 array로 변환된다.

print_r($R); // 배열 요소를 출력해준다.
echo '<br />';
?>

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>PHP JSON parser sample</title>
    </head>

    <body>
        <h3 id="gname">
        <?php
            echo $R['name'];
            if (array_key_exists('alias', $R))
                printf(" (%s)", $R['alias']);
        ?>
        </h3>
        <p>멤버 구성: <span id="members">
            <?php echo implode(', ', $R['members']);?></span>
        </p>
        <h3>앨범 목록</h3>
        <ul id="albums">
        <?php
            foreach ($R['albums'] as $key => $value) {
                printf("<li>%s: %s</li>\n", $key, $value);
            }
            ?>
        </ul>
    </body>
</html>


테스트에 사용한 파일 첨부


JSONwithPHP.zip


블로그 이미지

Link2Me

,
728x90

Android Studio 에서 회원가입 폼을 만들고, 회원가입 처리하는 로직을 구현했다.

 

 

1. Layout 구성(activity_register.xml)

더보기

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/bg_register"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="10dp" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        android:paddingLeft="20dp"
        android:paddingRight="20dp">

        <EditText
            android:id="@+id/name"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/input_register_bg"
            android:hint="@string/hint_name"
            android:inputType="textCapWords"
            android:padding="10dp"
            android:textColor="@color/input_register"
            android:textColorHint="@color/input_register_hint" />

        <EditText
            android:id="@+id/email"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/input_register_bg"
            android:hint="@string/hint_email"
            android:inputType="textEmailAddress"
            android:padding="10dp"
            android:textColor="@color/input_register"
            android:textColorHint="@color/input_register_hint" />

        <EditText
            android:id="@+id/password"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/input_register_bg"
            android:hint="@string/hint_password"
            android:inputType="textPassword"
            android:padding="10dp"
            android:textColor="@color/input_register"
            android:textColorHint="@color/input_register_hint" />

        <EditText
            android:id="@+id/mobileNO"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/input_register_bg"
            android:hint="@string/hint_mobileNO"
            android:inputType="number"
            android:padding="10dp"
            android:textColor="@color/input_register"
            android:textColorHint="@color/input_register_hint" />

        <EditText
            android:id="@+id/telNO"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/input_register_bg"
            android:hint="@string/hint_telNO"
            android:inputType="number"
            android:padding="10dp"
            android:textColor="@color/input_register"
            android:textColorHint="@color/input_register_hint" />
        <!-- Login Button -->

        <Button
            android:id="@+id/btnRegister"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dip"
            android:background="#ea4c88"
            android:text="@string/btn_register"
            android:textColor="@color/white" />

        <!-- Link to Login Screen -->

        <Button
            android:id="@+id/btnLoginScreen"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="40dip"
            android:background="@null"
            android:text="@string/btn_link_to_login"
            android:textAllCaps="false"
            android:textColor="@color/white"
            android:textSize="15dp" />
    </LinearLayout>

</LinearLayout>

 

2. Register.java

- OKHttp 라이브러리를 사용하여 코드를 작성했는데 에러가 발생해서 포기하고

  HttpUrlConnection 을 사용하여 코드를 구현했다. 100% 문제없이 동작하는 코드가 최고다.

package com.tistory.link2me.pdologin;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.tistory.link2me.common.PHPComm;

import okhttp3.MediaType;

public class Register extends AppCompatActivity {
    private static final String TAG = Register.class.getSimpleName();
    private Button btnRegister;
    private Button btnLogin;
    private EditText inputUserName;
    private EditText inputEmail;
    private EditText inputPassword;
    private EditText inputMobileNO;
    private EditText inputTelNO;
    private String userID; // SharedPreferences 저장 목적으로 Class 전역변수 선언

    public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
    public SharedPreferences settings;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);

        inputUserName = (EditText) findViewById(R.id.name);
        inputEmail = (EditText) findViewById(R.id.email);
        inputPassword = (EditText) findViewById(R.id.password);
        inputMobileNO = (EditText) findViewById(R.id.mobileNO);
        inputTelNO = (EditText) findViewById(R.id.telNO);
        btnRegister = (Button) findViewById(R.id.btnRegister);
        btnLogin = (Button) findViewById(R.id.btnLoginScreen);

        // 등록 버튼
        btnRegister.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                String name = inputUserName.getText().toString().trim();
                String email = inputEmail.getText().toString().trim();
                userID = email;
                String password = inputPassword.getText().toString().trim();
                String mobileNO = inputMobileNO.getText().toString().trim();
                String telNO = inputTelNO.getText().toString().trim();

                if (!name.isEmpty() && !email.isEmpty() && !password.isEmpty() && !mobileNO.isEmpty()) {
                    registerUser(name, email, password, mobileNO, telNO);
                } else {
                    Toast.makeText(getApplicationContext(), "필수사항은 모두 입력하세요!", Toast.LENGTH_LONG).show();
                }
            }
        });

        // Login Screen 으로 Activity 이동
        btnLogin.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                startActivity(new Intent(getApplicationContext(), Login.class));
                finish();
            }
        });

    }

    private void registerUser(String name, String userID, String password, String mobileNO, String telNO) {
        // 전달할 인자들
        Uri.Builder builder = new Uri.Builder()
                .appendQueryParameter("name", name)
                .appendQueryParameter("userID", userID)
                .appendQueryParameter("email", userID)
                .appendQueryParameter("password", password)
                .appendQueryParameter("mobileNO", mobileNO)
                .appendQueryParameter("telNO", telNO);
        String urlParameters = builder.build().getEncodedQuery();
        new getJSONData().execute(Value.IPADDRESS + "/register.php", urlParameters);
    }

    private class getJSONData extends AsyncTask<String, Void, String> {
        ProgressDialog pdLoading = new ProgressDialog(Register.this);

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            pdLoading.setMessage("\t회원가입 처리중...");
            pdLoading.setCancelable(false);
            pdLoading.show();
        }

        @Override
        protected String doInBackground(String... params) {
            try {
                return PHPComm.getJson(params[0],params[1]);
            } catch (Exception e) {
                return new String("Exception: " + e.getMessage());
            }
        }

        protected void onPostExecute(String result){
            pdLoading.dismiss();
            showJSONResult(result);
        }
    }

    protected void showJSONResult(String result) {
        if(result.equalsIgnoreCase("1")){
            Toast.makeText(this, "회원가입 완료", Toast.LENGTH_SHORT).show();
            storeUserData();
            startActivity(new Intent(getApplicationContext(), Login.class));
            finish();
        } else if(result.equalsIgnoreCase("-1")){
            Toast.makeText(this, "아이디가 이미 가입되어 있습니다", Toast.LENGTH_SHORT).show();
            inputEmail.clearFocus();
        } else {
            Toast.makeText(this, "회원 등록에 문제가 발생했습니다", Toast.LENGTH_SHORT).show();
        }
    }

    private void storeUserData(){
        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();
        editor.putString("userID", userID);
        editor.putBoolean("autologin", true);
        editor.commit();
    }
}

 

3. PHP 코드 작성(register.php)

<?php
extract($_POST);
if(isset($_POST) && !empty($_POST)) {
    require_once '../config/config.php';
    $c = new MemberClass();

    // 동일한 userID 등록되어 있는지 체크
    if ($c->isUserExisted($userID)) { // E-Mail 이 key value
        // user already existed
        echo -1;
    } else {
        // 사용자 등록
        $telNO = preg_replace("/[^0-9]/", "", $telNO);
        $mobileNO = preg_replace("/[^0-9]/", "", $mobileNO);
        $user = $c->storeUser($userID, $name, $email, $password, $telNO, $mobileNO);
        if ($user) { // 사용자 등록 성공
            echo 1;
        } else {
            echo 0;
        }
    }
} else { // 입력받은 데이터에 문제가 있을 경우
    echo -2;
}
?>
 

 

values.zip
다운로드

 

strings.xml 과 colors.xml 파일 두개가 포함되어 있다.

 

회원가입 테이블에 데이터가 저장되어 있는 걸 확인할 수 있다.

 

 

회원가입과 로그인처리는 완벽하게 처리되도록 구현한 소스코드를 그대로 오픈 것이니 복사해서 이용하면 된다.

AndroidManifest.xml 파일에 Register Activity 추가된 것을 추가해줘야 에러가 발생하지 않는다.소소한 에러를 잡아가면서 해보는게 좋을거 같아서 관련으로 수정된 부분은 첨부하지 않는다.

 

도움이 되셨다면 댓글 달아주세요.

블로그 이미지

Link2Me

,
728x90

Android Studio 3.0 에서 PHP PDO(PHP Data Object) 기반 함수와 연동하여 회원가입 및 로그인처리하는 예제를 구현하고 입문자를 위해서 상세한 설명과 더불어 일부 코드를 첨부한다.

 

1. Android Studio New Module 추가하는 방법

더보기

아래 그림 순서대로 진행하면 된다.

 

 

 

 

 

앱 build.gradle 을 수정할 준비를 한다.

 

 

2. 앱 build.gradle 수정사항

더보기

 apply plugin: 'com.android.application'

android {
    compileSdkVersion 23

    defaultConfig {
        applicationId "com.tistory.link2me.pdologin"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:23.4.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    compile "com.android.support:cardview-v7:24.2.0"
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리
    compile 'com.google.code.gson:gson:2.8.2'   
}

 

처음에 세팅된 값은 compileSdkVersion 26, targetSdkVersion 26 으로 되어 있는걸 23으로 낮추면 에러메시지가 몇개 나온다.

이때 수정해줘야 할 사항은

AndroidManifest.xml 파일에서 roundicon 나오는 행을 삭제한다.

그리고 res 폴더에서 아래 그림과 같은 부분만 남기도 나머지 폴더는 과감하게 삭제해버린다.

 

위와 같이 하면 코드를 작성할 준비는 완료된 것이라고 봐도 된다.

compileSdkVersion 26, targetSdkVersion 26 그대로 두고 코드를 작성해도 된다.

추가하는 라이브러리를 하나도 사용하지 않고 코드를 작성해보려고 한다.

 

컴파일을 하다보니 에러가 발생하면서 컴파일이 안된다.

Error:Execution failed for task ':pdologin:transformDexArchiveWithExternalLibsDexMergerForDebug'.
> java.lang.RuntimeException: com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex

그래서 implementation 하는 라이브러리 버전이 일치되지 않아서 생기는 문제인거 같아서 수정했더니 정상적으로 컴파일이 된다.

compileSdkVersion 24, targetSdkVersion 24 로 버전 일치 시킴

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:24.2.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    implementation "com.android.support:cardview-v7:24.2.0"
    implementation 'com.squareup.okhttp3:okhttp:3.9.0'
    implementation 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리
    implementation 'com.google.code.gson:gson:2.8.2'
} 

 

 

3. 로그인 과정 설계

- 아이디, 패스워드를 입력하고 자동로그인을 체크하면 로그인이 처리되고 MainActivity 로 넘어간다.

- 자동로그인 체크를 해두면 아이디, 패스워드를 묻지 않고 자동으로 로그인되어 MainActivity 로 넘어간다.

 

<여기서 고려할 사항>

자동로그인 체크를 하면 로그인창 화면이 표시되지 않아야 한다.

즉, 앱을 실행하자마자 바로 MainActivity 로 넘어가도록 처리하려면 별도의 Activity가 하나 필요하다는 의미다.

 

 

4. Activtiy 추가과정

더보기

Intro Activity 추가

 

위 그림에서 3번은 생략하면 된다.

테스트삼아 선택을 해봤는데 필요하지 않은 기능이다.

 

Login Activity 추가

위 그림과 같은 과정으로 추가를 한다.

 

 

 

이제 AndroidManifest.xml 에서 실행순서를 로그인 처리과정 순서를 고려하여 변경해야 한다.

 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tistory.link2me.pdologin">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

 

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".Intro">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".Login"></activity>
        <activity android:name=".MainActivity"></activity>
    </application>

</manifest>

 

5. Layout 구성

Layout 파일은 https://www.androidhive.info/2012/01/android-login-and-registration-with-php-mysql-and-sqlite 에서 받은 소스파일을 활용하여 필요한 사항을 수정하여 작성할 것이다.

activity_login.xml 파일 내용을 수정하고 나니 미리보기 화면이 android...actionbaroverlaylayout 라고 표시되며 보이지가 않는다.

검색해 보니 res/values/styles.xml 내용중에서 parent 부분을 아래 것으로 교체하면 정상적으로 보인다.

<style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar">
</style>

 

activity_login.xml

더보기

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/bg_login"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="10dp" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        android:paddingLeft="20dp"
        android:paddingRight="20dp" >

        <EditText
            android:id="@+id/login_id_edit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/white"
            android:hint="@string/hint_email"
            android:inputType="textEmailAddress"
            android:padding="10dp"
            android:singleLine="true"
            android:textColor="@color/input_login"
            android:textColorHint="@color/input_login_hint" />

        <EditText
            android:id="@+id/login_pw_edit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:background="@color/white"
            android:hint="@string/hint_password"
            android:inputType="textPassword"
            android:padding="10dp"
            android:singleLine="true"
            android:textColor="@color/input_login"
            android:textColorHint="@color/input_login_hint" />

        <Button
            android:id="@+id/login_btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dip"
            android:background="@color/btn_login_bg"
            android:text="@string/btn_login"
            android:textColor="@color/btn_login" />

        <CheckBox
            android:id="@+id/autologin"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="5dip"
            android:text="자동 로그인"
            android:textColor="#000000"
            android:textSize="14sp"
            android:textStyle="bold" />

        <!-- Link to Login Screen -->
        <Button
            android:id="@+id/btnRegisterScreen"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dip"
            android:background="@null"
            android:text="@string/btn_link_to_register"
            android:textAllCaps="false"
            android:textColor="@color/white"
            android:textSize="15dp" />
    </LinearLayout>

</LinearLayout>

 

6. Java 코드 작성

다른 코드는 생략하고 Login.java 코드만 적는다.

더보기

package com.tistory.link2me.pdologin;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import com.tistory.link2me.common.BackPressHandler;
import com.tistory.link2me.common.PHPComm;

import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.List;

public class Login extends AppCompatActivity {
    String getDeviceID; // 스마트기기의 장치 고유값
    EditText etId;
    EditText etPw;

    String userID;
    String userPW;
    CheckBox autologin;
    Boolean loginChecked;
    String idx;
    public SharedPreferences settings;
    private BackPressHandler backPressHandler;

    Context mContext;

    // 멀티 퍼미션 지정
    private String[] permissions = {
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.CALL_PHONE, // 전화걸기 및 관리
            Manifest.permission.WRITE_CONTACTS, // 주소록 액세스 권한
            Manifest.permission.WRITE_EXTERNAL_STORAGE // 기기, 사진, 미디어, 파일 엑세스 권한
    };
    private static final int MULTIPLE_PERMISSIONS = 101;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mContext = this.getBaseContext();
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        if (Build.VERSION.SDK_INT >= 23) { // 안드로이드 6.0 이상일 경우 퍼미션 체크
            checkPermissions();
        }

        // 네트워크 연결상태 체크
        if(NetworkConnection() == false){
            NotConnected_showAlert();
        }

        etId = (EditText) findViewById(R.id.login_id_edit);
        etPw = (EditText) findViewById(R.id.login_pw_edit);
        autologin = (CheckBox) findViewById(R.id.autologin);

        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        loginChecked = settings.getBoolean("LoginChecked", false);
        if (loginChecked) {
            etId.setText(settings.getString("userID", ""));
            etPw.setText(settings.getString("userPW", ""));
            autologin.setChecked(true);
        }

        if(!settings.getString("userID", "").equals("")) etPw.requestFocus();

        Button submit = (Button) findViewById(R.id.login_btn);
        submit.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(View view) {
                userID = etId.getText().toString().trim();
                userPW = etPw.getText().toString().trim();

                if(userID != null && !userID.isEmpty() && userPW != null && !userPW.isEmpty()){
                    login(userID, userPW);
                }
            }
        });

    }

    private void login(String loginID, String loginPW){
        // 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
        TelephonyManager mTelephony = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {

        }
        if (mTelephony.getDeviceId() != null) {
            getDeviceID = mTelephony.getDeviceId();  // 스마트폰 기기 정보
        } else {
            getDeviceID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
        }

        // 전달할 인자들
        Uri.Builder builder = new Uri.Builder()
                .appendQueryParameter("userID", loginID)
                .appendQueryParameter("userPW", loginPW)
                .appendQueryParameter("deviceID", getDeviceID);
        String urlParameters = builder.build().getEncodedQuery();
        new getJSONData().execute(Value.IPADDRESS + "/loginChk.php", urlParameters);
    }

    private class getJSONData extends AsyncTask<String, Void, String> {
        ProgressDialog pdLoading = new ProgressDialog(Login.this);
        HttpURLConnection conn;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //this method will be running on UI thread
            pdLoading.setMessage("\tValidating user...");
            pdLoading.setCancelable(false);
            pdLoading.show();
        }

        @Override
        protected String doInBackground(String... params) {
            try {
                return PHPComm.getJson(params[0],params[1]);
            } catch (Exception e) {
                return new String("Exception: " + e.getMessage());
            }
        }

        protected void onPostExecute(String result){
            pdLoading.dismiss();
            showJSONResult(result);
        }
    }

    protected void showJSONResult(String result) {
        if(Integer.parseInt(result) > 0){ // 로그인 정보 일치하면 idx 값 받음
            idx = result;
            AutoLoginChk(); // 정보 저장
            Toast.makeText(Login.this,"로그인 성공", Toast.LENGTH_SHORT).show();
            startActivity(new Intent(getApplication(), MainActivity.class));
            finish(); // 현재 Activity 를 없애줌
        } else if(result.equalsIgnoreCase("-1")){ // 등록된 단말기와 불일치
            deviceDismatch_showAlert();
        } else if (result.equalsIgnoreCase("0")) {
            showAlert();
        } else {
            Toast.makeText(Login.this, "서버로부터 정보가 잘못 전송되었습니다", Toast.LENGTH_SHORT).show();
        }
    }

    private boolean checkPermissions() {
        int result;
        List<String> permissionList = new ArrayList<>();
        for (String pm : permissions) {
            result = ContextCompat.checkSelfPermission(this, pm);
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionList.add(pm);
            }
        }
        if (!permissionList.isEmpty()) {
            ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), MULTIPLE_PERMISSIONS);
            return false;
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case MULTIPLE_PERMISSIONS: {
                if (grantResults.length > 0) {
                    for (int i = 0; i < permissions.length; i++) {
                        if (permissions[i].equals(this.permissions[i])) {
                            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                                showToast_PermissionDeny();
                            }
                        }
                    }
                } else {
                    showToast_PermissionDeny();
                }
                return;
            }
        }

    }

    private void showToast_PermissionDeny() {
        Toast.makeText(this, "권한 요청에 동의 해주셔야 이용 가능합니다. 설정에서 권한 허용 하시기 바랍니다.", Toast.LENGTH_SHORT).show();
        finish();
    }

    public void onStop(){
        // 어플리케이션이 화면에서 사라질때
        super.onStop();
        //AutoLoginChk();
    }

    private void AutoLoginChk(){
        // 자동 로그인이 체크되어 있고, 로그인에 성공했으면 폰에 자동로그인 정보 저장
        if (autologin.isChecked()) {
            settings = getSharedPreferences("settings",Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();

            editor.putString("userID", userID);
            editor.putString("userPW", userPW);
            editor.putBoolean("LoginChecked", true);
            editor.putString("idx", idx);

            editor.commit();
        } else {
            // 자동 로그인 체크가 해제되면 폰에 저장된 정보 모두 삭제
            settings = getSharedPreferences("settings",    Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();
            editor.clear(); // 모든 정보 삭제
            editor.commit();
        }
    }

    public void deviceDismatch_showAlert(){
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("등록단말 불일치");
        builder.setMessage("최초 등록된 단말기가 아닙니다.\n" + "관리자에게 문의하여 단말기 변경신청을 하시기 바랍니다.")
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    public void showAlert(){
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("로그인 에러");
        builder.setMessage("로그인 정보가 일치하지 않습니다.")
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    private void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("네트워크 연결 오류");
        builder.setMessage("사용 가능한 무선네트워크가 없습니다.\n" + "먼저 무선네트워크 연결상태를 확인해 주세요.")
                .setCancelable(false)
                .setPositiveButton("확인", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        finish(); // exit
                        //application 프로세스를 강제 종료
                        android.os.Process.killProcess(android.os.Process.myPid() );
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();

    }

    private boolean NetworkConnection() {
        int[] networkTypes = ;
        try {
            ConnectivityManager manager = (ConnectivityManager) getSystemService (Context.CONNECTIVITY_SERVICE);
            for (int networkType : networkTypes) {
                NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
                if(activeNetwork != null && activeNetwork.getType() == networkType){
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

}

 

Intro.java 코드는 Login.java 코드를 활용하면 얼마든지 만들 수 있다.

코드의 핵심로직은 다음과 같다.

// 자동 로그인 체크 검사
settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
loginChecked = settings.getBoolean("LoginChecked", false);
if (loginChecked) {
    userID = settings.getString("userID", "");
    userPW = settings.getString("userPW", "");
    if (userID != null && !userID.isEmpty() && userPW != null && !userPW.isEmpty()) {
        login(userID, userPW);
    } else {
        startActivity(new Intent(getApplication(), Login.class));
        finish();
    }
} else {
    startActivity(new Intent(getApplication(), Login.class));
    finish();
}

 

protected void showJSONResult(String result) {
    if(Integer.parseInt(result) > 0){ // 로그인 정보 일치하면 idx 값 받음
        SharedPreferences.Editor editor = settings.edit();
        editor.putString("idx", result);
        Toast.makeText(Intro.this,"로그인 성공", Toast.LENGTH_SHORT).show();
        startActivity(new Intent(getApplication(), MainActivity.class));
        finish(); // 현재 Activity 를 없애줌
    } else {
        startActivity(new Intent(getApplication(), Login.class));
        finish();
    }
}

 

7. 서버 PHP 코드 작성

loginChk.php

<?php
extract($_POST);

if (isset($_POST['userID']) && isset($_POST['userPW'])) {
    require_once '../config/config.php';
    $c = new MemberClass();

    // get the user by userID and password
    $rs = $c->LoginUserChk($userID,$userPW,$deviceID);
    $user = $c->getUser($userID, $userPW);

    if ($user != false && $rs > 0) {
        // use is found
        echo $user['idx'];
    } else if ($user != false && $rs == -1) {
        // 등록된 폰이 아닙니다. 관리자에게 문의하세요!
        echo -1;
    } else {
        // user is not found with the credentials
        echo 0;
    }
} else {
    echo -2;
}
?>

 

서버 코드 값을 좀 더 세분화하면 등록된 userID가 없는 경우도 안드로이드폰에 반환할 수 있고, 패스워드가 일치하지 않는 경우를 반환할 수도 있다.

이러한 것은 개발자의 몫이다.

 

서버 PHP 와 안드로이드간에 통신 개념부분은 http://link2me.tistory.com/1110 를 참조하면 도움된다.

Bootstrap 기반 로그인 폼 및 로그인 처리(PHP PDO Class 연동) : http://link2me.tistory.com/1402 참조

[부트스트랩] 로그인 폼 및 로그인 처리 (PHP PDO Class 연동)

출처: http://link2me.tistory.com/1402 [소소한 일상 및 업무TIP 다루기]
[부트스트랩] 로그인 폼 및 로그인 처리 (PHP PDO Class 연동)

출처: http://link2me.tistory.com/1402 [소소한 일상 및 업무TIP 다루기]

 

 

 

 

다음에 시간이 되면 안드로이드 회원가입 처리부분을 해볼 생각이다.

 

회원가입이 필요한 분은 https://www.androidhive.info/2012/01/android-login-and-registration-with-php-mysql-and-sqlite 에 명시된 코드를 활용하여 만들어도 될 것이다.

이 코드는 서버에 저장된 DB 데이터를 안드로이드폰 SQLite DB에 저장하는 로직이 추가되어 있으므로 그대로 구현해도 되고, 서버에만 저장하고 저장된 결과를 간단하게 받아서 정상 가입여부만 알 수 있도록 register.php 코드를 수정해도 된다.

Bootstrap 기반 회원가입 폼 및 회원가입 처리(http://link2me.tistory.com/1403) 코드를 참조해서 xml 파일을 수정하고 작성하면 될 것이다.

서버와의 통신에 사용된 HttpURLConnection 부분은 http://link2me.tistory.com/1349 게시글과 첨부파일을 참조하면 된다.

Asynchronous Http Client 를 사용한 서버와의 통신은 http://link2me.tistory.com/1493 게시글을 참조하면 도움된다.

 

첨부파일은 코드 작성에 도움되도록 일부만 발췌하여 첨부한다.

android_Login.zip
다운로드

 

블로그 이미지

Link2Me

,
728x90

부트스트랩 로그인 폼은 https://bootsnipp.com/tags/login 에 많은 예제가 있다.

Preview, HTML, CSS 코드가 있으므로 원하는 걸 선택하거나 수정해서 사용하면 된다.

 

 

input-group 클래스를 사용해서 아이콘 모양이 나오도록 하려면

<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">

한줄을 header에 추가해준다.

 

<div class="form-group">
    <label for="password">비밀번호</label>
    <input type="password" id="password" value='' placeholder="비밀번호를 입력하세요" class="form-control" />
</div>

 <div class="input-group">
    <span class="input-group-addon"><i class="fa fa-lock"></i></span>
    <input type="password" id="password" value='' placeholder="비밀번호를 입력하세요" class="form-control" />
</div>

 

style.css 파일 추가사항

.input-group {
    margin-top: 1em;
    margin-bottom: 1em;
}

 

.login-box {
    line-height: 2.3em;
    font-size: 1em;
    color: #aaa;
    margin-top: 1em;
    margin-bottom: 1em;
    padding-top: 0.5em;
    padding-bottom: 0.5em;
}

 

로그인 폼 전체 코드

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>로그인</title>
        <!-- Bootstrap -->
        <link href="../plugin/bootstrap/css/bootstrap.min.css" rel="stylesheet">
        <!-- font awesome -->
        <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
        <!-- Custom style -->
        <link rel="stylesheet" href="../plugin/bootstrap/css/style.css" media="screen" title="no title" charset="utf-8">

        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
        <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
        <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
        <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <!-- Include all compiled plugins (below), or include individual files as needed -->
        <script src="../plugin/bootstrap/js/bootstrap.min.js"></script>
        <script src="../config/js/login.js"></script>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-sm-3">

                    <div class="login-box well">
                        <form accept-charset="UTF-8" role="form" method="post" action="">
                            <legend>로그인</legend>
                            <div class="input-group">
                                <span class="input-group-addon"><i class="fa fa-user"></i></span>
                                <input type="text" id="userid" value='' placeholder="E-mail을 입력하세요" class="form-control" />
                            </div>
                            <div class="input-group">
                                <span class="input-group-addon"><i class="fa fa-lock"></i></span>
                                <input type="password" id="password" value='' placeholder="비밀번호를 입력하세요" class="form-control" />
                            </div>
                            <button type="submit" id="login-submit" class="btn btn-default btn-block"/>로그인</button> <span class='text-center'><a href="" class="text-sm">비밀번호 찾기</a></span>
                            <div class="form-group">
                                <a href="registerForm.php" class="btn btn-default btn-block"> 회원가입</a>
                            </div>
                        </form>
                    </div>

                </div>
            </div>
        </div>
    </body>
</html>

 

login.js

$(function() {
    // 로그인 처리
    $('#login-submit').click(function(e) {
        e.preventDefault();
        if ($("#userid").val() == '') {
            alert('아이디를 입력하세요');
            $("#userid").focus();
            return false;
        }

        if ($("#password").val() == '') {
            alert('비밀번호를 입력하세요');
            $("#password").focus();
            return false;
        }

        $.ajax({
            url : 'loginChk.php',
            type : 'POST',
            data : {
                userid : $('#userid').val(),
                password : $('#password').val()
            },
            dataType : "json",
            success : function(response) {
                if (response.result == 1) {
                    //alert('로그인 성공');
                    location.replace('../index.php'); // 화면 갱신
                    //location.reload(); // 화면 갱신
                } else if (response.result == -2) {
                    alert('입력된 값이 없습니다');
                } else {
                    alert('로그인 실패');
                }
            },
            error : function(jqXHR, textStatus, errorThrown) {
                alert("arjax error : " + textStatus + "\n" + errorThrown);
            }
        });
    });
    
});

 

loginChk.php

<?php
extract($_POST);
if (isset($_POST)) {
    require_once '../config/config.php';
    $c = new MemberClass();

    $user = $c->getUser($userid, $password);

    if ($user != false) {
        if (!isset($_SESSION)) {
            session_start();
        }
        $_SESSION['userID'] = $user['userID'];
        $_SESSION['admin'] = $user['admin'];
        echo json_encode(array('result' => '1'));
    } else {
        echo json_encode(array('result' => '0'));
    }
} else {// 입력받은 데이터에 문제가 있을 경우
    echo json_encode(array('result' => '-2'));
}
?>

 

로그인 처리에 필요한 파일은 모두 위에 열거하였다.

Class 함수에 대한 코드는 http://link2me.tistory.com/1401 에 있다.

테이블 구조는 부트스트랩 회원가입 처리하면서 칼럼 몇개를 더 추가했다.

 

PDO Class 만든 것과 연계하여 부트스트랩 회원가입과 로그인처리하는 기능을 만들고 있다.

로그인 처리하는 부분을 약간 더 손보고 관련 파일을 첨부한다.

비밀번호 찾기, 회원정보 수정 등은 첨부된 파일에 더 추가해서 직접 작성하면 될 것이다.

pdo.zip
다운로드

 

첨부파일은 기존에 올려진 첨부파일과 내용이 모두 다르다고 보면 된다.

 

다음에는 안드로이드 회원가입 처리와 로그인처리 부분을 작성해서 올릴 예정이다.

 

 

 

블로그 이미지

Link2Me

,
728x90

부트스트랩 회원가입 폼 예제다.

PDO(PHP Data Object) Class 와 연계하여 회원가입 처리를 하는 입력 폼, jQuery 코드다.



registerForm.php

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>회원가입</title>
        <!-- Bootstrap -->
        <link href="../plugin/bootstrap/css/bootstrap.min.css" rel="stylesheet">
        <!-- font awesome -->
        <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
        <!-- Custom style -->
        <link rel="stylesheet" href="../plugin/bootstrap/css/style.css" media="screen" title="no title" charset="utf-8">

        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
        <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
        <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
        <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <!-- Include all compiled plugins (below), or include individual files as needed -->
        <script src="../plugin/bootstrap/js/bootstrap.min.js"></script>
        <script src="../config/js/join.js"></script>
    </head>
    <body>
        <article class="container">
            <div class="page-header">
                <div class="col-md-6 col-md-offset-3">
                <h3>회원가입 Form</h3>
                </div>
            </div>
            <div class="col-sm-6 col-md-offset-3">
                <form role="form">
                    <div class="form-group">
                        <label for="inputName">성명</label>
                        <input type="text" class="form-control" id="inputName" placeholder="이름을 입력해 주세요">
                    </div>
                    <div class="form-group">
                        <label for="InputEmail">이메일 주소</label>
                        <input type="email" class="form-control" id="InputEmail" placeholder="이메일 주소를 입력해주세요">
                    </div>
                    <div class="form-group">
                        <label for="inputPassword">비밀번호</label>
                        <input type="password" class="form-control" id="inputPassword" placeholder="비밀번호를 입력해주세요">
                    </div>
                    <div class="form-group">
                        <label for="inputPasswordCheck">비밀번호 확인</label>
                        <input type="password" class="form-control" id="inputPasswordCheck" placeholder="비밀번호 확인을 위해 다시한번 입력 해 주세요">
                    </div>
                    <div class="form-group">
                        <label for="inputMobile">휴대폰 번호</label>
                        <input type="tel" class="form-control" id="inputMobile" placeholder="휴대폰번호를 입력해 주세요">
                    </div>
                    <div class="form-group">
                        <label for="inputtelNO">사무실 번호</label>
                        <input type="tel" class="form-control" id="inputtelNO" placeholder="사무실번호를 입력해 주세요">
                    </div>

                    <div class="form-group">
                    <label>약관 동의</label>
                    <div data-toggle="buttons">
                    <label class="btn btn-primary active">
                    <span class="fa fa-check"></span>
                    <input id="agree" type="checkbox" autocomplete="off" checked>
                    </label>
                    <a href="#">이용약관</a>에 동의합니다.
                    </div>
                    </div>

                    <div class="form-group text-center">
                        <button type="submit" id="join-submit" class="btn btn-primary">
                            회원가입<i class="fa fa-check spaceLeft"></i>
                        </button>
                        <button type="submit" class="btn btn-warning">
                            가입취소<i class="fa fa-times spaceLeft"></i>
                        </button>
                    </div>
                </form>
            </div>

        </article>
    </body>
</html>


join.js

$(function(){
    // 회원 가입 처리
    $('#join-submit').click(function(e){
        e.preventDefault();
        if($("#inputName").val() ==''){
            alert('이름을 입력하세요');
            $("#inputName").focus();
            return false;
        }

        var email = $('#InputEmail').val();
        if(email == ''){
            alert('이메일을 입력하세요');
            $("#InputEmail").focus();
            return false;
        } else {
            var emailRegex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
            if (!emailRegex.test(email)) {
                alert('이메일 주소가 유효하지 않습니다. ex)abc@gmail.com');
                $("#inputEmail").focus();
                return false;
            }
        }

        if($("#inputPassword").val() ==''){
            alert('비밀번호를 입력하세요');
            $("#inputPassword").focus();
            return false;
        }

        if($("#inputPasswordCheck").val() ==''){
            alert('비밀번호를 다시 한번 더 입력하세요');
            $("#inputPasswordCheck").focus();
            return false;
        }
        
        if($("#inputPassword").val()!== $("#inputPasswordCheck").val()){
            alert('비밀번호를 둘다 동일하게 입력하세요');
            return false;
        }

        if($("#inputMobile").val() ==''){
            alert('휴대폰 번호를 입력하세요');
            $("#inputMobile").focus();
            return false;
        }
        
        if($("#agree").is(":checked") == false){
            alert('약관에 동의하셔야 합니다');
            return false;      
        }
        
        $.ajax({
            url: 'register.php',
            type: 'POST',
            data: {
                name:$("#inputName").val(),
                userID:$('#InputEmail').val(),
                email:$('#InputEmail').val(),
                password:$('#inputPassword').val(),
                telNO:$("#inputtelNO").val(),
                mobileNO:$("#inputMobile").val()
            },
            dataType: "json",
            success: function (response) {
                if(response.result == 1){
                    alert('가입 완료');
                    location.replace('../index.php'); // 화면 갱신
                } else if(response.result == 0){
                    alert('이미 가입된 아이디입니다');
                } else if(response.result == -2){
                    alert('입력된 값이 없습니다');
                } else {
                    alert('등록중에 에러가 발생했습니다');
                }
            },
            error: function(jqXHR, textStatus, errorThrown){
                alert("arjax error : " + textStatus + "\n" + errorThrown);
            }
        });        
        
    });

});


register.php

<?php
extract($_POST);
if (isset($_POST)) {
    require_once '../config/config.php';
    $c = new MemberClass();
    // 동일한 userID(email) 등록되어 있는지 체크
    if ($c->isUserExisted($userID)) {
        echo '{"result":"0"}'; // echo json_encode(array('result' => '0')); 과 동일
    } else {
        // 회원 등록
        $user = $c->storeUser($userID, $name, $email, $password, $telNO, $mobileNO);
        if ($user) {// 사용자 등록 성공
            if (!isset($_SESSION)) {
                session_start();
            }
            $_SESSION['userID'] = $user['userID'];
            $_SESSION['admin'] = $user['admin'];
            echo json_encode(array('result' => '1'));
        } else {
            // 회원 등록 실패
            echo json_encode(array('result' => '-1'));
        }
    }
} else {// 입력받은 데이터에 문제가 있을 경우
    echo json_encode(array('result' => '-2'));
}
?>


코드는 100% 동작되는 코드다. 모두 테스트를 하고 올린 코드들이다.

PDO Class 는 http://link2me.tistory.com/1401 에 올려져 있다.


안드로이드 회원가입 처리 코드 예제는 https://link2me.tistory.com/1405 를 참조하시라.

블로그 이미지

Link2Me

,
728x90

대체 MVC가 뭐지?

MVC(Model-View-Controller)는 소프트웨어 공학에서 사용하는 아키텍쳐 패턴이다.

PHP 코드를 작성할 때 Data 와 Design, Controller 를 분리시켜 만드는 것이다.

개발하는데 복잡하고 어려워도 확장성이 좋고 수정/유지보수하기가 편하다.

 

Framework(프레임워크)는 Web 애플리케이션을 개발할 때 많이 사용하는 기능이나 일상적인 구조를 정리한 것이다. 대부분의 프레임워크는 MVC의 구조를 채용한다.

 

뭐 대충 이렇게 정의하고 있다.

- Model : application 의 정보(데이터) → 데이터의 생성, 접근, 수정, 삭제하는 부분

             데이터의 값과 상태 및 각종 기능을 처리하는 Business Logic 부분과 사용되는 데이터 부분으로 구성

             Controller 에서 건네준 요청을 처리하고 다시 돌려준다.

             모델은 데이터의 가공이나 저장, 비즈니스 로직 등의 프로그램 처리를 담당한다.

- View : User Interface (HTML, CSS 로 만든 정적인 페이지 부분)

            화면 UI 부분이며 사용자게게 응답 처리 결과를 보여준다.

            뷰는 필요에 따라 모델의 데이터 내용을 참조하고 사용자에게 화면을 보여준다.

- Controller : Data 와 Business Logic 사이의 상호동작을 관리한다.

                   Model 과 View 사이에서 request 와 response를 관리하며 동작한다.

                   컨트롤러는 사용자가 조작하면 모델이나 뷰에 필요한 지시를 보낸다.

 

 

PHP MVC Frmaework 의 대표적인 것이 Laravel(라라벨), CodeIgniter(코드이그나이터) 라는데 아직 사용해본 적은 없다.

그누보드, 제로보드, KIMSQRB 는 프레임웍이라기 보다는 CMS툴에 가깝다고 봐야 한다.

 

프레임웍을 만들정도까지 실력을 키우려면 한참 배워야 한다.

이제 그런 구조(패턴)을 고려해서 PHP 코드 작성을 해보고 있는 중이다.

 

게시글 http://link2me.tistory.com/1398 , http://link2me.tistory.com/1399 에 첨부된 파일과 비교해서 수정된 부분이 어떤 것인지 살펴보면 도움될 것이다.

본 코드는 테스트용도이므로 Class를 더 세분화해서 분리하고 Class 이름을 정해서 추가해나가면 된다.

 

아래 코드와 login.php, register.php 파일이 포함된 압축파일을 첨부한다.

pdo.zip
다운로드

 

 

 

 

 

<?php
//****************
// DB 정보 입력
//****************
define('_DBHOST','localhost');
define('_DBUSER','android'); // DB 사용자
define('_DBPASS', '!android@'); // DB 패스워드
define('_DBNAME','android'); // DB 명
define('_DBTYPE','mysql'); // 데이터베이스 종류
define('_DSN',_DBTYPE.':host='._DBHOST.';dbname='._DBNAME.';charset=utf8');

//*****************************
// 파일 디렉토리
//*****************************
define('_CONFIG_DIR',__DIR__);
define('_CLASS_DIR',_CONFIG_DIR.'/Class/');

//***************************
//클래스 파일 불러오기
//***************************
require_once _CLASS_DIR.'DBConClass.php'; // DB 연결 클래스
require_once _CLASS_DIR.'MemberClass.php';

?>

<?php
class DBConClass{
    protected $db; // 변수를 선언한 클래스와 상속받은 클래스에서 참조할 수 있다.

    public function __construct(){
        $this->dbConnect();

        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.

    }

    private function dbConnect() {
        try{
            // MySQL PDO 객체 생성
            $this->db = new PDO(_DSN, _DBUSER, _DBPASS);
            $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
        } catch(PDOException $ex){
            die("오류 : ".$ex->getMessage());
        }
    }
}
?>

<?php
class MemberClass extends DBConClass {
    // 회원 정보 신규 입력
    public function storeUser($userID, $userNM, $email, $password, $telNO, $mobileNO) {
        $hash = $this->hashSSHA($password);
        $encrypted_password = $hash['encrypted']; // encrypted password
        $salt = $hash['salt']; // salt

        try{
            $this->db->beginTransaction();
            $sql = "INSERT INTO members(userID, userNM, email, passwd, salt, telNO,mobileNO,created_at) VALUES(:userID,:userNM,:email,:passwd,:salt,:telNO,:mobileNO,:created_at)";
            $stmt = $this->db->prepare($sql);
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $stmt->bindValue(':userNM',$userNM,PDO::PARAM_STR);
            $stmt->bindValue(':email',$email,PDO::PARAM_STR);
            $stmt->bindValue(':passwd',$encrypted_password,PDO::PARAM_STR);
            $stmt->bindValue(':salt',$salt,PDO::PARAM_STR);
            $stmt->bindValue(':telNO',$telNO,PDO::PARAM_STR);
            $stmt->bindValue(':mobileNO',$mobileNO,PDO::PARAM_STR);
            $stmt->bindValue(':created_at',date("YmdHis"));
            $result = $stmt->execute();
            $this->db->commit();
        } catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
        // check for successful store
        if ($result) {
            $stmt = $this->db->prepare("SELECT * FROM members WHERE userID = :userID");
            $stmt->bindValue(':userID', $userID, PDO::PARAM_STR);
            $stmt->execute();
            $user = $stmt->fetch(PDO::FETCH_ASSOC);

            return $user;
        } else {
            return false;
        }
    }

    // 로그인 체크
    public function getUser($userID, $password) {
        $sql = "SELECT * FROM members WHERE userID=:userID";
        $stmt = $this->db->prepare($sql);
        $stmt->bindValue(':userID', $userID);
        $stmt->execute();

        if ($user=$stmt->fetch()) {
            // verifying user password
            $salt = $user['salt'];
            $encrypted_password = $user['passwd'];
            $hash = $this->checkhashSSHA($salt, $password);
            if ($encrypted_password == $hash) {
                // user authentication details are correct
                return $user;
            }
        } else {
            return NULL;
        }
    }

    // 안드로이드/아이폰 로그인 체크
    public function LoginUserChk($userID,$password,$deviceID){
        if(empty($userID) || empty($password)){
            return 0;
        } else {
            $user = $this->getUser($userID, $password);
            if($user['idx']>0){
                // 장치 일련번호 체크
                if($user['phoneSE'] == NULL){
                    // 신규 장비번호 입력(최초 로그인)
                    $this->LoginUserEquipInput($userID,$deviceID);
                    return $user['idx'];
                } else {
                    if($user['phoneSE'] === $deviceID){
                        return 1; // 일련번호 일치
                    } else {
                        return -1; //일련번호 불일치
                    }
                }
            } else {
                return 0; //계정오류
            }
        }

    }

    // 장치번호 업데이트
    public function LoginUserEquipInput($userID,$deviceID){
        if(strlen($deviceID)>0 && is_numeric($deviceID)){ // 안드로이드폰
            $ostype = 2;
        } else if(strlen($deviceID)>30){ // 아이폰
            $ostype = 1;
        } else { // 기타
            $ostype = 0;
        }

        try{
            $this->db->beginTransaction();
            $sql='update members set phoneSE=:phoneSE, OStype=:OStype where userID=:userID';
            $stmt = $this->db->prepare($sql);
            $stmt->bindValue(':phoneSE',$deviceID,PDO::PARAM_STR);
            $stmt->bindValue(':OStype',$ostype,PDO::PARAM_INT);
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $status = $stmt->execute();
            $this->db->commit();
            if($status == true){
                return 1;
            } else {
                return 0;
            }
        }catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
    }//end

    // 장치번호 초기화
    public function EquipReset($userID){
        try{
            $this->db->beginTransaction();
            $ostype = 0;
            $sql='update members set phoneSE=NULL,OStype=:OStype where userID=:userID';
            $stmt = $this->db->prepare($sql);
            $stmt->bindValue(':OStype',$ostype,PDO::PARAM_INT);
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $status = $stmt->execute();
            $this->db->commit();
            if($status == true){
                return 1;
            } else {
                return 0;
            }
        } catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
    }//end

    // 회원 가입 여부 체크
    public function isUserExisted($userID) {
        $stmt = $this->db->prepare("SELECT userID from members WHERE userID=:userID");
        $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
        $stmt->execute();
        if ($stmt->rowCount() > 0) {
            return true;
        } else {
            return false;
        }
    }

    // 회원 정보 삭제
    public function deleteUser($userID){
        try{
            $this->db->beginTransaction();
            $stmt = $this->db->prepare("delete FROM members WHERE userID=:userID");
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $stmt->execute();
            $this->db->commit();
        }catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
    }

    public function hashSSHA($password) {
        $salt = sha1(rand());
        $salt = substr($salt, 0, 10);
        $encrypted = base64_encode(sha1($password . $salt, true) . $salt);
        $hash = array("salt" => $salt, "encrypted" => $encrypted);
        return $hash;
    }

    public function checkhashSSHA($salt, $password) {
        $hash = base64_encode(sha1($password . $salt, true) . $salt);
        return $hash;
    }
}
?>

 

블로그 이미지

Link2Me

,
728x90

윈도우 기반 AutoSet9 에서 테스트를 하다보니 계속 driver 를 못찾는다는 에러 메시지를 출력한다.

원인은 PDO(PHP Data Object) 설정이 안되어 있어서다.

 

PHP에는 다양한 데이터베이스를 쉽게 이용할 수 있도록 PDO라는 확장모듈이 포함되어 있다.

PDO(PHP Data Object) 동작이 되도록 설정하는 방법은 간단하다.

 

1. conf/php.ini 파일을 열어서 아래와 같이 주석을 제거한다.

- php_pdo_mysql.dll 만 주석제거 해도 되는데 PostgreSQL 도 사용하게 될지 몰라서 같이 주석 제거를 했다.

 

 

 

2. php.ini 수정사항을 반영하기 위해서는 Apache Web서버를 재기동해야 한다.

 

 

웹서버 멈춤을 눌러서 웹서버를 종료시켰다가 웹서버 시작을 눌러서 재실행되도록 한다.

 

 

3. phpinfo(); 로 설정된 상태가 반영되었는지 확인한다.

 

이렇게 해주면 PDO 방식으로 코딩 테스트를 할 수 있다.

 

AutoSet10 (PHP 7.0 + MariaDB) 도 동일하게 설정하면 된다.

블로그 이미지

Link2Me

,
728x90

<?php
// 절차지향 방식
$sql = "SELECT * FROM table WHERE field1 = $field1 AND field2 = '$field2' ORDER BY id";
$stmt = mysqli_query($db,$sql);

// PDO (정석대로)
$sql = "SELECT * FROM table WHERE field1 = ? AND field2 = ? ORDER BY id";
$stmt = $db->prepare($sql);
$stmt->bindParam(1, $field1, PDO::PARAM_INT);
$stmt->bindParam(2, $field2, PDO::PARAM_STR);
$stmt->execute();

// PDO (그나마 좀 간소화시킨 문법)
$sql = "SELECT * FROM table WHERE field1 = ? AND field2 = ? ORDER BY id";
$stmt = $db->prepare($sql);
$stmt->execute(array($field1, $field2));
?>

 

PDO(PHP Data Object) 방식 함수를 사용하여 만든 MemberClass 예제다.

<?php
class MemberClass {
    // 회원 정보 신규 입력
    public function storeUser($userID$userNM$email$password$telNO$mobileNO) {
        global $db;
 
        $hash = $this->hashSSHA($password);
        $encrypted_password = $hash['encrypted']; // encrypted password
        $salt = $hash['salt']; // salt
 
        try{
            $db->beginTransaction();
            $sql = "INSERT INTO members(userID, userNM, email, passwd, salt, telNO,mobileNO,created_at) VALUES(:userID,:userNM,:email,:passwd,:salt,:telNO,:mobileNO,:created_at)";
            $stmt = $db->prepare($sql);
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $stmt->bindValue(':userNM',$userNM,PDO::PARAM_STR);
            $stmt->bindValue(':email',$email,PDO::PARAM_STR);
            $stmt->bindValue(':passwd',$encrypted_password,PDO::PARAM_STR);
            $stmt->bindValue(':salt',$salt,PDO::PARAM_STR);
            $stmt->bindValue(':telNO',$telNO,PDO::PARAM_STR);
            $stmt->bindValue(':mobileNO',$mobileNO,PDO::PARAM_STR);
            $stmt->bindValue(':created_at',date("YmdHis"));
            $result = $stmt->execute();
            $db->commit();
        } catch (PDOException $pex) {
            $db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
        // check for successful store
        if ($result) {
            $stmt = $db->prepare("SELECT * FROM members WHERE userID = :userID");
            $stmt->bindValue(':userID'$userID, PDO::PARAM_STR);
            $stmt->execute();
            $user = $stmt->fetch(PDO::FETCH_ASSOC);
 
            return $user;
        } else {
            return false;
        }
    }
 
    // 로그인 체크
    public function getUser($userID$password) {
        global $db;
        $sql = "SELECT * FROM members WHERE userID=:userID";
        $stmt = $db->prepare($sql);
        $stmt->bindValue(':userID'$userID);
        $stmt->execute();
 
        if ($user=$stmt->fetch()) {
            // verifying user password
            $salt = $user['salt'];
            $encrypted_password = $user['passwd'];
            $hash = $this->checkhashSSHA($salt$password);
            if ($encrypted_password == $hash) {
                // user authentication details are correct
                return $user;
            }
        } else {
            return NULL;
        }
    }
 
    // 안드로이드/아이폰 로그인 체크
    public function LoginUserChk($userID,$password,$deviceID){
        global $db;
        if(empty($userID|| empty($password)){
            return 0;
        } else {
            $user = $this->getUser($userID$password);
            if($user['idx']>0){
                // 장치 일련번호 체크
                if($user['phoneSE'== NULL){
                    // 신규 장치번호 입력(최초 로그인)
                    $this->LoginUserEquipInput($userID,$deviceID);
                    return $user['idx'];
                } else {
                    if($user['phoneSE'=== $deviceID){
                        return 1// 일련번호 일치
                    } else {
                        return -1//일련번호 불일치
                    }
                }
            } else {
                return 0//계정오류
            }
        }
 
    }
 
    // 장치번호 업데이트
    public function LoginUserEquipInput($userID,$deviceID){
        global $db;
        if(strlen($deviceID)>0 && is_numeric($deviceID)){ // 안드로이드폰
            $ostype = 2;
        } else if(strlen($deviceID)>30){ // 아이폰
            $ostype = 1;
        } else { // 기타
            $ostype = 0;
        }
 
        try{
            $db->beginTransaction();
            $sql='update members set phoneSE=:phoneSE, OStype=:OStype where userID=:userID';
            $stmt = $db->prepare($sql);
            $stmt->bindValue(':phoneSE',$deviceID,PDO::PARAM_STR);
            $stmt->bindValue(':OStype',$ostype,PDO::PARAM_INT);
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $status = $stmt->execute();
            $db->commit();
            if($status == true){
                return 1;
            } else {
                return 0;
            }
        }catch (PDOException $pex) {
            $db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
    }//end
 
    // 장치번호 초기화
    public function EquipReset($userID){
        global $db;
        try{
            $db->beginTransaction();
            $ostype = 0;
            $sql='update members set phoneSE=NULL,OStype=:OStype where userID=:userID';
            $stmt = $db->prepare($sql);
            $stmt->bindValue(':OStype',$ostype,PDO::PARAM_INT);
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $status = $stmt->execute();
            $db->commit();
            if($status == true){
                return 1;
            } else {
                return 0;
            }
        } catch (PDOException $pex) {
            $db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
    }//end
 
    // 회원 가입 여부 체크
    public function isUserExisted($userID) {
        global $db;
        $stmt = $db->prepare("SELECT userID from members WHERE userID=:userID");
        $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
        $stmt->execute();
        if ($stmt->rowCount() > 0) {
            return true;
        } else {
            return false;
        }
    }
 
    // 회원 정보 삭제
    public function deleteUser($userID){
        global $db;
        try{
            $db->beginTransaction();
            $stmt = $db->prepare("delete FROM members WHERE userID=:userID");
            $stmt->bindValue(':userID',$userID,PDO::PARAM_STR);
            $stmt->execute();
            $db->commit();
        }catch (PDOException $pex) {
            $db->rollBack();
            echo "에러 : ".$pex->getMessage();
        }
    }
 
    public function hashSSHA($password) {
        $salt = sha1(rand());
        $salt = substr($salt010);
        $encrypted = base64_encode(sha1($password . $salttrue) . $salt);
        $hash = array("salt" => $salt"encrypted" => $encrypted);
        return $hash;
    }
 
    public function checkhashSSHA($salt$password) {
        $hash = base64_encode(sha1($password . $salttrue) . $salt);
        return $hash;
    }
}
?>

 

 

안드로이드와 연동하는 login.php, register.php 파일 예제는 첨부파일을 참조하면 된다.

pdo.zip
다운로드

 

조만간에 부트스트랩 registerForm.php 와 loginForm.php 파일을 간단하게 만들고, 관련 처리 기능을 만들 예정이다.

 

 

PHP 7.0 에서 mysql 기존방식은 지원을 아예 안하고 mysqli  및 pdo를 지원하므로 PDO 방식으로 코딩하는 방법을 계속 연습해두고 기능을 하나 하나 익혀두는게 나을거 같아서 정리를 해보는 중이다.

 

수정사항(2017.11.30)

회원정보 삭제 테스트를 하다보니 원하는 결과가 나오지 않아서 로직을 수정 보완했다.

// 회원 가입 여부 체크
public function isUserExisted($userID) {
    $stmt = $this -> db -> prepare("SELECT count(userID) from members WHERE userID=:userID");
    $stmt -> bindValue(':userID', $userID, PDO::PARAM_STR);
    $stmt -> execute();
    if ($row = $stmt -> fetch()) {
        return $row[0];
        // 미가입이면 0 반환, 가입이면 1 반환
    } else {
        return -1;
    }
}

// 회원 정보 삭제 : Move
public function deleteUser($userID) {
    if ($this -> isUserExisted($userID) > 0) {
        try {
            $this -> db -> beginTransaction();
            $stmt = $this -> db -> prepare("delete FROM members WHERE userID=:userID");
            $stmt -> bindValue(':userID', $userID, PDO::PARAM_STR);
            $stmt -> execute(); // 삭제할 데이터의 존재 유무에 상관없이 실행한다.
            $this -> db -> commit();
        } catch (PDOException $pex) {
            $this -> db -> rollBack();
            echo "에러 : " . $pex -> getMessage();
        }
        return 1; // 삭제 성공
    } else {
        return 0; // 삭제할 데이터가 없음
    }
}

 

블로그 이미지

Link2Me

,
728x90

PDO(PHP Data Object) 방식으로 DB을 연결하는 방법이다.

- PDO는 여러가지 DBMS를 제어하는 방법을 표준화시켜 다양한 DB(Oracle, PostgrSQL, MySQL 등)를 동일한 방법으로 제어할 수 있다.

- PHP 7.0 에서는 MySQL API 지원 중단, MySQLi 절차식 API 및 객체 API 제공, PDO 객체 API 제공


사용법

require_once 'DBController.php';
require_once 'loginClass.php';
$c=new LoginClass;

LoginClass 만 객체 생성해주면 부모클래스인 DBController 클래스는 자동으로 인식된다.


<?php
//****************
// DB 정보 입력
// define('상수명', '값', true);
//****************
define('_DBHOST','localhost');
define('_DBUSER','address_root'); // DB 사용자
define('_DBPASS', 'autoset'); // DB 패스워드
define('_DBNAME','address'); // DB 명
define('_DBTYPE','mysql'); // 데이터베이스 종류
define('_DSN',_DBTYPE.':host='._DBHOST.';dbname='._DBNAME.';charset=utf8');
?>

 <?php
class DBController {
    protected $db;

    public function __construct() {
        $this->db = $this -> dbConnect();
        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.
    }

    private function dbConnect() {
        require_once 'config.php';
        try { // MySQL PDO 객체 생성
            $conn = new PDO(_DSN, _DBUSER, _DBPASS);
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
        } catch(PDOException $ex) {
            die("오류 : " . $ex -> getMessage());
        }
        return $conn;
    }
?>

 <?php
class LoginClass extends DBController {
    // 회원 정보 신규 입력
    public function storeUser($userID, $userNM, $password, $mobileNO) {
        $hash = $this -> hashSSHA($password);
        $encrypted_password = $hash['encrypted']; // encrypted password
        $salt = $hash['salt']; // salt
        $regdate = date("YmdHis");

        $key = "userID, userNM, passwd, salt, regdate";
        $val = "$userID,$userNM,$encrypted_password,$salt,$regdate";
        $result = $this -> getDbInsert('members', $key, $val); //
DBController에서 별도 생성 함수
        // check for successful store
        if ($result) {
            $user = $this -> getUser($userID, $password);
            $rs = $this -> storeUserDetail($user['idx'], $userNM, $userID, $mobileNO);
            if ($rs == 1) {
                return $user;
            } else {
                return -1;
            }
        } else {
            return -1; // 0
        }
    }

    // 회원 세부 정보 입력
    public function storeUserDetail($relatedidx, $userNM, $email, $mobileNO) {
        $regdate = date("YmdHis");
        $key = 'relatedidx,name,email,mobileNO,modifydate'; // 칼럼 정확한지 체크
        $val = "$relatedidx,$userNM,$email,$mobileNO,$regdate";
        $this -> getDbInsert('member_data', $key, $val);
    }

    // 회원정보 반환
    public function getUser($userID, $password) {
        $sql = "SELECT * FROM members WHERE userID=:userID";
        $stmt = $this -> db -> prepare($sql);
        $stmt -> bindValue(':userID', $userID);
        $stmt -> execute();

        if ($user = $stmt -> fetch()) {
            // verifying user password
            $salt = $user['salt'];
            $encrypted_password = $user['passwd'];
            $hash = $this -> checkhashSSHA($salt, $password);
            if ($encrypted_password == $hash) {
                // user authentication details are correct
                return $user;
            }
        } else {
            return NULL;
        }
    }

    // 회원 가입 여부 체크
    public function isUserExisted($userID) {
        $stmt = $this -> db -> prepare("SELECT count(userID) from members WHERE userID=:userID");
        $stmt -> bindValue(':userID', $userID, PDO::PARAM_STR);
        $stmt -> execute();
        if ($row = $stmt -> fetch()) {
            return $row[0];
            // 미가입이면 0 반환, 가입이면 1 반환
        } else {
            return -1;
        }
    }

    // 회원정보 수정
    public function updateUser($idx, $userNM, $mobileNO) {
        if(isset($idx) && !empty($idx)){
            $this->getDbUpdate('members', "userNM=?", array($userNM), "idx=".$idx);
            $set = "name=?,mobileNO=?";
            $params = array($userNM,$mobileNO);
            $this->getDbUpdate('member_data', $set, $params, 'relatedidx='.$idx);
            return 1;
        } else {
            return 0;
        }
    }

    // 회원 정보 삭제 : Move
    public function deleteUser($userID) {
        if ($this -> isUserExisted($userID) > 0) {
            try {
                $this -> db -> beginTransaction();
                $stmt = $this -> db -> prepare("delete FROM members WHERE userID=:userID");
                $stmt -> bindValue(':userID', $userID, PDO::PARAM_STR);
                $stmt -> execute(); // 삭제할 데이터의 존재 유무에 상관없이 실행한다.
                $this -> db -> commit();
            } catch (PDOException $pex) {
                $this -> db -> rollBack();
                echo "에러 : " . $pex -> getMessage();
            }
            return 1; // 삭제 성공
        } else {
            return 0; // 삭제할 데이터가 없음
        }
    }

    // 안드로이드/아이폰 로그인 체크
    public function LoginUserChk($userID, $password, $deviceID) {
        if (empty($userID) || empty($password)) {
            return 0;
        } else {
            $user = $this -> getUser($userID, $password);
            if ($user['idx'] > 0) {
                // 장치 일련번호 체크
                if ($user['phoneSE'] == NULL) {
                    // 신규 장비번호 입력(최초 로그인)
                    $this -> LoginUserEquipInput($userID, $deviceID);
                    return $user['idx'];
                } else {
                    if ($user['phoneSE'] === $deviceID) {
                        return 1;
                        // 일련번호 일치
                    } else {
                        return -1;
                        //일련번호 불일치
                    }
                }
            } else {
                return 0;
                //계정오류
            }
        }

    }

    // 장치번호 업데이트
    public function LoginUserEquipInput($userID, $deviceID) {
        if (strlen($deviceID) > 0 && is_numeric($deviceID)) {// 안드로이드폰
            $ostype = 2;
        } else if (strlen($deviceID) > 30) {// 아이폰
            $ostype = 1;
        } else {// 기타
            $ostype = 0;
        }

        try {
            $this -> db -> beginTransaction();
            $sql = 'update members set phoneSE=:phoneSE, OStype=:OStype where userID=:userID';
            $stmt = $this -> db -> prepare($sql);
            $stmt -> bindValue(':phoneSE', $deviceID, PDO::PARAM_STR);
            $stmt -> bindValue(':OStype', $ostype, PDO::PARAM_INT);
            $stmt -> bindValue(':userID', $userID, PDO::PARAM_STR);
            $status = $stmt -> execute();
            if ($status == true) {
                $this -> db -> commit();
                return 1;
            } else {
                return 0;
            }
        } catch (PDOException $pex) {
            $this -> db -> rollBack();
            echo "에러 : " . $pex -> getMessage();
        }
    }//end

}
?>


추가 (2018.04.09)

검색하다보니 https://gist.github.com/kijin/5947026 에 ezPDO 라는 Class 파일을 만든 고수분이 있네.


블로그 이미지

Link2Me

,
728x90

PHP에서 구분자로 문자열 자르기를 하려고 하는데 구분자가 문자열에 여러개 나올 경우가 있다.

구분자를 뒤에서부터 찾는 방법을 알아보려고 찾아보고 적어둔다.


stripos 는 대상 문자열을 앞에서 부터 검색하여 찾고자 하는 문자열이 몇번째 위치에 있는지를 리턴하는 함수
stripos(대상 문자열, 조건 문자열);

strripos 는 대상 문자열을 뒤에서 부터 검색하여 찾고자 하는 문자열이 몇번째 위치에 있는지를 리턴하는 함수
strripos(대상 문자열, 조건 문자열);


$curdir = substr($dir,1,strlen($dir)-1); // 현재 폴더 정보

$filePath = substr($curdir,0,strripos($curdir, "/")); // 상위폴더

$updir = substr($filePath,0,strripos($filePath, "/")); // 차상위폴더


'Web 프로그램 > PHP 문법' 카테고리의 다른 글

HTML Entity List (특수문자 용어)  (0) 2021.11.27
[PHP] 배열에만 동작하는 foreach 문  (0) 2017.01.26
PHP Class 개념 이해 ★★  (0) 2017.01.10
PHP isset 과 empty 설명  (1) 2016.04.27
PHP 대체 문법  (0) 2016.04.05
블로그 이미지

Link2Me

,
728x90

네이버 지도 API 기능을 학습하려고 구글링하다보니 Gson 라이브러리를 사용한 코드가 있어서 이참에 Gson 라이브러리를 테스트하고 정리해둔다.


1. Gson 라이브러리 최신버전 구하는 곳 : https://github.com/google/gson

   에 가면 최신버전의 정보가 나온다.

   단순하게 toJson() and fromJson() 메소드로 Java 객체를 JSON으로 변환하고

   SON 을 Java 객체로 변환하는 라이브러리 를 목표로 한다고 나온다.


2. 안드로이드 스튜디오에서 Gson 라이브러리를 이용하려면

   앱 build.gradle 파일내 dependencies 에 코드 한줄 추가하면 된다.

   compile 'com.google.code.gson:gson:2.8.2'



FileList_Item Class

- 변수명을 서버 JSON 변수명과 동일하게 처리하라.



Gson 라이브러리를 사용하지 않고 서버 데이터를 가져와서 RecyclerView 에 보여주는 코드중에서 비교할 수 있는 것만 발췌했다.


Gson 라이브러리 사용 이전 코드


listViewAdapter = new ListViewAdapter(this,fileList); // Adapter 생성
audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅

코드 부분을 onCreate 에 추가하고 listViewAdapter.notifyDataSetChanged(); 를 하면 JSON Array 데이터가 RecyclerView 에 잘 보인다.


Gson 라이브러리 사용 코드



Gson gson = new Gson();
Type listType = new TypeToken<ArrayList<FileList_Item>>(){}.getType();
fileList = gson.fromJson(audiofiles.toString(), listType);


검색해보면 위 코드 3줄이면 해결된다고 모두 답변이 되어 있다. 그런데도 불구하고 RecyclerView 에 출력되지 않았다.


서버에서 가져온 JSON Array 데이터를 ArrayList 에 추가하는 코드가 매우 간단해진다는 건 알았다.

상위폴더 ArrayList 를 별도로 만들 필요가 없는 경우에는 매우 유용할 거 같다.

Stackoverflow 에 질문한 내용을 검색해보니 RecyclerView 에 데이터 보이지 않는다고 질의한 사항이 몇건 있었다.  System.out.println 으로 찍어보면 데이터가 ArrayList에 정상적으로 추가되어 있다. 그러나 화면에 보이지 않았다.


Android RecyclerView JSON Parsing using Retrofit 게시글을 보니까 ArrayList 데이터를 추가하고 난 후 바로 다음 줄에 아래 2줄을 적어준 걸 보고 시도했더니 폰 화면에 정상적으로 출력된다.

listViewAdapter = new ListViewAdapter(this,fileList); // Adapter 생성
audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅

Gson 라이브러리를 사용하면 JSON Array 데이터를 String으로 하나하나 분리할 필요없이 ArrayList에 바로 추가할 수 있어 코드가 심플해지는거 같다.

단, Class 에서 생성한 변수명과 서버(PHP)에서 json_encode로 생성한 변수명이 동일해야 하는거 같다.

서버에서 생성한 데이터가 JSON Object 가 아니라 JSON Array 면 코드가 더 심플해질 수 있는거 같다.


상위폴더를 안드로이드 Java 코드에서 생성한 것을 없애고, 서버 PHP 코드에서 구현하는 것으로 변경하고 테스트를 해보니 정상적으로 잘 처리된다.


코드 줄어든 것만 발췌하여 비교한다.

=== Gson 라이브러리 미사용 코드 ===

// 서버 정보를 파싱하기 위한 변수 선언
private static final String TAG_RESULTS="result";
private static final String TAG_fileNAME = "fileName";
private static final String TAG_filePATH ="filePath";
private static final String TAG_curPATH ="curPath";
private static final String TAG_isFile ="isFile";
JSONArray audiofiles = null;

protected void showList(String jsonData) {
    try {
        JSONObject jsonObj = new JSONObject(jsonData);
        audiofiles = jsonObj.getJSONArray(TAG_RESULTS);

        fileList.clear(); // 서버에서 가져온 데이터 초기화
        for(int i = 0; i < audiofiles.length(); i++){
            JSONObject c = audiofiles.getJSONObject(i);
            final String fileName = c.getString(TAG_fileNAME);
            final String filePath = c.getString(TAG_filePATH);
            final String curPath = c.getString(TAG_curPATH);
            final String isFile = c.getString(TAG_isFile);

            // 서버에서 가져온 데이터 저장
            FileAllList(fileName, filePath, curPath, isFile);
        }

        runOnUiThread(new Runnable() { // 화면에 반영하기 위하여 runOnUiThread()를 호출하여 실시간 갱신한다.
            @Override
            public void run() {
                // 갱신된 데이터 내역을 어댑터에 알려줌
                listViewAdapter.notifyDataSetChanged();
            }
        });
    } catch (JSONException e) {
        e.printStackTrace();
    }

}

// 파일 리스트 추가를 위한 메소드
public void FileAllList(String fileName, String filePath, String curPath, String isFile){
    FileList_Item item = new FileList_Item();
    item.setFileName(fileName);
    item.setFilePath(filePath);
    item.setCurPath(curPath);
    item.setIsFile(isFile);
    fileList.add(item);
}

=== Gson 라이브러리 사용한 코드 ====

 // 서버 정보를 파싱하기 위한 변수 선언
private static final String TAG_RESULTS="result";
JSONArray audiofiles = null;

protected void showList(String jsonData) {
    JSONObject jsonObj = null;
    try {
        jsonObj = new JSONObject(jsonData);
        audiofiles = jsonObj.getJSONArray(TAG_RESULTS);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    fileList.clear(); // 서버에서 가져온 데이터 초기화
    Gson gson = new Gson();
    Type listType = new TypeToken<ArrayList<FileList_Item>>(){}.getType();
    fileList = gson.fromJson(audiofiles.toString(), listType);
    listViewAdapter = new ListViewAdapter(this,fileList); // Adapter 생성
    audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅
}



정리 핵심

Gson gson = new Gson();
JSONArray jsonArray = new JSONArray(JSONdata);
Type listType = new TypeToken<ArrayList<FileList_Item>>(){}.getType();
ArrayList<FileList_Item> myFileList = gson.fromJson(jsonArray.toString(), listType);


블로그 이미지

Link2Me

,
728x90

PHP Connect to MySQL

- MySQL (procedural) : MySQL DB에서만 동작하며, PHP 7.0 이상에선 동작안됨. PHP 5.5에서 deprecated

- MySQLi (procedural) : MySQL DB에서만 동작한다.

- MySQLi (object-oriented) : MySQL DB에서만 동작한다.
- PDO(PHP Data Objects) :
Oracle, PostgreSQL, SQLite, MySQL 등 12가지의 DB에서 동작한다.


MySQLi 기반 Prepared Statements PHP 연동 방식이 SQL 인젝션에 강하다고 되어 있다.

그러면 절차지향 방식의 MySQLi 방식에서는 어떻게 보강하면 될까?

$userid = mysqli_real_escape_string($db, $userid);

와 같이 mysqli_real_escape_string 를 써주면 된다.

해킹 대비를 위해서는 INPUT 해킹 시도(SQL Injection), 출력 해킹 시도(Cross Site Scripting) 둘다 신경써야 한다.


기존 사용하던 MySQLi (procedural) 대신에 MySQLi (object-oriented) 로 연습을 해봤다.


https://www.androidhive.info/2012/01/android-login-and-registration-with-php-mysql-and-sqlite 에서 파일을 다운로드하여 좀 수정했다.


테스트 환경

- Windows10 기반 Autoset9 (PHP 5.6.0, MySQL 5.6.20) : 정상

- Windows10 기반 Autoset10(Apache 2.4.17, PHP 7.0, MariaDB 10.1.9 ) : 정상


리눅스 기반 PHP 5.6.30, MySQL 5.6.30 에서 테스트를 하다보니 일부 함수가 문제가 발생하는 걸 알았다.

mysqlnd 가 설치되지 않아서였더라.

phpize로 추가 설치하는 방법을 검색해봤는데 아직 해결책을 못찾았다.

정 안되면 APM(Apache + PHP + MySQL) 소스 설치를 다시 해볼 생각이다.


- SHA1 패스워드를 적용한 알고리즘이 좋더라. 그래서 이걸 수정/추가해서 코드를 보완

- 아이폰/안드로이드폰에서 접속시 폰의 고유장치번호를 인식하여 최초 등록 단말이 아닌 경우 로그인 불가처리

- 테이블명을 변경하고 칼럼을 추가하고 user 권한을 설정하는 걸 추가


테이블 구조

- phpMyAdmin SQL에서 아래코드를 붙여넣기 하면 자동으로 DB명과 테이블을 생성한다.

- DB명과 user 권한을 수정해서 이용하면 된다.


DB_Functions 클래스 및 DB 접속정보는 첨부파일 참조

- DB_Functions 클래스가 핵심 내용이다.

 <?php

class DB_Functions {
    private $conn;

    // 생성자
    function __construct() {
        require_once 'dbconnect.php';
        // DB 연결
        $db = new Db_Connect();
        $this->conn = $db->connect();
    }

    // 소멸자(destructor)
    function __destruct() {

    }

    // 회원 정보 신규 입력
    public function storeUser($userID, $name, $email, $password, $telNO, $mobileNO) {
        $hash = $this->hashSSHA($password);
        $encrypted_password = $hash['encrypted']; // encrypted password
        $salt = $hash['salt']; // salt

        $stmt = $this->conn->prepare("INSERT INTO members(userID, userNM, email, passwd, salt, telNO,mobileNO,created_at) VALUES(?, ?, ?, ?, ?, ?, ?, NOW())");
        $stmt->bind_param("sssssss", $userID, $name, $email, $encrypted_password, $salt, $telNO, $mobileNO);
        $result = $stmt->execute();
        $stmt->close();

        // check for successful store
        if ($result) {
            $stmt = $this->conn->prepare("SELECT * FROM members WHERE userID = ?");
            $stmt->bind_param("s", $userID);
            $stmt->execute();
            $user = $stmt->get_result()->fetch_assoc();
            $stmt->close();

            return $user;
        } else {
            return false;
        }
    }

    // 로그인 체크
    public function getUser($userID, $password) {
        $stmt = $this->conn->prepare("SELECT * FROM members WHERE userID = ?");
        $stmt->bind_param("s", $userID);

        if ($stmt->execute()) {
            $user = $stmt->get_result()->fetch_assoc();
            $stmt->close();

            // verifying user password
            $salt = $user['salt'];
            $encrypted_password = $user['passwd'];
            $hash = $this->checkhashSSHA($salt, $password);
            // check for password equality
            if ($encrypted_password == $hash) {
                // user authentication details are correct
                return $user;
            }
        } else {
            return NULL;
        }
    }

    // 안드로이드/아이폰 로그인 체크
    public function LoginUserChk($userID,$password,$deviceID){
        if(empty($userID) || empty($password)){
            return 0;
        } else {
            $user = $this->getUser($userID, $password);
            if($user['idx']>0){
                // 장치 일련번호 체크
                if($user['phoneSE'] == NULL){
                    // 신규 장비번호 입력(최초 로그인)
                    $this->LoginUserEquipInput($userID,$deviceID);
                    return $user['idx'];
                } else {
                    if($user['phoneSE'] === $deviceID){
                        return 1; // 일련번호 일치
                    } else {
                        return -1; //일련번호 불일치
                    }
                }
            } else {
                return 0; //계정오류
            }
        }

    }

    // 장치번호 업데이트
    public function LoginUserEquipInput($userID,$deviceID){
        if(strlen($deviceID)>0 && is_numeric($deviceID)){ // 안드로이드폰
            $ostype = 2;
        } else if(strlen($deviceID)>30){ // 아이폰
            $ostype = 1;
        } else { // 기타
            $ostype = 0;
        }

        $sql='update members set phoneSE=?, OStype=? where userID=?';
        $stmt = $this->conn->prepare($sql);
        $stmt->bind_param("sss", $deviceID, $ostype, $userID);
        $status = $stmt->execute();
        if($status == true){
            return 1;
        } else {
            return 0;
        }
    }//end

    // 장치번호 초기화
    public function EquipReset($userID){
        $ostype = 0;
        $sql='update members set phoneSE=NULL, OStype=? where userID=?';
        $stmt = $this->conn->prepare($sql);
        $stmt->bind_param("ss", $ostype, $userID);
        $status = $stmt->execute();
        if($status == true){
            return 1;
        } else {
            return 0;
        }
    }//end

    // 회원 가입 여부 체크
    public function isUserExisted($userID) {
        $stmt = $this->conn->prepare("SELECT userID from members WHERE userID = ?");

        $stmt->bind_param("s", $userID);
        $stmt->execute();
        //$stmt->store_result();
        $result = $stmt->get_result();

        if ($result->num_rows > 0) {
            // user existed
            $stmt->free_result();
            $stmt->close();
            return true;
        } else {
            // user not existed
            $stmt->free_result();
            $stmt->close();
            return false;
        }
    }

    // 회원 정보 삭제
    public function deleteUser($userID){
        $stmt = $this->conn->prepare("delete FROM members WHERE userID = ?");
        $stmt->bind_param("s", $userID);
        $stmt->execute();
        $stmt->close();
    }

    public function hashSSHA($password) {

        $salt = sha1(rand());
        $salt = substr($salt, 0, 10);
        $encrypted = base64_encode(sha1($password . $salt, true) . $salt);
        $hash = array("salt" => $salt, "encrypted" => $encrypted);
        return $hash;
    }

    public function checkhashSSHA($salt, $password) {
        $hash = base64_encode(sha1($password . $salt, true) . $salt);
        return $hash;
    }

}

?>


- user 권한 및 패스워드 설정을 다르게 한 경우에는 해당 파일을 수정해야 한다.


register.php

- 안드로이드에서 전송된 데이터라고 가정하고 값을 직접 입력하여 DB에 잘 저장되는지 테스트했다.


json 으로 넘길 데이터 형태는 개발자가 변경하고 싶은데로 변경하여 사용하면 된다.


login.php


등록된 폰과 다를 경우 관리자에게 문의하여 장치번호를 초기화하고 새로운 폰으로 다시 등록해야만 로그인이 가능해진다.


안드로이드 코드 부분은 다음에 기회되면 작성할 예정이다.

이미 기존에 로그인 처리하는 코드 예제가 있으므로 그걸 수정해서 활용하거나 PHP 코드를 일부 수정해서 사용해도 된다.


androidPHP_Login_and_Registration.zip



PDO 개념 및  사용법 : http://link2me.tistory.com/1332

PDO DB 연결방법 : http://link2me.tistory.com/1398

PDO MemberClass : http://link2me.tistory.com/1399

MVC 패턴 고려한 PDO Class 만들기 : http://link2me.tistory.com/1401

블로그 이미지

Link2Me

,
728x90

Audio Streaming 이 잘 동작하는 걸 확인하고 좀 더 살을 붙여서 제대로 동작되도록 1단계 작업을 했다.


서버에 지정될 폴더에 있는 음악 파일을 모두 가져와서 ListView 로 보여준다.

코드 구현하면서 상위 폴더로의 이동을 어떻게 할까 고민하느라고 시간이 좀 걸렸다.


- 음악 어플을 구동하면 화면이 꺼지지 않도록 했다. 파일 재생이 완료되면 화면 꺼짐 기능이 동작된다.

- ListView 구현은 RecyclerView 를 이용했다.

- 서버(PHP Web)에서 데이터를 가져올 때 음악파일인지, 폴더인지 구분하여 가져오고

  구분 정보를 기준으로 ImageView 아이콘이 다르게 표시되도록 하였다.

- 서버와의 통신은 HttpURLConnection 방식으로 접속한다.

- 상위 폴더로 이동할 수 있도록 하였다.

- 음악을 선택하면 해당 곡부터 아래 곡을 연속재생하도록 구현했다.

- 미구현 사항 : 음악 재생, 멈춤 하는 팝업화면을 아직 구현하지 않았다.


앱 build.gradle


AndroidManifest.xml


activity_main.xml


file_item.xml


폴더 아이콘은 구글 이미지에서 구해서 파일 사이즈를 줄였다.


FileList_Item.java


MainActivity.java


서버와의 통신 HttpURLConnection


서버에서 데이터를 가져오지 못하거나 접속 연결이 잘못되는 경우를 고려하여 GET Response Code 를 표시하도록 했다. 데이터를 못가져오면 서버 코드 파일이 잘못일 수도 있으니 코드를 잘 구현해야 한다.

 



곡의 총 재생시간과 현 재생시간을 표시하고 앞으로 5초, 뒤로 5초를 선택할 수 있게 추가를 해봤다.

http://link2me.tistory.com/1350 게시글을 참조하면 도움된다.

블로그 이미지

Link2Me

,
728x90

이전 게시글(http://link2me.tistory.com/1391)에서 서버 파일 1개만 재생하는 걸 Spinner 기능을 이용하여 몇개 선택해서 듣어볼 수 있도록 수정해봤다.

스트리밍 기능이 동작되는 걸 확인할 수 있다.


activity_main.xml



파일을 테스트해보니 mp3, wav 파일은 스트리밍 재생이 되는데 flac, ape 파일은 재생을 못한다.


MainActivity.java

 import android.app.ProgressDialog;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {
    private Button btn;
    private boolean playPause;
    private MediaPlayer mediaPlayer;
    private ProgressDialog progressDialog;
    private boolean initalStage = true;

    private String[] getMp3files(){
        return new String[]{
                "/kim_01.wav",
                "/ImYours.mp3",
                "/kim_02.mp3",
                "/Rua_Vincent.wav",
                "/kimbumsu_01.mp3"
        };
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getMp3fileOptions();

    }

    private void getMp3fileOptions(){
        final Spinner mp3fileOptions = (Spinner) findViewById(R.id.mp3fileOptions);
        String[] audiofile = getMp3files();
        ArrayAdapter<String> arrayAdapter =  new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, audiofile);
        mp3fileOptions.setAdapter(arrayAdapter);
        mp3fileOptions.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                initStreaming(Value.IPADDRESS + mp3fileOptions.getItemAtPosition(i).toString());
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {

            }
        });
    }

    public void initStreaming(final String uri){
        if(mediaPlayer != null && mediaPlayer.isPlaying()){
            initalStage = true;
            playPause = false;
            btn.setText("Launch Streaming");
            mediaPlayer.stop();
            mediaPlayer.reset();
        }

        mediaPlayer = new MediaPlayer();

        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        progressDialog = new ProgressDialog(this);
        btn = (Button) findViewById(R.id.audioStreamBtn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(!playPause){
                    btn.setText("Pause Streaming");
                    if(initalStage){
                        new Player().execute(uri);
                    } else {
                        if(!mediaPlayer.isPlaying()){
                            mediaPlayer.start();
                        }
                    }
                    playPause = true;
                } else {
                    btn.setText("Launch Streaming");
                    if(mediaPlayer.isPlaying()){
                        mediaPlayer.pause();
                    }
                    playPause = false;
                }
            }
        });
    }

    @Override
    protected void onPause() {
        super.onPause();
        if(mediaPlayer != null){
            mediaPlayer.reset();
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }
   
    class Player extends AsyncTask<String, Void, Boolean>{

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressDialog.setMessage("Buffering...");
            progressDialog.show();
        }

        @Override
        protected Boolean doInBackground(String... strings) {
            Boolean prepared = false;

            try {
                mediaPlayer.setDataSource(strings[0]);
                mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(MediaPlayer mediaPlayer) {
                        initalStage = true;
                        playPause = false;
                        btn.setText("Launch Streaming");
                        mediaPlayer.stop();
                        mediaPlayer.reset();
                    }
                });

                mediaPlayer.prepare();
                prepared = true;
            } catch (IOException e) {
                Log.e("MyAudioStreamingApp",e.getMessage());
                prepared = false;
                e.printStackTrace();
            }
            return prepared;
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            if(progressDialog.isShowing()){
                progressDialog.cancel();
            }

            mediaPlayer.start();
            initalStage = false;
        }
    }
}


제어없이 바로 재생되는 것은  아래 메소드를 이용해도 된다.


     public void StartMusic(String url) {
        // HTTP스트리밍을 통한 원격의 URL에서 재생
        try {
            if(mediaPlayer!=null && mediaPlayer.isPlaying()){
                mediaPlayer.stop();
                mediaPlayer.reset();
            }

            mediaPlayer = new MediaPlayer();
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mediaPlayer.setDataSource(url);
            mediaPlayer.prepareAsync();
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                }
            });

        } catch (Exception e) {
            Log.e("MusicPlayer", e.getMessage());
        }
    }



스트리밍이 잘 되는 걸 확인했으니, 서버 파일을 자동으로 인식해서 처리하는 걸 해봐야겠다.

블로그 이미지

Link2Me

,
728x90

구글링해서 유투브 동영상에서 설명하는 코드를 보고 타이핑하여 기능이 제대로 동작되는지 확인하고 적어둔다.

가장 간단하게 서버에 있는 음악파일을 받아서 스트리밍으로 재생하는 것만 되는 코드라고 보면 된다.


테스트 환경

- Android Studio 3.3


앱 build.gradle

 apply plugin: 'com.android.application'

android {
    compileSdkVersion 26



    defaultConfig {
        applicationId "com.tistory.link2me.audiostreamingplayer"
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}


AndroidManifest.xml 파일에는

<uses-permission android:name="android.permission.INTERNET" />
만 추가했다.


MainActivity.java

import android.app.ProgressDialog;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {
    private Button btn;
    private boolean playPause;
    private MediaPlayer mediaPlayer;
    private ProgressDialog progressDialog;
    private boolean initalStage = true;
    String url = Value.IPADDRESS + "/kim_01.wav"; // 서버 음악파일 경로

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mediaPlayer = new MediaPlayer();
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        progressDialog = new ProgressDialog(this);
        btn = (Button) findViewById(R.id.audioStreamBtn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(!playPause){
                    btn.setText("Pause Streaming");
                    if(initalStage){
                        new Player().execute(url);
                    } else {
                        if(!mediaPlayer.isPlaying()){
                            mediaPlayer.start();
                        }
                    }
                    playPause = true;
                } else {
                    btn.setText("Launch Streaming");
                    if(mediaPlayer.isPlaying()){
                        mediaPlayer.pause();
                    }
                    playPause = false;
                }
            }
        });

    }

    @Override
    protected void onPause() {
        super.onPause();
        if(mediaPlayer != null){
            mediaPlayer.reset();
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }

    class Player extends AsyncTask<String, Void, Boolean>{

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressDialog.setMessage("Buffering...");
            progressDialog.show();
        }

        @Override
        protected Boolean doInBackground(String... strings) {
            Boolean prepared = false;

            try {
                mediaPlayer.setDataSource(strings[0]);
                mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(MediaPlayer mediaPlayer) {
                        initalStage = true;
                        playPause = false;
                        btn.setText("Launch Streaming");
                        mediaPlayer.stop();
                        mediaPlayer.reset();
                    }
                });

                mediaPlayer.prepare();
                prepared = true;
            } catch (IOException e) {
                Log.e("MyAudioStreamingApp",e.getMessage());
                prepared = false;
                e.printStackTrace();
            }
            return prepared;
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            if(progressDialog.isShowing()){
                progressDialog.cancel();
            }

            mediaPlayer.start();
            initalStage = false;
        }
    }
}


이 기본적인 걸 가지고 코드에 살이 붙이는 작업을 진행할 예정이다.

블로그 이미지

Link2Me

,