728x90

PHP의 $_POST 변수는 application/x-www-form-urlencoded 또는 multipart/form-data 형식의 데이터를 처리한다.

 

로그인 처리하는 파일에서 코드 구조가

 

로 되어 있다면 application/json 으로 Content-Type를 넘기면 안된다.

Content-Type:application/x-www-form-urlencoded 로 설정해야 한다.

 

 

JMeter에서 JSON을 Body Data로 보낼 경우, $_POST가 아니라 php://input을 통해 데이터를 읽어야 한다.

 

와 같이 데이터를 넘기면 로그인 결과 처리를 하는 파일 구조도 아래와 같이 변경되어야 JSON 으로 넘어온 데이터를 인식하고 처리한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
if(!isset($_SESSION)) {
    session_start();
}
 
// jwt 라이브러리 사용
use \Firebase\JWT\JWT;
 
error_reporting(0);
 
// JSON 데이터를 가져오기
$json_data = file_get_contents("php://input");
 
// JSON을 PHP 배열로 변환
$data = json_decode($json_datatrue);
 
if(isset($data['userID']) && !empty($data['userID']) && isset($data['password']) && !empty($data['password'])) {
    require_once 'path.php';
    require_once $g['path_config'].'config.php';
    require_once $g['path_class'].'dbconnect.php';
    require_once $g['path_class'].'loginClass.php';
    require_once $g['path_root'].'vendor/autoload.php';
 
    @extract($data);
 
    $c = new LoginClass();
 

 

 

POST 요청이 application/json 방식으로 보내진 경우
→ $_POST['userID']로 접근 불가능 ❌
→ file_get_contents("php://input")를 사용해야 데이터 수신 가능

POST 요청이 application/x-www-form-urlencoded 방식으로 보내진 경우
→ $_POST['userID']로 접근 가능
→ JSON이 아니라 key-value 쌍으로 데이터를 전송해야 함

728x90
블로그 이미지

Link2Me

,
728x90

RSA 암호화/복화화 시 실시간으로 KEY 조합이어야 할 경우에는 어떻게 접근해야 할까?

실시간이라는 의미는 매번 접속할 때마다 RSA 암호화/복화화 KEY가 달라진다는 것이다.

HDD 에 저장된 Public KEY, Private KEY 가 아니라 메모리 상에서 접속시마다 생성하는 KEY 쌍이다.

최근의 보안검증에서는 이런 걸 요구하고 있다.

해킹 시도를 원천 차단하고자 하는 것이 목적이기 때문일 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
if(!isset($_SESSION)) {
    session_start();
}
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'deviceChk.php';
require_once $g['path_config'].'config.php';
require_once $g['path_class'].'dbconnect.php';
require_once $g['path_class'].'loginClass.php';
$c = new LoginClass();
 
$salt = sha1(rand());
$salt = substr($salt020);
 
$key = $c->rsa_generate_keys($salt);
$pubkey = $c->rsa_publickey($key['pub_key']);
$prikey = $c->rsa_privatekey($key['pri_key']);
$_SESSION['prikey'= $prikey;
$_SESSION['salt'= $salt;
 
// JSON 응답으로 공개키 전달
header('Content-Type: application/json');
echo json_encode(["pub_key" => $pubkey]);
 
/*
echo json_encode([
    "pub_key" => $pubkey,
    "session_id" => session_id(),
    "prikey" => isset($_SESSION['prikey']) ? $_SESSION['prikey'] : "NOT_SET",
    "salt" => isset($_SESSION['salt']) ? $_SESSION['salt'] : "NOT_SET",
]);
 
// */
?>

 

위 코드와 같이 PHP SESSION 으로 Private KEY를 전달한다.

KEY 쌍을 생성할 때 salt 를 적용하면 변화가 더욱 심해진다.

 

이런 점을 감안해서 JMeter 에서 암호화 로그인을 성공하기 위한 과정이다.

처음부터 부하를 많이 주는 테스트를 하면 안되고 로그인까지 정상적으로 되는지 확인해야 하기 때문에 1로 설정한다.

 

 

 

JSON Path expression 을 왜 이렇게 설정했는지는 위 PHP 소스코드를 보면 알 수 있다.

만약 data 배열 하단에 pub_key 로 생성하면 $.data.pub_key 로 변경해줘야 한다.

 

PHP SESSION 을 변수에 담아서 저장하기 위한 과정이다.

매번 HTTP Request 추가하여 실행될 때 마다 다른 PHPSESSID 가 생성될 수 있으니 필요한 곳에 값을 전달하면 된다.

 

 

추출한 PHP SESSION 값을 변수에 저장하는 과정이다.

 

 

공개키 가져오는 과정이다. JSR223 PreProcess 를 사용하여 변수를 가져와 암호화된 비밀번호를 생성할 수 있다.

 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import java.security.PublicKey
import javax.crypto.Cipher
import org.apache.commons.codec.binary.Base64
 
// JSON Extractor에서 추출한 공개 키 가져오기
String publicKeyString = vars.get("pubkey");
 
// Debugging: JMeter log에서 확인 가능
log.info("Extracted Public Key: " + publicKeyString);
 
if (publicKeyString == null || publicKeyString.equals("NOT_FOUND"|| publicKeyString.isEmpty()) {
    throw new RuntimeException("Public Key not found in response! Check JSON Extractor.");
}
 
// 암호화할 비밀번호 가져오기
String password = vars.get("password");
 
// 공개 키 복원
byte[] keyBytes = Base64.decodeBase64(publicKeyString);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(spec);
 
// RSA 암호화 수행
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(password.getBytes("UTF-8"));
 
// Base64로 인코딩
String encryptedPassword = Base64.encodeBase64String(encryptedBytes);
 
// JMeter 변수 저장
vars.put("encryptedPassword", encryptedPassword);
log.info("Encrypted Password: " + encryptedPassword);
 

 

 

로그인할 때 userID와 password 를 앞에서 생성한 encryptedPassword 를 입력변수로 전달한다.

Front-End 단의 코드는 필요없고, Back-End 단의 PHP 가 POST 변수로 받는 부분을 고려하면 된다.

 

 

Header 메시지는 웹브라우저의 개발자모드에서 확인한 사항을 적어둔다.

Cookie 에 앞에서 추출한 PHPSESSID를 변수로 적어준다.

 

 

 

토큰 추출 관련 PHP 소스코드

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
if(!isset($_SESSION)) {
    session_start();
}
error_reporting(0);
 
header('Content-Type: application/json');
/*
echo json_encode([
    "session_id" => session_id(),
    "prikey" => isset($_SESSION['prikey']) ? $_SESSION['prikey'] : "NOT_SET",
    "salt" => isset($_SESSION['salt']) ? $_SESSION['salt'] : "NOT_SET",
]);
exit;
// */
// jwt 라이브러리 사용
use \Firebase\JWT\JWT;
 
// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST&& $_SERVER['REQUEST_METHOD'== "POST"){
    @extract($_POST);
    if(isset($userID&& !empty($userID&& isset($password&& !empty($password)) {
        require_once 'path.php';
        require_once $g['path_config'].'config.php';
        require_once $g['path_class'].'dbconnect.php';
        require_once $g['path_class'].'loginClass.php';
        require_once $g['path_root'].'vendor/autoload.php';
        $c = new LoginClass();
 
        $private_key = $_SESSION['prikey'];
        $salt = $_SESSION['salt'];
 
        $password = $c->rsa_decrypt_key($password$private_key,$salt); // RSA 패스워드 복호화 기능 구현
                
        $rs = $c->LoginSuccessChk($userID,$password); // 개발 서버
        $rs = (int)$rs;
        switch($rs){
            case 11:
                // 로그인 허용
                $user = $c->getUser($userID$password);
                if ($user != false) {
                    $issuedAt = time();
                    $expirationTime = $issuedAt + 3600// 1시간 유효
                    if($user['admin']==1 || $user['admin']==2) {
                        $payload = array(
                            'iat' => $issuedAt,
                            "exp" => $expirationTime,
                            "userID" => $user['userID'],
                            "userNM" => $user['userNM'],
                            "access" => $user['access'],
                            "usrIDX" => $user['idx'],
                            "orgId" => $user['orgId'],
                            "authID" => $user['admin'],
                        );
                        $rs = 21;
                    } else {
                        $payload = array(
                            'iat' => $issuedAt,
                            "exp" => $expirationTime,
                            "userID" => $user['userID'],
                            "userNM" => $user['userNM'],
                            "access" => $user['access'],
                            "usrIDX" => $user['idx'],
                            "orgId" => $user['orgId'],
                        );
                    }
 
                    // JWT 생성
                    $token = JWT::encode($payload, SECRET_KEY, ALGORITHM);
 
                    // 세션 및 쿠키 저장
                    //$_SESSION['token'] = $token;
                    setcookie('token'$tokentime() + 3600'/'); // 1시간 동안 쿠키 저장
                    
                    header('Content-Type: application/json');
                    echo json_encode(array('token' => $token));
 
                } else {
                    echo json_encode(array('result' => '-3')); // 체크 필요
                }
                break;
 
            default:
                echo json_encode(array('result' => $rs));
                break;
        }
    } else {// 입력받은 데이터에 문제가 있을 경우
        echo json_encode(array('result' => '-2'));
    }
else { // 비정상적인 접속인 경우
    echo json_encode(array('result' => '-3')); // loginChk.php 파일을 직접 실행할 경우에는 화면에 0을 찍어준다.
    exit;
}
?>

 

 

 

로그인 이후의 파일에 접근하는 과정 설명 그림이다.

 

 

 

 

 

이 정도면 충분한 설명은 되었다고 본다.

소스코드를 분석하면서 JMeter 스크립트 과정을 작성해야 하는 거 같다.

 

본문내에 광고가 떠서 가독성을 떨어뜨리는 거 같아서 광고를 삭제시켰다.

블로그 접속빈도가 떨어져서 광고를 클릭할 가능성도 매우 낮다고 보기 때문이고, 정보 전달의 목적에 충실하자는 의도도 있다.

728x90
블로그 이미지

Link2Me

,
728x90

JMeter 에서 RSA 암호화 로그인 방법을 찾으려고 개고생을 했다.

구글링으로 원하는 답을 구하지 못했고 chatGPT도 엉터리 답변 때문에 수많은 시간을 낭비했다.

Javascript 에서 jsencrypt.min.js 라이브러리를 활용하여 RSA 암호화를 하기 때문에 이 코드 찾는 방법에 수많은 시간을 허비 했던 것이 가장 큰 오류중의 하나였다.

이기종 언어간에 RSA 암호화/복호화 가능하기 때문에 Java에서 제공하는 RSA 암호화 로직을 활용하면 된다.

 

자세한 설명은 생략하고 이미지 순서에 따라 처리하면 된다.

처음에는 로그인이 성공하는지 확인하기 위한 목적이니 2번과 같이 1회로 한정한다.

 

공개키 추출하는 방법

- 공개키를 JSON 으로 반환하도록 처리해야 한다.

 

 

JSON 으로 제공된 pub_key 를 추출하여 pubkey 변수에 저장하기 위한 목적이다.

가장 크게 삽질한 것은 JSR223 PreProcessor 부분이다. 바로 변수로 저장할 수 있는 줄 알고 했는데 ㅠㅠㅠ

 

 

변수를 저장하기 위해서 HTTP Request 를 추가하고 pubkey 변수를 입력받도록 파라미터를 아래와 같이 지정한다.

 

 

 

 

21번 항목의 코드 내용이다.

아래 코드에서 vars.get("pubkey") 의 pubkey 가 입력변수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import java.security.PublicKey
import javax.crypto.Cipher
import org.apache.commons.codec.binary.Base64
 
// JSON Extractor에서 추출한 공개 키 가져오기
String publicKeyString = vars.get("pubkey");
 
// Debugging: JMeter log에서 확인 가능
log.info("Extracted Public Key: " + publicKeyString);
 
if (publicKeyString == null || publicKeyString.equals("NOT_FOUND"|| publicKeyString.isEmpty()) {
    throw new RuntimeException("Public Key not found in response! Check JSON Extractor.");
}
 
// 암호화할 비밀번호 가져오기
String password = vars.get("password");
 
// 공개 키 복원
byte[] keyBytes = Base64.decodeBase64(publicKeyString);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(spec);
 
// RSA 암호화 수행
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(password.getBytes("UTF-8"));
 
// Base64로 인코딩
String encryptedPassword = Base64.encodeBase64String(encryptedBytes);
 
// JMeter 변수 저장
vars.put("encryptedPassword", encryptedPassword);
log.info("Encrypted Password: " + encryptedPassword);
 

Java 에서의 RSA 암호화가 다른 언어에서 복호화가 가능하다.

Javascript 에서는 jsencrypt.min.js 라이브러리를 활용하여 RSA 암호화가 가능하다.

하지만 JMeter 에서는 javascript 암호화를 지원하지 않기 때문에 Java RSA 암호화를 활용하는 것이다.

 

vars.put(" encryptedPassword ") 가 아래 그림에서 패스워드 입력값이다.

 

나머지 사항은 이전 게시글 내용과 이미지를 참조하면 된다.

RSA 암호화 시 KEY 쌍이 실시간으로 변경되는 경우에 대한 처리는 아직 테스트하지 못했다. → 테스트 성공했다. 이 경우는 다른 게시글에서 올리겠다.

RSA 암호화/복호화 시 사용할 KEY 쌍이 일정시간동안 변경되지 않는 방식인 경우를 테스트 후 성공했다.

 

 

 

 

 

chatGPT 에게 제대로 된 답변을 알려줬다.

728x90
블로그 이미지

Link2Me

,
728x90

먼저 token 인증을 할 환경을 구축해야 한다.

JWT 토큰 인증 환경 구성 이해하는데 시간이 좀 걸렸다. 이 부분은 필요하면 나중에 설명하겠다.

 

설명은 그림 위주로 설명을 할 것이다.

로그인 처리 없이 JMeter 부하테스트하는 것은 어렵지 않게 찾을 수 있다.

 

처음에는 Thread 숫자를 1로 놓고 확인해야 한다. Loop Count 도 1로 설정한다.

2번 Thread Group 이라는 명칭을 주의깊게 보자. 앞으로 반복되는 이미지에서 2번 위치에 표기되는 항목 명칭을 보면 이해가 빠르게 될 것이다.

 

Threads Group
- continue : 에러가 발생해도 테스트를 계속 진행
- Start Next Thread Loop : 현재 쓰레드의 루프를 종료하고 다음 루프를 시작
- Stop Thread : 에러가 발생한 쓰레드만 종료
- Stop Test Now : 현재 실행중인 모든 샘플러를 강제로 중지하고 테스트를 즉시 종료

 

목표

- HTTP 요청을 통해 로그인 API 호출
- JSON 응답에서 토큰 추출
- 추출된 토큰을 이후 요청에 활용할 수 있도록 저장

 

HTTP Request 추가하고, Name을 로그인으로 변경했다.

 

 

Path 에 도메인 주소를 적어줬다. Virtual Host 로 여러개 설정한 환경에서 테스트해보니 첫번째 URL을 인식하더라.

그래서 테스트하고 싶은 도메인을 상단으로 올리고 클라우드 서버를 재기동해줬다.

7번 항목은 loginView.php 에서는 RSA 암호화를 하고 jwtLoginChk.php 파일에서는 RSA 복호화를 한 다음에 로그인에 성공하면 jwt 토큰인증을 생성하도록 처리한다.

하지만 JMeter 에서 RSA 암호화 생성하고 로그인처리하는 것은 쉽지 않는 거 같아서 jwtLoginChk2.php 파일을 생성하고 RSA 복호화처리하는 부분을 주석처리하고 8번 userID, password 를 입력받아 토큰 생성을 하도록 임시 변통했다.

JMeter에서는 JavaScript를 직접 실행할 수 없으므로, 사전에 암호화된 값으로 요청해야 한다.

하지만 Java 코드로 RSA 암호화하는 방법이 있더라. 다른 언어를 같이 다뤄보지 않았으면 해결방법 찾기가 쉽지 않을 수 있다.

 

로그인 하위에 HTTP Header Manager 를 추가한다.

12번 항목에 나오는 사항은 실행을 했을 때 반환하는 결과를 보고 찾아서 입력하면 된다.

 

 

로그인 결과로 반환하는 JSON 메시지에서 토큰을 자동 추출하기 위한 과정이다.

 

JSON 응답에서 토큰 추출
   JSON Extractor 추가 (HTTP Request 하위에 추가)
    Names of created variables: authToken
    JSON Path expressions: $.token
    Match No: 1
    Default Value: NOT_FOUND

 

 

로그인 후 쿠키/세션 처리
  로그인 성공 시, 응답 헤더에 Set-Cookie가 포함될 수 있다.
  HTTP Cookie Manager를 추가하여 이후 요청에서도 쿠키를 유지해야 한다.

 

 

토큰이 잘 생성되고 있는지 확인하기 위해 View Results Tree 를 추가해준다.

 

 

 

 

JSON 응답 확인 및 토큰 추출
  View Results Tree에서 응답 확인
  JSON 응답이 오면 JSON Extractor 추가하여 $.token 값 추출

 

 

Debug Sampler 에서 값이 잘 추출되고 있는지 확인할 수 있다.

 

 

로그인 이후의 URL 에 접속 테스트 설정

 

 

 

 

 

 

Label: 요청했던 Request Sampler의 이름
Samples: 서버에 요청한 횟수
Average: 평균응답시간(ms)
Min: 최소응답시간(ms)
Max: 최대응답시간(ms
Std.Dev.: 표준편차 요청에 대한 응답시간이 일정하고 안정적인가를 확인한다. 값이 적을수록 안정적이다.
Error: Error율(%)
Throughput: 처리량( 초당 처리건수)
KB/sec: 처리량(초당 처리 KB)

728x90
블로그 이미지

Link2Me

,
728x90

PHP 에서 DB 와 연결해서 TreeView 처리하는 코드 예제이다.

Treeview 의 깊이를 좁게 처리하기 위해서 style 을 수정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php
error_reporting(0); // 경고 출력 없애기
//*
ini_set("display_startup_errors"1);
ini_set("display_errors"1);
error_reporting(E_ALL);
// */
 
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php'// 세션 체크
require_once $g['path_config'].'config.php';
require_once $g['path_class'].'dbconnect.php';
require_once $g['path_class'].'adminClass.php';
$a = new adminClass();
$dbInstance = new DBDataClass();
$db = $dbInstance->db;
 
date_default_timezone_set('Asia/Seoul');
 
// Function to fetch categories
function fetchCategories($parent_id = null$db) {
    $stmt = $db->prepare("SELECT * FROM categories WHERE parent_id " . ($parent_id ? "= :parent_id" : "IS NULL"));
    if ($parent_id) {
        $stmt->bindParam(':parent_id'$parent_id, PDO::PARAM_INT);
    }
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
 
// Recursive function to build tree
function buildTree($parent_id = null$db) {
    $categories = fetchCategories($parent_id$db);
    if (count($categories> 0) {
        echo '<ul class="nested">';
        foreach ($categories as $category) {
            echo '<li data-id="' . $category['id'] . '">' . $category['name'];
            buildTree($category['id'], $db);
            echo '</li>';
        }
        echo '</ul>';
    }
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PHP Treeview with MySQL and jQuery</title>
    <style>
        ul {
            list-style-type: none;
            padding-left: 10px;
        }
        li {
            cursor: pointer;
            padding: 5px;
        }
        .nested {
            display: none; /* 기본적으로 닫힌 상태 */
        }
    </style>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body>
    <h1>Category Treeview</h1>
    <div id="treeview">
        <?php buildTree(null$db); ?>
    </div>
 
    <script>
        $(document).ready(function () {
            // Initially hide all nested lists
            $(".nested").show();
 
            // Toggle visibility on click
            $("#treeview li").click(function (e) {
                $(this).children("ul").slideToggle();
                e.stopPropagation(); // Prevent event bubbling
            });
        });
    </script>
</body>
</html>

 

 

MySQL 테이블 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE categories (
  id int(11NOT NULL,
  name varchar(255NOT NULL,
  parent_id int(11DEFAULT NULL
ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
 
INSERT INTO categories (id, name, parent_id) VALUES
(1'Electronics'NULL),
(2'Laptops'1),
(3'Smartphones'1),
(4'Dell'2),
(5'HP'2),
(6'Samsung'3),
(7'Apple'3);
 
ALTER TABLE categories
  ADD PRIMARY KEY (id),
  ADD KEY parent_id (parent_id);
 
ALTER TABLE categories
  MODIFY id int(11NOT NULL AUTO_INCREMENTAUTO_INCREMENT=8;
 
ALTER TABLE categories
  ADD CONSTRAINT categories_ibfk_1 FOREIGN KEY (parent_id) REFERENCES categories (id) ON DELETE CASCADE;
COMMIT;

 

categories.sql
0.00MB

 

728x90
블로그 이미지

Link2Me

,
728x90

jQuery Mobile 로 된 오래된 코드를 최신의 jQuery 로 적용했더니 화면이 깨지고 엉망이다.

이를 해결하기 위한 방법이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html> 
<html> 
<head> 
<title><?php echo $hostName;?></title>
<meta charset="utf-8" />     
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0,user-scalable=no"/>
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="-1" />
<meta http-equiv="pragma" content="no-cache" />
<link rel="shortcut icon" href="./images/icon72.png">
<link rel="apple-touch-icon" href="./images/icon57.png">
<link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.css" rel="stylesheet" type="text/css" />
<link href="css/mWebStyle.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://code.jquery.com/jquery-migrate-3.5.2.js"></script>
<script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script src="js/jsencrypt.min.js"></script>
<script src="js/mWebScript.js"></script>
 

 

별도 코드가 추가될 수 있기 때문에 하단에 </head> 태그를 생략했고 개별 파일에서 추가했다.

728x90
블로그 이미지

Link2Me

,
728x90

네이버 STMP 메일 발송하기 위한 준비 과정이다.

2단계로 SMS 인증을 선택한 경우에는 SMTP 메일 발송을 위한 별도의 기기용 비밀번호를 추가로 설정해야 한다.

 

 

 

 

 

 

설정한 비밀번호를 구현한 코드에 넣어야 한다.

 

function email_send($to_email,$subject,$message){
    $mail = new PHPMailer(true);
    //$mail->SMTPDebug = SMTP::DEBUG_SERVER;  //Enable verbose debug output
 
    $mail->Charset = 'UTF-8';
 
    $mail->isSMTP();                           //Send using SMTP
    $mail->Host       = 'smtp.naver.com';     //Set the SMTP server to send through
    $mail->SMTPAuth   = true;                //Enable SMTP authentication
    $mail->Username   = 'jsk005@naver.com';  //SMTP username
    $mail->Password   = '';      //SMTP password (기기 비밀번호)
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; //Enable implicit TLS encryption
    $mail->Port       = 465//TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`
 
    //Recipients
    $mail->setFrom('jsk005@naver.com''홍길동');
    //foreach($recipients as $to_email){
       $mail->addAddress($to_email);     //Add a recipient
    //}
    $mail->addReplyTo('jsk005@naver.com''홍길동');
 
    //Content
    $mail->isHTML(true);        //Set email format to HTML
    $mail->Subject = $subject;
    $mail->Body    = $message;
 
    $mail->send();
}

 

728x90
블로그 이미지

Link2Me

,
728x90

juso.go.kr 사이트에 Python selenium 으로 조회하여 반환받은 결과를 DB에 넣고자 하는데 지번주소 세부주소까지 나오는 데이터가 있다.

 

이를 파싱처리는 코드를 PHP로 구현하고 적어둔다.

주소를 살펴보다 확인 결과 세종로 같은 경우가 빠져있다.

 

<?php
 
ini_set("display_startup_errors"1);
ini_set("display_errors"1);
error_reporting(E_ALL);
 
 
$address = '경기도 평택시 안중읍 안중리 산 69-7번지';
$address = '인천광역시 강화군 화도면 사기리 502-1번지';
$address = '경기도 부천시 소사구 괴안동 105-3 역곡현대아파트2차';
$address = '경기도 부천시 오정구 여월동 329 부천소방서 여월119안전센터';
 
preg_match('/(.+?)동\s/'$address,$out);
if(!empty($out&& strlen($out[0])>0) {
    $jusoDong = preg_replace("/\s{2,}/"," ",$out[0]);
    $str = explode($out[0],$address);
    $vout = explode(" ",$str[1]);
    $jiAddress = $jusoDong.' '.$vout[0];
    $jiAddress = preg_replace("/\s{2,}/"," ",$jiAddress);
    echo $jiAddress.'<br/>';
else {
    preg_match('/(.+?)가\s/'$address,$out);
    if(!empty($out&& strlen($out[0])>0) {
        $jusoDong = preg_replace("/\s{2,}/"," ",$out[0]);
        echo $jusoDong.'<br/>';        
    } else {
        preg_match('/(.+?)[읍,면]\s/'$address,$out);
        if(!empty($out&& strlen($out[0])>0) {
            $str = explode($out[0],$address);
            preg_match('/(.+?)리\s/'$str[1],$vout);
            if(!empty($vout&& strlen($vout[0])>0) {
                $jusoDong = trim($out[0].' '.$vout[0]);
                $jusoDong = preg_replace("/\s{2,}/"," ",$jusoDong);
                echo $jusoDong.'<br/>';
            } else {
                echo $address.'<br/>';
            }
            
        }
    }
 
?>
 

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

주소(juso.go.kr) 사이트에서 파일을 다운로드 받아서 주소를 만들고 파싱처리를 하다보니 이런 데이타가 있더라.

 

대부분은 지번주소가 존재하는데 신규로 생성되는 도로명주소에 해당하는 지번주소가 없는 것이다.

이런 데이터는 Naver Map API 에 위치좌표를 조회하면 결과가 없는 경우가 있다.

그렇다고 구글 Geocording API 를 연동하여 처리를 하자니 건당 비용이 발생해서 못하겠다.

불편하지만 구글 스프레드시트를 통해서 자료를 업데이트하는 수 밖에...

방법을 좀 더 찾아봐야겠다.

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

Javascript 코드를 구현할 때 재귀호출을 하면서 Identifier 'marker' has already been declared 라는 메시지가 나온다.

뭐가 문제일까?

 

아래코드를 새로운 창에서 매번 실행한다면 문제될 것이 전혀 없다.

그리고 var 변수로 선언된 변수는 다시 선언해도 되므로 문제가 되지 않는다.

즉, 특정 div 영역에 재로딩을 해도 var 변수가 덮어쓰기 되므로 문제될 것이 없다.

하지만 변수를 let, const  로 변경하면 문제가 될 수 있다.

<body>
<div id="map" style="width:100%;height:350px;"></div>
 
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 사용하세요"></script>
<script>
var mapContainer = document.getElementById('map'), // 지도를 표시할 div 
    mapOption = { 
        center: new kakao.maps.LatLng(33.450701126.570667), // 지도의 중심좌표
        level: 3 // 지도의 확대 레벨
    };
 
var map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
 
// 마커가 표시될 위치입니다 
var markerPosition  = new kakao.maps.LatLng(33.450701126.570667); 
 
// 마커를 생성합니다
var marker = new kakao.maps.Marker({
    position: markerPosition
});
 
// 마커가 지도 위에 표시되도록 설정합니다
marker.setMap(map);
 
// 아래 코드는 지도 위의 마커를 제거하는 코드입니다
// marker.setMap(null);    
</script>
</body>

 

 

아래와 같이 코드를 구현했을 경우에는 어떤 문제가 생기는지 살펴보자.

id가 panel_content 라는 DIV 영역에 필요한 파일을 계속 Load하여 화면을 새로 그리는 구조로 코드를 구현해서 사용하고 있다.

<main>
    <div class="container-fluid text-center">
        <div class="row">
            <div class="col-md-12">
                <div class="content" id="panel_content">
                </div>
                <div class="card">
                </div>
            </div>
        </div>
    </div>
</main>
 
<script>
function MListTable(where,keyword,curPage,uri,bidx,sort,cat1,cat2,idx) {
 
    if (keyword.length > 20) {
        calluri = uri + '?p='+curPage+'&idx='+idx+'&cat1='+cat1+'&cat2='+cat2+'&where='+where+'&keyword='+encodeURIComponent(keyword)+'&sortby='+sort;
        console.log('bCode : ' + calluri);
    }
    else
        calluri = uri + '?p='+curPage+'&idx='+idx+'&cat1='+cat1+'&cat2='+cat2+'&where='+where+'&keyword='+encodeURIComponent(keyword)+'&bidx='+bidx+'&sortby='+sort;
 
    $('#panel_content').load(calluri, function() {
        var curPage = $('#paging .act a').text();
 
 
        $('#MNListTable tbody tr').mouseover(function() {
            $(this).children().css({
                'backgroundColor' : '#DCDCDC''cursor' : 'pointer'
            });
        }).mouseout(function() {
            $(this).children().css({
                'backgroundColor' : '#FFFFFF''cursor' : 'default'
            });
        });
 
        $('#MNListTable tbody tr').click(function() {
            var idx = $(this).attr('id');
            MNView(idx,curPage,where,keyword,cat1,cat2,bidx,sort);
        });
        $('#MNHome').click(function(e) {
            e.preventDefault();
            var uri = $('#urlPath').attr('url-path');
           MListTable('','',1,uri,'','','','','');
        });
 
    });
}
 
</script>

 

 

테이블에서 특정 TR 를 누르면 해당 값을 읽어서 MNView 로 넘기는 형태이다.

<script>
function MNView(idx,curPage,where,keyword,cat1,cat2,bidx,sort){
    var uri = 'View.php';
   MListTable(where,keyword,curPage,uri,bidx,sort,cat1,cat2,idx);
}
</script>

 

 

id 가 panel_content 의 DIV 영역에 View.php?idx=1 파일의 내용을 로딩하게 된다.

아래 파일이 View.php?idx=3 으로 다시 로딩하게 되면 let marker 가 이미 선언되어 있기 때문에 중복 선언되는 문제가 발생하는 것이다.

<div class="card">
<div class="mt-1 mb-2 mx-2">
    <div id="map" style="width:100%;height:450px;"></div>
</div>
</div>
 
<script>
var latitude = '<?php echo $lat;?>';
var longitude = '<?php echo $lng;?>';
 
// 마커를 담을 배열입니다
let marker = [];
 
const mapContainer = document.getElementById('map'), // 지도를 표시할 div 
    mapOption = { 
        center: new kakao.maps.LatLng(latitude, longitude), // 지도의 중심좌표
        level: 6 // 지도의 확대 레벨
    };
 
const map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
 
// 마커가 표시될 위치입니다 
const markerPosition  = new kakao.maps.LatLng(latitude, longitude); 
 
// 마커를 생성합니다
marker = new kakao.maps.Marker({
    position: markerPosition
});
 
// 마커가 지도 위에 표시되도록 설정합니다
marker.setMap(map);
 
// 아래 코드는 지도 위의 마커를 제거하는 코드입니다
// marker.setMap(null); 
</script>

 

 

이 문제를 방지하려면 함수를 만들어서 구현하면 해결된다.

index.php 파일 하단에 DrawMap 함수를 추가한다.

<script>
function DrawMap(latitude,longitude){
    // 마커를 담을 배열입니다
    let marker = [];
 
    const mapContainer = document.getElementById('map'), // 지도를 표시할 div 
        mapOption = { 
            center: new kakao.maps.LatLng(latitude, longitude), // 지도의 중심좌표
            level: 6 // 지도의 확대 레벨
        };
 
    const map = new kakao.maps.Map(mapContainer, mapOption); // 지도를 생성합니다
 
    // 마커가 표시될 위치입니다 
    const markerPosition  = new kakao.maps.LatLng(latitude, longitude); 
 
    // 마커를 생성합니다
    marker = new kakao.maps.Marker({
        position: markerPosition
    });
 
    // 마커가 지도 위에 표시되도록 설정합니다
    marker.setMap(map);
 
    // 아래 코드는 지도 위의 마커를 제거하는 코드입니다
    // marker.setMap(null); 
    
</script>

 

그리고 View.php 파일은 아래와 같이 수정한다.

그러면 let 과 const 로 선언한 변수가 재선언되지 않기 때문에 문제가 해결된다.

<div class="card">
<div class="mt-1 mb-2 mx-2">
    <div id="map" style="width:100%;height:450px;"></div>
</div>
</div>
 
<script>
var latitude = '<?php echo $lat;?>';
var longitude = '<?php echo $lng;?>';
 
DrawMap(latitude,longitude);
</script>

 

728x90
블로그 이미지

Link2Me

,
728x90

서버가 PDO를 지원하지 못한다고 해서 부득이하게 MySQLi 방식으로 변경해야 했다.

모든 코드를 전부 변경하려니 보통 난감한 상황이 아니다.

물론 완전 Legacy PHP 코드로 변경하면 좀 수월할 수도 있는데 stmt 로 변경하는 걸 고려했다.

 

PDO 로 작성된 코드 예시

<?php
class adminClass {
 
    function getUserNMFromIdx($idx){
        $sql = "SELECT userNM FROM members WHERE idx=?";
        $params = array($idx);
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        if($row = $stmt->fetch()){
            if($row[0== NULL)    return '';
            return $row[0];
        } 
    }    
}
 
?>

 

 

MySQLi 코드로 변경하는 방법

<?php
class adminClass {
 
    function sql($sql$params) {
        global $db;
        // $params : array 를 사용해야 한다.
        $stmt = $db->prepare($sql);
        $types = str_repeat('s'count($params)); //types
        $stmt->bind_param($types, ...$params); // bind 
        $stmt->execute();
        $result = $stmt->get_result();
        return $result->fetch_array(); // 배열 결과 처리
    }
 
    function getUserNMFromIdx($idx){
        $sql = "SELECT userNM FROM members WHERE idx=?";
        $params = array($idx);
        $row = $this->sql($sql$params);
        if($row[0== NULL)    return '';
        return $row[0];
    }    
}
 
?>

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

PHP와 혼용된 HTML Form 태크의 값을 넘길 때 변수명이 너무 많으면 일일이 값을 기입하여 전달하기가 쉽지 않다.

게다가 해킹 방지를 위해서는 key 값은 암호화처리를 해서 넘기는 것이 좋다.

JSEncrypt 암호화코드를 적용하려고 하니 일일이 변수명에 대한 값을 기록해야 하는 거 같아 이 방법을 사용하지 PHP 암호화 코드를 적용하고 복호화하는 방법을 택했다.

idx 값이 보통 숫자인데 이 값을 그대로 노출해서 수정/삭제를 하면 값을 변경하여 처리하는 해킹시도에 노출되기 쉽다.

그래서 반드시 암호화해서 처리를 하여야 한다.

<form class='form-horizontal' id='MNRegister'>
    <input type="hidden" name="idx" value="<?php echo $a->Encrypt($idx)?>" />
    <table class='table table-bordred'>
        <tr>
            <th style='width:15%'>성명</th>
            <td style='width:35%'>
                <?php echo $a->letterMasking($row['userNM']).'('.$a->IDMasking($row['userID']).')';?>
            </td>
            <th style='width:15%'>휴대폰℡</th>
            <td style='width:35%'>
                <?php echo $a->phoneNoMasking($a->Decrypt($row['mobileNO']));?>
            </td>
        </tr>
        <tr>
            <th style='width:15%'>직위</th>
            <td style='width:35%'><select class="browser-default custom-select" name="codeID">
            <?php
                foreach($posArr as $k=>$v){
                    if($row['codeID']==$k){
                        echo "<option value='".$k."' selected>".$v."</option>";
                    } else {
                        echo "<option value='".$k."'>".$v."</option>";
                    }
                }
            ?>
            </select>
            </td>
            <th style='width:15%'>팀서열</th>
            <td style='width:35%'>
                <input class="form-control input-sm" type="text" name="regNO" value="<?php echo $row['regNO'];?>">
            </td>
        </tr>
        <tr>
            <th style='width:15%'>본부</th>
            <td style='width:35%'>
                <input class="form-control input-sm" type="text" name="group2" value="<?php echo $row['group2'];?>">
            </td>
            <th style='width:15%'>담당</th>
            <td style='width:35%'>
                <input class="form-control input-sm" type="text" name="group3" value="<?php echo $row['group3'];?>">
            </td>
        </tr>
        <tr>
            <th style='width:15%'>부서</th>
            <td style='width:35%'>
                <input class="form-control input-sm" type="text" name="group4" value="<?php echo $row['group4'];?>">
            </td>
            <th style='width:15%'>팀명</th>
            <td style='width:35%'>
                <input class="form-control input-sm" type="text" name="group5" value="<?php echo $row['group5'];?>">
            </td>
        </tr>
        <tr>
            <th style='width:15%'>회원등급</th>
            <td style='width:35%'><select class="browser-default custom-select" name="admin">
            <?php
                foreach($sysrole as $k=>$v){
                    if($row['admin']==$k){
                        echo "<option value='".$k."' selected>".$v."</option>";
                    } else {
                        echo "<option value='".$k."'>".$v."</option>";
                    }
                }
            ?>
            </select>
            </td>
            <th style='width:15%'>개인정보</th>
            <td style='width:35%'><select class="browser-default custom-select" name="smart">
            <?php
                foreach($personinfo as $k=>$v){
                    if($row['smart']==$k){
                        echo "<option value='".$k."' selected>".$v."</option>";
                    } else {
                        echo "<option value='".$k."'>".$v."</option>";
                    }
                }
            ?>
            </select>
            </td>
        </tr>
        <tr>
            <th style='width:15%'>로그인</th>
            <td style='width:35%'><select class="browser-default custom-select" name="access">
            <?php
                foreach($access as $k=>$v){
                    if($row['access']==$k){
                        echo "<option value='".$k."' selected>".$v."</option>";
                    } else {
                        echo "<option value='".$k."'>".$v."</option>";
                    }
                }
            ?>
            </select>
            </td>
            <th style='width:15%'>Status</th>
            <td style='width:35%'><select class="browser-default custom-select" name="hidden">
            <?php
                foreach($hidden as $k=>$v){
                    if($row['hidden']==$k){
                        echo "<option value='".$k."' selected>".$v."</option>";
                    } else {
                        echo "<option value='".$k."'>".$v."</option>";
                    }
                }
            ?>
            </select>
            </td>
        </tr>
    </table>
</form>
 

 

 

jQuery 코드

form id NMRegister 값을 serialize하여 모든 변수를 한꺼번에 POST ajax로 넘길 수 있다.

function MNRegChk(idx,curPage,where,keyword,cat1,cat2,bidx,sort){
    //if(CheckErr($('input[name=userID]'),'아이디를 입력하세요.') == false) return false;
    //if(CheckErr($('input[name=userNM]'),'성명을 입력하세요.') == false) return false;
 
    var params = $('#MNRegister').serialize();
    $.post('MemberRegChk.php',params,function(msg){
        //prompt('msg',msg);
        var uri = $('#urlPath').attr('url-path');
        if(msg == 1){
            alert('등록되었습니다.');
            $('#dialog').dialog('close');
            MemberListTable(where,keyword,curPage,uri,bidx,sort,cat1,cat2,'');
        } else if(msg == 2){
            alert('수정되었습니다');
            $('#dialog').dialog('close');
            MemberListTable(where,keyword,curPage,uri,bidx,sort,cat1,cat2,'');
        } else if(msg == 0){
            alert('변경에 실패했습니다');
        }
    },'json');
 
}
 
function CheckErr(jsel,msg) {
    var count = jsel.val().length;
    if(count < 1) {
        alert(msg);
        jsel.focus();
        return false;
    }
    return true;
}
 

 

 

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

파일 다운로드 코드 예시이다.

파일을 업로드할 때 실제 저장되는 파일명과 View 파일 화면상에 출력되는 파일명이 다르게 처리하는 것이 중요하다.

 

<?php
if(!isset($_SESSION)){
    session_start();
}
 
require_once 'path.php';// root 폴더를 기준으로 상대경로 자동 구하기
require_once $g['path_config'].'dbconnect.php';
require_once $g['path_class'].'dbDataClass.php';
require_once $g['path_class'].'dbconnect.php';
require_once $g['path_class'].'loginClass.php';
 
$c = new LoginClass;
$d = new LegacyDBClass;
 
$idx = $_GET['idx'];
$idx = preg_replace("/[^0-9]/","",$idx);
 
$R = $d->getDbData('bbs_data','idx='.$idx,'*');
 
$file_path$g['path_root'].'files/infile/' . $R['filename'];
$filename = urldecode($R['realname']);
if(file_exists($file_path)) { 
    header("Content-disposition: attachment; filename={$filename}"); //Tell the filename to the browser    
    header('Content-type: application/octet-stream'); //Stream as a binary file! So it would force browser to download    
    readfile($file_path); //Read and stream the file
else {    
    echo -2;
}
 
?>
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

PHP 위지윅 에디터로 DB에 저장된 게시글을 열람하는 코드 예시이다.

파일 다운로드하는 코드를 jQuery 로 구현하려고 했으나 원하지 않는 결과가 나와서 a href 링크를 그대로 활용하여 코드를 구현했다.

<?php
error_reporting(0);
/*
ini_set("display_startup_errors", 1);
ini_set("display_errors", 1);
error_reporting(E_ALL);
// */
 
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php';
require_once $g['path_root'].'deviceChk.php';
require_once $g['path_root'].'ipFiltering.php';
require_once $g['path_config'].'config.php';
require_once $g['path_config'].'dbconnect.php';
require_once $g['path_class'].'dbDataClass.php';
require_once $g['path_class'].'dbconnect.php';
require_once $g['path_class'].'adminClass.php';
require_once $g['path_class'].'bbsClass.php';
 
$a = new adminClass();
$b = new bbsClass();
$d = new LegacyDBClass;
 
$idx = preg_replace("/[^0-9]/"""$_GET['idx']); // 숫자 이외 제거
$curPage = isset($_GET['p']) ? $d->XSSFilter($_GET['p']) : 1;
 
$R = $d->getDbData('bbs_data''idx='.$idx'*');
$html = ($R['html'== 1) ? 'HTML' : 'TEXT';
 
// 쿠키를 이용한 중복 조회수 증가 방지
if(!empty($R['idx']) && empty($_COOKIE['bbs_data_'.$R['idx']])) {
    if(strcmp($_SESSION['userID'],$R['userID']) !== 0){ // 등록자 본인이 아니면
        $d->getDbUpdate('bbs_data','hit=hit+1','idx='.$R['idx']); // 확인 필요
        setcookie('bbs_data_'.$R['idx'], TRUE, time() + (60 * 60 * 24), '/');
    }
}
 
$status = array('','접수','처리중','처리완료','처리불가');
 
$imgpath="../img/etc/";
 
?>
<table class="table table-bordered table-hover table-sm" cellspacing="0" width="100%">
    <tr>
        <td style="width:70px;">제목</td>
        <td class="text-left"><?php echo $R['subject']?></td>
    </tr>
    <tr>
        <td>내용</td>
        <td class="text-left"><?php echo $b->conv_content($R['content']);?></td>
    </tr>
    <tr>
        <td>등록자</td>
        <td class="text-left"><?php echo $d->letterMasking($R['userNM']);?></td>
    </tr>
    <tr>
        <td>첨부파일</td>
        <td class="text-left">
            <a href="bbsFiledown.php?idx=<?=$R['idx'];?>">
            <span class="badge badge-pill badge-secondary" id="attachFile" data-toggle="tooltip" title="누르면 다운로드 가능합니다.">
            <?php echo $R['realname'];?></span>
            </a>
        </td>
    </tr>
</table>
 
<?php include_once $g['path_admin'].'bbsComment.php';?>
 
<div class="table-responsive text-nowrap">
    <div class="float-left info">
        <button class="btn btn-md btn-outline-default m-0 px-3 py-2 z-depth-0 waves-effect" type="button" id="BBSHome">목록</button>
        <div id="bbsView" data-id="<?=$R['idx'];?>" curPage="<?=$curPage;?>"></div>
    </div>
    <div class="float-right info">
        <?php if($R['userID'== $_SESSION['userID']):?>
        <a href="bbsWrite.php" class="btn btn-md btn-outline-default m-0 px-3 py-2 z-depth-0 waves-effect" id="bbsModify">수정</a>
        <?php endif;?>
        <?php if($R['userID'== $_SESSION['userID'] ):?>
        <button class="btn btn-md btn-outline-default m-0 px-3 py-2 z-depth-0 waves-effect" type="button" id="bbsDelete">삭제</button>
        <?php endif;?>
    </div>
</div>
<script>
$('#BBSHome').click(function(e) {
    e.preventDefault();
    var uri = "bbsList.php";
    var page = $('#bbsView').attr('curPage');
    MemberListTable('','',page,uri,'','','','','');
});
 
$('#bbsModify').click(function(e){
    e.preventDefault();
    var uri = $(this).attr('href');
    var idx = $('#bbsView').attr('data-id');
    var page = $('#bbsView').attr('curPage');
    MemberListTable('','',page,uri,'','','','',idx);
});
 
$('#bbsDelete').click(function(e){
    e.preventDefault();
    var idx = $('#bbsView').attr('data-id');
    var curPage = $('#bbsView').attr('curPage');
 
    var verify = confirm('삭제하시겠습니까? \n 복구할 수 없습니다.');
    if (verify) {
        $.ajax({
            url:'bbsDelete.php',
            type: 'POST',
            data: { 
                idx:encrypt.encrypt(idx) 
            },
            dataType:'text',
            success:function(msg){
                if (msg == 1) {
                    alert('삭제되었습니다.');
                    var uri = "bbsList.php";
                    MemberListTable('','',curPage,uri,'','','','',0);
                } else if(msg == -2){
                    alert('삭제 권한이 없습니다.');
                } else {
                    alert('삭제중 오류가 발생하였습니다.');
                }
            },
            error: function(jqXHR, textStatus, errorThrown){
                alert("ajax error : " + textStatus + "\n" + errorThrown);
            }
        });
 
    }
 
});
 
$('#bbsStatus').click(function(e){
    e.preventDefault();
    var uri = "bbsStatus.php";
    var idx = $('#bbsView').attr('data-id');
    var page = $('#bbsView').attr('curPage');
    MemberListTable('','',page,uri,'','','','',idx);
});
 
$('#comment_form').click(function(e){
    e.preventDefault();
    var comment = $("textarea[name=comment]");
    if(comment.val() ==''){
        alert('댓글을 입력하세요');
        comment.focus();
        return false;
    }
    var page = $("input[name=p]").val();
    var idx = $("input[name=parentid]").val();
 
    $.ajax({
        url:'bbsCommentChk.php',
        type: 'POST',
        data: {
            mode:$("input[name=mode]").val(),
            parentid:idx,
            comment:$("textarea[name=comment]").val()
            },
        dataType:'text',
        success:function(msg){
            if(msg == 1){
                alert('등록했습니다.');
                uri = "bbsView.php";
                MemberListTable('','',page,uri,'','','','',idx);
            } else if(msg==-2){
                alert('수정권한이 없습니다.');
                return false;
            } else {
                alert('데이터를 다시 한번 확인하세요\n'+msg);
                return false;
            }
        },
        error: function(jqXHR, textStatus, errorThrown){
            alert("ajax error : " + textStatus + "\n" + errorThrown);
        }
    });
 
});
 
$(".comment_del").click(function(){
    var idx = $(this).parent().parent().attr('id');
    var page = $("input[name=p]").val();
    CommnetDelete(page,idx);
});
 
function CommnetDelete(curPage,idx){
    var verify = confirm('삭제하시겠습니까? \n 복구할 수 없습니다.');
    if (verify) {
        $.ajax({
            url:'bbsCommentDelete.php',
            type: 'POST',
            data: { 
                idx:encrypt.encrypt(idx) 
            },
            dataType:'text',
            success:function(msg){
                if (msg == 1) {
                    uri = "bbsView.php";
                    var idx = $("input[name=parentid]").val();
                    MemberListTable('','',curPage,uri,'','','','',idx);
                } else if(msg == -2){
                    alert('삭제 권한이 없습니다.');
                } else {
                    //alert('삭제중 오류가 발생하였습니다.\n'+msg);
                    alert('삭제중 오류가 발생하였습니다.');
                }
            },
            error: function(jqXHR, textStatus, errorThrown){
                alert("ajax error : " + textStatus + "\n" + errorThrown);
            }
        });
    }
 
}
 
</script>
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

PHP 위지윅 에디터를 이용하여 글쓰기를 할 경우 DB에 저장하는 코드 예시이다.

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

 

 
<?php
if(!isset($_SESSION)){
    session_start();
}
// 데이터가 제대로 넘어오는지 검증 목적
//echo '<pre>';print_r($_POST);echo '</pre>';
//echo '<pre>';print_r($_FILES);echo '</pre>';
//exit;
if(isset($_POST&& $_SERVER['REQUEST_METHOD'== "POST"){
    @extract($_POST);
    require_once 'path.php';// root 폴더를 기준으로 상대경로 자동 구하기
    require_once $g['path_config'].'config.php';
    require_once $g['path_config'].'dbconnect.php';
    require_once $g['path_class'].'dbDataClass.php';
    require_once $g['path_class'].'dbconnect.php';
    require_once $g['path_class'].'adminClass.php';
    require_once $g['path_class'].'bbsClass.php';
 
    $a = new adminClass();
    $b = new bbsClass();
    $d = new LegacyDBClass;
 
    if($mode == 'write'){
        $subject = trim($subject);
        $subject = preg_replace("/\s{2,}/"" "$subject); // s(공백문자)가 2회 이상을 1번으로 변경
        $subject = $d->XSSFilter($subject);
        $content = trim($content);
        $content = $b->html_purifier($content);
 
        $filename = NULL;
        if ($_FILES['file']['name']) { // 파일이 첨부되어 있으면
            $allowed_ext = array('jpg','bmp','png','gif','zip','doc','xls''xlsx');
            $tmpname = $_FILES['file']['tmp_name'];  // 임시파일명
            $realname  = $_FILES['file']['name'];  // 실제 파일명
            $filesize  = $_FILES['file']['size']; // 파일 크기
            $filetype  = $_FILES['file']['type']; // 파일 형태
 
            if (!$_FILES['file']['error']) { // 오류 없이 파일 업로드 성공
 
                $fileExt = $b->getExt($realname);  // 파일 확장자 구하는 함수
                if(!in_array($fileExt$allowed_ext)) {
                    @unlink($tmpname);
                    echo "-1"// 허용되지 않는 확장자
                    exit;
                } else {
                    // 새로운 파일을 업로드하면 기존 파일은 삭제 처리
                    if($idx > 0) {
                        $R = $d->getDbData('bbs_data','idx='.$idx,'*');
                        if($R['filename'!= NULL) {
                            $oldfilename = $g['path_root'].'files/infile/' . $R['filename'];
                            @unlink($oldfilename);
                        }
                    }
 
                    // 신규 파일 등록
                    if (is_uploaded_file($tmpname)){  // 임시파일이 디렉토리에 존재하는 경우
                        $filename = date("Ymd").md5(uniqid($tmpname)) .round(microtime(true)).'.'.$fileExt;
                        $uploadFile = $g['path_root'].'files/infile/' . $filename//change this directory
                        if(move_uploaded_file($tmpname$uploadFile)){ // 임시 디렉토리에 있던 파일을 실제 파일의 이름으로 전송
                            @chmod($uploadFile,0606); // 리눅스에서 파일 권한 변경
                        }
                    }
                }
            }
        }
 
        $userID = $_SESSION['userID'];
        $userNM = $_SESSION['userNM'];
        $html = 1;
        $depth = 1;
        $notice = 0;
        date_default_timezone_set('Asia/Seoul');
 
        if($idx == 0){
            $d_regis = date('YmdHis');
            $access_ip=$d->getClientIP();
            $QKEY = "subject,content,html,depth,filename,notice,d_regis,userID,userNM,ip";
            $QVAL = "'$subject','$content',$html,$depth,'$filename',$notice,'$d_regis','$userID','$userNM','$access_ip'";
            $d->getDbInsert('bbs_data',$QKEY,$QVAL);
            echo 1;
        } else {
            $idx = $d->XSSFilter($idx);
            // 등록자 여부 체크
            $R = $d->getDbData('bbs_data','idx='.$idx,'*');
            if($R['userID'=== $_SESSION['userID']){ // 등록자만 수정 가능하며 관리자도 수정은 불가
                $QSET="subject='".$subject."',";
                $QSET.="content='".$content."',";
                $QSET.="filename='".$filename."',";
                $QSET.="html='".$html."'";
                $QVAL="idx='".$idx."'";
 
                $d->getDbUpdate('bbs_data',$QSET,$QVAL);
                echo 2;
            } else {
                echo -2;
            }
        }
    } else if($mode == 'delete'){
        $d->getDbDelete('bbs_data',"idx='".$idx."'");
        echo 3// 삭제
    }
else {
    echo -1;
}
 
?>
 

여기서 더 신경써야 할 사항은

realname 을 DB 칼럼에 추가로 저장하는 것이다.

그래야 파일 다운로드할 때 실제 서버에 저장된 파일명과 다운로드되는 파일명이 달라서 해킹 시도 자체를 방지하는 것이 된다.

 

 

 

최근에는 그누보드 소스코드를 분석해보니 html_purifier 를 이용하여 악성코드를 걸러내고 있는 거 같다.

아래 코드는 위에 사용하는 Class 내에 있는 함수들 중에서 일부 발췌하였다.

<?php
class bbsClass extends DBDataClass {
 
    function conv_content($content){
        $source = array();
        $target = array();
 
        $source[] = "//";
        $target[] = "";
 
            $source[] = "/\n/";
            $target[] = "<br/>";
 
        // 테이블 태그의 개수를 세어 테이블이 깨지지 않도록 한다.
        $table_begin_count = substr_count(strtolower($content), "<table");
        $table_end_count = substr_count(strtolower($content), "</table");
        for ($i=$table_end_count$i<$table_begin_count$i++)
        {
            $content .= "</table>";
        }
 
        $content = preg_replace($source$target$content);
 
        $content = $this->html_purifier($content);
 
        $content = str_replace('<A href=','<a target="_blank" href=',$content);
        $content = str_replace('<a href=','<a target="_blank" href=',$content);
 
        return $content;
    }
 
    function html_purifier($html){
        // 절대경로 지정
        //include_once('/home/httpd/htdocs/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php');
        // 상대경로 지정
        include_once('../vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php');
        $config = HTMLPurifier_Config::createDefault();
        // data/cache 디렉토리에 CSS, HTML, URI 디렉토리 등을 만든다.
        $config->set('Attr.EnableID'false);
        $config->set('Attr.DefaultImageAlt''');
 
        // 인터넷 주소를 자동으로 링크로 바꿔주는 기능
        $config->set('AutoFormat.Linkify'true);
 
        // 이미지 크기 제한 해제 (한국에서 많이 쓰는 웹툰이나 짤방과 호환성 유지를 위해)
        $config->set('HTML.MaxImgLength'null);
        $config->set('CSS.MaxImgLength'null);
 
        $config->set('Core.Encoding''UTF-8'); // 인코딩
 
        $config->set('HTML.SafeEmbed'false);
        $config->set('HTML.SafeObject'false);
        $config->set('Output.FlashCompat'false);
        $config->set('HTML.SafeIframe'true);
 
        $config->set('Attr.AllowedFrameTargets'array('_blank'));
 
        $purifier = new HTMLPurifier($config);
        return $purifier->purify($html);
    }
 
 
    // 파일 확장자 추출
    function getExt($filename){
        $ext = substr(strrchr($filename,"."),1); // 공격방어 감지를 위해 콤마의 취지를 뒤에서부터 검색
        // strrchr(문자열, 찾을 문자) : 찾을 문자가 마지막으로 나온 위치부터 끝까지 반환 
        // strstr(문자열, 찾을 문자) : 찾을 문자열이 나온 처음 위치부터 끝까지 반환.
        $ext = strtolower($ext); // 확장자 소문자로 변환
        return $ext;
    }
 
}//end class boardClass
 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

PHP 위지윅 에디터를 추가하여 글쓰기하는 기능의 화면이다.

파일 첨부가 없는 경우에는 ajax 처리가 정상동작 한다.

하지만 파일 첨부가 있는 것은 인식 못하고 있다. 수정해 줘야 동작된다. 해답은 아래에....

<?php
error_reporting(0);
//*
ini_set("display_startup_errors"1);
ini_set("display_errors"1);
error_reporting(E_ALL);
// */
// 테이블 접속 처리
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php';
require_once $g['path_root'].'deviceChk.php';
if($mtype == 3)    require_once $g['path_root'].'ipFiltering.php';
require_once $g['path_config'].'config.php';
require_once $g['path_config'].'dbconnect.php';
require_once $g['path_class'].'dbDataClass.php';
require_once $g['path_class'].'dbconnect.php';
require_once $g['path_class'].'bbsClass.php';
 
$b = new bbsClass();
$d = new LegacyDBClass;
 
 
$idx = isset($_GET['idx']) ? $d->XSSFilter($_GET['idx']): 0;
$curPage = isset($_GET['p']) ? $d->XSSFilter($_GET['p']) : 1;
 
if($idx > 0){
    $R=$d->getDbData('bbs_data','idx='.(int)$idx,'*'); 
else {
    $R['idx'= 0;
    $R['subject'= '';
    $R['content'= '';
}
 
?>
<div class="table-responsive text-nowrap">
<form id="bbsForm" method="post" enctype="multipart/form-data" class="text-center border border-light p-5">
    <table class="table table-striped table-bordered table-hover table-sm" cellspacing="0" width="100%">
        <input type="hidden" name="mode" value="write" />
        <input type="hidden" name="p" value="<?php echo $curPage;?>" />
        <input type="hidden" name="idx" value="<?php echo $R['idx'];?>" />
        <tr>
            <td><input type="text" name="subject" id="subject" class="form-control mb-4" placeholder="제목을 입력하세요" value="<?=$R['subject'];?>"></td>
        </tr>
 
        <tr>
            <td>
            <?php if($skin_editor==1):?>
                <textarea name="content" id="content" class="form-control mb-4 summernote" rows="10">
                <?php echo htmlspecialchars($R['content']);?>
                </textarea>
            <?php endif;?>
            <?php if($skin_editor==2):?>
                <textarea name="content" id="ir1" class="form-control mb-4" rows="10" style="width:100%;display:none;">
                <?php
                    if(isset($R['content'])&& strlen($R['content'])){
                        echo $b->conv_content($R['content']);
                    } else { echo '';}
                ?>
                </textarea>
                <!-- 아래 스크립트를 추가 -->
                <script type="text/javascript" src="<?php echo $g['path_plugin']?>smarteditor/js/service/se2_insert.js"></script>
            <?php endif;?>
            </td>
        </tr>
        <tr>
            <td><input type=file name="file" id="file" class="form-control mb-4" size=30 /></td>
        </tr>
 
        <tr>
            <td>
            <button class="btn btn-info btn-block my-4" id="bbsRegister" type="submit">등록</button>
            </td>
        </tr>
    </table>
</form>
</div>
<script>
$('#bbsRegister').click(function(e){
    e.preventDefault();
    var subject = $('#subject');
    var curPage = $('input[name=p]').val();
    var uri = "bbsList.php";
 
    if(subject.val() ==''){
        alert('제목을 입력하세요');
        subject.focus();
        return false;
    }
 
    // 이부분에 smarteditor validation 검증
    var ir1_data = oEditors.getById['ir1'].getIR();
    var checkarr = ['<p>&nbsp;</p>','&nbsp;','<p><br></p>','<p></p>','<br>'];
    if(jQuery.inArray(ir1_data.toLowerCase(), checkarr) != -1){
        alert("내용을 입력하세요.");
        oEditors.getById["ir1"].exec('FOCUS');
        return false;
    }
 
    // id가 ir1인 에디터의 내용이 textarea에 적용됨
    oEditors.getById["ir1"].exec("UPDATE_CONTENTS_FIELD", []);
 
    $.ajax({
        url:'bbsWriteChk.php',
        type: 'POST',
        data: $("#bbsForm").serializeArray(),
        dataType:'text',
        success:function(msg){
            console.log(msg);
            if(msg == 1){
                alert('게시글을 등록했습니다.');
                MemberListTable('','',curPage,uri,'','','','',0);
            } else if(msg == 2){
                alert('게시글을 수정했습니다.');
                MemberListTable('','',curPage,uri,'','','','',0);
            } else if(msg==-1){
                alert('첨부할 수 없는 파일입니다.');
            } else if(msg==-2){
                alert('수정권한이 없습니다.');
            } else {
                alert('데이터를 다시 한번 확인하세요');
                return false;
            }
        },
        error: function(jqXHR, textStatus, errorThrown){
            alert("arjax error : " + textStatus + "\n" + errorThrown);
        }
    });
 
});
</script>
 

 

 

파일 첨부를 인식하는 코드

encType: 'multipart/form-data', // 필수
processData: false, // 필수
contentType: false, // 필수
data: formData, // 필수

<?php
error_reporting(0);
//*
ini_set("display_startup_errors"1);
ini_set("display_errors"1);
error_reporting(E_ALL);
// */
// 테이블 접속 처리
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php';
require_once $g['path_root'].'deviceChk.php';
if($mtype == 3)    require_once $g['path_root'].'ipFiltering.php';
require_once $g['path_config'].'config.php';
require_once $g['path_config'].'dbconnect.php';
require_once $g['path_class'].'dbDataClass.php';
require_once $g['path_class'].'dbconnect.php';
require_once $g['path_class'].'bbsClass.php';
 
$b = new bbsClass();
$d = new LegacyDBClass;
 
 
$idx = isset($_GET['idx']) ? $d->XSSFilter($_GET['idx']): 0;
$curPage = isset($_GET['p']) ? $d->XSSFilter($_GET['p']) : 1;
 
if($idx > 0){
    $R=$d->getDbData('bbs_data','idx='.(int)$idx,'*'); 
else {
    $R['idx'= 0;
    $R['subject'= '';
    $R['content'= '';
}
 
?>
<div class="table-responsive text-nowrap">
<form id="bbsForm" method="post" class="text-center border border-light p-5">
    <table class="table table-striped table-bordered table-hover table-sm" cellspacing="0" width="100%">
        <input type="hidden" name="mode" value="write" />
        <input type="hidden" name="p" value="<?php echo $curPage;?>" />
        <input type="hidden" name="idx" value="<?php echo $R['idx'];?>" />
        <tr>
            <td><input type="text" name="subject" id="subject" class="form-control mb-4" placeholder="제목을 입력하세요" value="<?=$R['subject'];?>"></td>
        </tr>
 
        <tr>
            <td>
            <?php if($skin_editor==1):?>
                <textarea name="content" id="content" class="form-control mb-4 summernote" rows="10">
                <?php echo htmlspecialchars($R['content']);?>
                </textarea>
            <?php endif;?>
            <?php if($skin_editor==2):?>
                <textarea name="content" id="ir1" class="form-control mb-4" rows="10" style="width:100%;display:none;">
                <?php
                    if(isset($R['content'])&& strlen($R['content'])){
                        echo $b->conv_content($R['content']);
                    } else { echo '';}
                ?>
                </textarea>
                <!-- 아래 스크립트를 추가 -->
                <script type="text/javascript" src="<?php echo $g['path_plugin']?>smarteditor/js/service/se2_insert.js"></script>
            <?php endif;?>
            </td>
        </tr>
        <tr>
            <td><input type=file name="file" id="file" class="form-control mb-4" size=30 /></td>
        </tr>
 
        <tr>
            <td>
            <button class="btn btn-info btn-block my-4" id="bbsRegister" type="submit">등록</button>
            </td>
        </tr>
    </table>
</form>
</div>
<script>
$('#bbsRegister').click(function(e){
    e.preventDefault();
    var idx = $('input[name=idx]').val();
    var subject = $('#subject');
    var curPage = $('input[name=p]').val();
    var uri = "bbsList.php";
 
    if(subject.val() ==''){
        alert('제목을 입력하세요');
        subject.focus();
        return false;
    }
 
    // 이부분에 smarteditor validation 검증
    var ir1_data = oEditors.getById['ir1'].getIR();
    var checkarr = ['<p>&nbsp;</p>','&nbsp;','<p><br></p>','<p></p>','<br>'];
    if(jQuery.inArray(ir1_data.toLowerCase(), checkarr) != -1){
        alert("내용을 입력하세요.");
        oEditors.getById["ir1"].exec('FOCUS');
        return false;
    }
 
    // id가 ir1인 에디터의 내용이 textarea에 적용됨
    oEditors.getById["ir1"].exec("UPDATE_CONTENTS_FIELD", []);
 
    // 파일 첨부 기능을 사용할 때에는 아래와 같이 해야 동작된다.
    var formData = new FormData();
    var files = $('#file')[0].files[0];
    formData.append('file',files);
    formData.append('mode','write');
    formData.append('idx', idx);
    formData.append('subject',subject.val());
    formData.append('content',ir1_data);
 
    $.ajax({
        url:'bbsWriteChk.php',
        type: 'POST',
        dataType:'text',
        encType: 'multipart/form-data'// 필수
        processData: false// 필수
        contentType: false// 필수
        data: formData, // 필수
        async: false,
        success:function(msg){
            console.log(msg);
            if(msg == 1){
                alert('게시글을 등록했습니다.');
                MemberListTable('','',curPage,uri,'','','','',0);
            } else if(msg == 2){
                alert('게시글을 수정했습니다.');
                MemberListTable('','',curPage,uri,'','','','',0);
            } else if(msg==-1){
                alert('첨부할 수 없는 파일입니다.');
            } else if(msg==-2){
                alert('수정권한이 없습니다.');
            } else {
                alert('데이터를 다시 한번 확인하세요');
                return false;
            }
        },
        error: function(jqXHR, textStatus, errorThrown){
            alert("arjax error : " + textStatus + "\n" + errorThrown);
        }
    });
 
});
</script>
 

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/2311

 

접속로그 통계 (신규, 중복 동시 처리)

접속로그 통계 구현을 위해서 로그를 쌓고 있는 테이블에서 데이터를 가공해야 한다. 접속로그 테이블에는 접속실패, 접속성공 등 모든 접속 시도 데이터를 저장해야 한다. CREATE TABLE `rb_accessLog`

link2me.tistory.com

SQL 데이터를 차트로 그리는 통계를 구현하는 코드를 예제로 작성했다.

 

statsClass.php

<?php
class statsClass {
    // 테이블 비우기
    function AccessLogTableTruncate($tablename){
        global $db;
        $sql ="TRUNCATE TABLE ".$tablename."";
        mysqli_query($db,$sql);
    }
 
    function rb_access_Stats($date){
        global $db;
        $sql ="select count(date) from rb_access_Stats where date='".$date."'"// 기록된 데이터 있는지 조회
        $result=mysqli_query($db,$sql);
        if($row=mysqli_fetch_row($result)){
            return $row[0];
        }
    }
 
    function AccessLogCnt($date){
        global $db;
        $sql ="select count(distinct userID),sum(hit) from rb_accessLog_tmp where date='".$date."'";
        $result=mysqli_query($db,$sql);
        if($row=mysqli_fetch_row($result)){
            return $row;
        }
    }
 
    // 통계데이터가 쌓이고 있는지 검사하는 로직
    function AccessLogIsData($date){
        global $db;
        $sql ="select userID from rb_accessLog_tmp where date='".$date."'";
        $result=mysqli_query($db,$sql);
        if($row=mysqli_fetch_row($result)){
            if($row[0== NULL) { // 데이터가 하나도 없으면
                return 0;
            } else {
                return 1;
            }
        }
    }
 
    // 년월 DB 추출
    function extract_YM(){
        global $db;
        $sql ="select distinct(YM) from rb_access_Stats order by YM DESC";
        $result = mysqli_query($db,$sql);
        return $result;
    }
 
    // 년월의 max date 구해서 배열 -> 문자열 만들기
    function maxdate_YM($ym){
        global $db;
        date_default_timezone_set('Asia/Seoul');
        if(empty($ym)){
            $cur_year = date("Y");
            $cur_month = date("m");
        } else {
            $cur_year = substr($ym,0,4);
            $cur_month = substr($ym,4,2);
        }
        // 현재 월의 마지막 날짜를 일단 선택하도록 처리하고, 년/월을 선택하면 자동으로 변경
        $last_date = date("t"mktime(001$cur_month1$cur_year));
        $R=array();
        for($i=0;$i < $last_date$i++){
            $R[$i]=$i+1;
        }
        return $R// 배열로 반환
    }
 
 
    function dailyCnt_value($ym){
        global $db;
        date_default_timezone_set('Asia/Seoul');
        if(empty($ym)){
            $ym = date("Ym"); // 입력이 없으면 현재월의 데이터를 추출하라.
            $cur_year = date("Y");
            $cur_month = date("m");
        } else {
            $cur_year = substr($ym,0,4);
            $cur_month = substr($ym,4,2);
        }
        $R=array();
        // 현재 월의 마지막 날짜를 일단 선택하도록 처리하고, 년/월을 선택하면 자동으로 변경
        $last_date = date("t"mktime(001$cur_month1$cur_year));
        for($i=1;$i <= $last_date$i++){
            $R[$i]='';
        }
        $sql ="select day,dailyCnt from rb_access_Stats ";
        $sql.="where YM='".$ym."'";
        $result = mysqli_query($db,$sql);
        while($row=mysqli_fetch_row($result)){
            // DB에 저장되는 날짜가 01, 02로 저장되는 것을 1, 2로 출력되도록 처리
            $R[intval($row[0])] = $row[1];
        }
        return $R// 배열로 반환
    }
 
    function userCnt_value($ym){
        global $db;
        date_default_timezone_set('Asia/Seoul');
        if(empty($ym)){
            $ym = date("Ym"); // 입력이 없으면 현재월의 데이터를 추출하라.
            $cur_year = date("Y");
            $cur_month = date("m");
        } else {
            $cur_year = substr($ym,0,4);
            $cur_month = substr($ym,4,2);
        }
        $R=array();
        // 현재 월의 마지막 날짜를 일단 선택하도록 처리하고, 년/월을 선택하면 자동으로 변경
        $last_date = date("t"mktime(001$cur_month1$cur_year));
        for($i=1;$i <= $last_date$i++){
            $R[$i]='';
        }
        $sql ="select day,IDCnt from rb_access_Stats ";
        $sql.="where YM='".$ym."'";
        $result = mysqli_query($db,$sql);
        while($row=mysqli_fetch_row($result)){
            // DB에 저장되는 날짜가 01, 02로 저장되는 것을 1, 2로 출력되도록 처리
            $R[intval($row[0])] = $row[1];
        }
        return $R// 배열로 반환
    }
 
}

 

 

stats_db.php

<?php
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
//require_once $g['path_root'].'sessionChk.php'; // 세션 체크
if(isset($_POST['ym'])){
    require_once $g['path_config'].'dbconnect.php';
    require_once $g['path_class'].'statsClass.php';
 
    $s = new statsClass;
 
    $ym = $_POST['ym'];
    $data1 = $s->dailyCnt_value($ym);
    $data2 = array_values($s->userCnt_value($ym));
    $ticks = $s->maxdate_YM($ym);
    $R = array('data1'=>$data1,'data2'=>$data2'ticks'=>$ticks);
    echo json_encode($R);
}
?>

 

 

차트 출력 파일 : accessStats.php

<?php
error_reporting(0);
//*
ini_set("display_startup_errors"1);
ini_set("display_errors"1);
error_reporting(E_ALL);
// */
 
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_config'].'config.php';
require_once $g['path_config'].'dbconnect.php';
require_once $g['path_class'].'statsClass.php';
 
$s = new statsClass();
$R = $s->extract_YM();
 
date_default_timezone_set('Asia/Seoul');
if(isset($_GET['ym'])){
    $ym = $_GET['ym'];
else {
    if(date("d"=== '01'){ // 1 일에는 전월 출력하도록 설정
        $ym = date("Ym"mktime(0,0,0,date("m")-1, date("d"), date("Y")));
    } else {
        $ym = date("Ym");
    }
}
$ticks = json_encode($s->maxdate_YM($ym));
$line1 = json_encode(array_values($s->dailyCnt_value($ym)));
$line2 = json_encode(array_values($s->userCnt_value($ym)));
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<?php header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1 ?>
<?php header('Pragma: no-cache'); // HTTP 1.0 ?>
<?php header('Expires: 0'); // Proxies ?>
<?php header('X-Frame-Options:SAMEORIGIN',true);?>
<?php header('X-Content-Type-Options: nosniff',true);?>
<?php header('X-XSS-Protection:1;mode=block',true);?>
<title>일일 접속 통계</title>
<link rel="shortcut icon" href="/img/etc/favicon.ico">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css">
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/mdb.lite.css" rel="stylesheet">
<link href="/css/jquery-ui.css" rel="stylesheet">
</head>
<body>
 
<div class="row">
    <div class="col-md-2">
    </div>
    <div class="col-md-8 col-xl-8 col-lg-8">
        <p class="h5 my-4">일일 접속 통계</p>
        <div class="card shadow mb-4">
            <canvas id="myChart"></canvas>
        </div>
            <div class="float-right info mt-4">
            <select class="browser-default custom-select" name="month" id="selectmonth">
            <option value="">년월 선택</option>
            <?php
                while ($row = mysqli_fetch_array($R)){
                    echo "<option value='".$row[0]."' ";
                    if($row[0=== $ymecho "selected='selected'";
                    echo ">".$row[0]."</option>\n";
                }
            ?>
            </select>
            </div>
    </div>
</div>
<script type="text/javascript" src="/js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="/js/popper.min.js"></script>
<script type="text/javascript" src="/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/js/mdb.lite.min.js"></script>
<script type="text/javascript" src="/js/jquery-ui.js"></script>
<script type="text/javascript" src="/js/jsencrypt.min.js"></script>
<script type="text/javascript" src="/js/user/user.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.min.js"></script>
<script>
var xAxis = <?php echo $ticks;?>;
var adIDData1 = <?php echo $line1;?>;
var adIDData2 = <?php echo $line2;?>;
 
drawPlot(adIDData1, adIDData2, xAxis);
 
function drawPlot(adIDData1, adIDData2, xAxis){
    var ctx = document.getElementById("myChart").getContext('2d');
    var myChart = new Chart(ctx, {
    type: 'bar',
    data: {
      labels: xAxis ,
      datasets: [
          {
            label: '로그인수',
            data: adIDData1 ,
            backgroundColor: 'rgba(50, 169, 132, 0.5)',
            borderColor: 'rgba(50,169,132,1)',
            borderWidth: 1
          },
          {
            label: '사용자수',
            data: adIDData2 ,
            backgroundColor: 'rgba(54, 162, 235, 0.5)',
            borderColor:'rgba(54, 162, 235, 1)',
            borderWidth: 1
          },
 
        ]
    },
    options: {
      scales: {
        yAxes: [{
          ticks: {
            beginAtZero: true
          }
        }]
      }
    }
    });
}
 
$('#selectmonth').on('change',function(){
    if(this.value !== ""){
        var optVal=$(this).find(":selected").val();
        $.post('stats_db.php',{ ym:optVal },function(msg){
            //console.log(msg); // 배열 형태로 넘어오는지 확인 목적
            var jsonObj = $.parseJSON(msg); // JSON 문자열을 JavaScript object 로 반환
            var data1 = $.map(jsonObj.data1, function(el) { return el });
            var data2 = jsonObj.data2;
            var xAxis = jsonObj.ticks;
            drawPlot(data1,data2,xAxis);
        });
    }
});
</script>
</body>
</html>
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

로그인 6개월 차단하는 기능을 구현해 달라는 요청으로 간단 구현한 사항 적어둔다.

 

<?php
error_reporting(0);
/*
ini_set("display_startup_errors", 1);
ini_set("display_errors", 1);
error_reporting(E_ALL);
// */
 
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php'// 세션 체크
require_once $g['path_root'].'ipFiltering.php';
require_once $g['path_class'].'dbconnect.php';
$a = new DBDataClass();
 
date_default_timezone_set('Asia/Seoul');
$today = date("Y-m-d"); // 오늘 날짜
 
// getDbUpdate($table, $set, $params, $where);
// $access // 이용권한 없음(0), 로그인 허용(1), 가입승인대기(2)
$rows = $a->getDbresult('members','admin=0','date, idx, regdate');
foreach($rows as $R):
    if($R['date'!== NULL) {
        // 최근접속일자
        $from_day = substr($R['date'],0,4)."-".substr($R['date'],4,2)."-".substr($R['date'],6,2);
        echo $from_day.'<br />';
        $from = new DateTime($from_day);
        $to = new DateTime($today);
        $day_diff = $from->diff($to)->days;
        if($day_diff > 180){ // 6개월동안 로그인한 기록이 없으면
            $SET ="access=0"// 접속을 차단시킨다.
            $params=array($R['idx']);
            $a->getDbUpdate('members',$SET,$params,'idx=?');
            echo 'idx : '.$R[1].'<br />';
        }
    } else {
        // 가입일자
        $from_day = substr($R['regdate'],0,4)."-".substr($R['regdate'],4,2)."-".substr($R['regdate'],6,2);
        echo $from_day.'<br />';
        $from = new DateTime($from_day);
        $to = new DateTime($today);
        $day_diff = $from->diff($to)->days;
        if($day_diff > 365){
            $SET ="access=2";
            $params=array($R['idx']);
            $a->getDbUpdate('members',$SET,$params,'idx=?');
            echo 'no access : '.$R[1].'<br />';
        }
    }
endforeach;
?>
 

 

 

<?php
class DBDataClass {
    protected $db// 변수를 선언한 클래스와 상속받은 클래스에서 참조할 수 있다.
 
    public function __construct() {
        $this->dbConnect();
        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.
    }
 
    private function dbConnect() {
        require_once 'dbinfo.php';
        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());
        }
    }
 
    /*
     $sql = "INSERT INTO users (name, surname, sex) VALUES (?,?,?)";
     $stmt= $pdo->prepare($sql);
     $stmt->execute(array($name, $surname, $sex));
    */
    public function recur_quot($cnt) {
        $R = array();
        for ($i = 0$i < $cnt$i++) {
            array_push($R"?");
        }
        return implode(","$R); // 배열을 문자열로
    }
 
    // 신규 자료 추가(ok)
    function putDbInsert($table$key$params) {
        try {
            $this->db->beginTransaction();
            $sql = "insert into " . $table . " (" . $key . ") values(" . $this->recur_quot(count($params)) . ")";
            $stmt = $this->db->prepare($sql);
            $status = $stmt->execute($params); // $params 는 배열 값
            $this->db->commit();
            return 1;
        } catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : " . $pex->getMessage();
            return 0;
        }
    }
 
 
    function getDbUpdate($table$set$params$where) {
        $sql = "update " . $table . " set " . $set . ($where ? ' where ' . $where : '');
        try {
            $this->db->beginTransaction();
            $stmt = $this->db->prepare($sql);
            $status = $stmt->execute($params);
            $this->db->commit();
            return 1;
        } catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : " . $pex->getMessage();
            return 0;
        }
    }
 
    public function putDbArray($sql$params) {
        // $params : array 를 사용해야 한다.
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(); //foreach 문과 연동하여 결과처리
    }
 
    
    // 검색조건에 일치하는 데이터 가져오기
    public function getDbData($table$where$column$returntype = '') {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        if ($returntype == 1) {
            return $stmt->fetch(PDO::FETCH_ASSOC);
        } else {
            return $stmt->fetch();
        }
    }
 
    // DB Query result 함수
    function getDbresult($table,$where,$column) {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        //echo $sql.'<br />';
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        return $stmt->fetchAll();
    }
 
    // table 결과 조회 용도
    public function getDbArray($table$where$column$orderby$rowsPage$curPage) {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '') . ($orderby ? ' order by ' . $orderby : '') . ($rowsPage ? ' limit ' . (($curPage - 1* $rowsPage) . ', ' . $rowsPage : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        return $stmt->fetchAll(); //foreach 문과 연동하여 결과처리 하면 됨
    }
 
    //SQL필터링
    public function getSqlFilter($sql) {
        //$sql = preg_replace("/[\;]+/","", $sql); // 공백은 제거 불가
        return $sql;
    }
 
?>
 

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

자바스크립트 코드로 동적으로 태그를 추가하는 예제이다.

 

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script defer src="./main.js"></script>
</head>
<body>
<h1>Hello world!</h1>
<ul></ul>
</body>
</html>

 

main.js

<script>
// for(시작조건;종료조건;변화조건) {}
 
const ulEl = document.querySelector('ul')
 
for(let i =0 ; i < 3; i++){
    const li = document.createElement('li')
    li.textContent = `list-${i+1}`
    li.addEventListener('click'function () {
        console.log(li.textContent)
    })
    ulEl.appendChild(li)
}
</script>

 

728x90
블로그 이미지

Link2Me

,
728x90

https://mdbootstrap.com/docs/b4/jquery/javascript/charts/

 

Bootstrap Charts Guideline - examples & tutorial

Bootstrap charts are graphical representations of data. Charts come in different sizes and shapes: bar, line, pie, radar, polar and more.

mdbootstrap.com

기본적인 예제가 나와 있다.

이 예제를 가지고 DB와 연동하여 처리하는 방법이다.

 

 

<?php
error_reporting(0);
//*
ini_set("display_startup_errors"1);
ini_set("display_errors"1);
error_reporting(E_ALL);
// */
 
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php'// 세션 체크
require_once $g['path_config'].'config.php';
require_once $g['path_config'].'dbconnect.php';
require_once $g['path_class'].'statsClass.php';
$b = new statsClass();
 
$link_url = "Stats.php"// 현재 실행중인 파일명 가져오기
 
$ageData = $b->ageCnt_array();
 
?>
<p class="h4 mb-4">통계</p>
<div class="row">
    <div class="col-md-8 col-xl-8 col-lg-7">
        <div class="col-md-11">
            <canvas id="Trend1Chart"></canvas>
        </div>
        <div class="col-md-11  mb-4">
            
        </div>
        <div class="col-md-11">
            <canvas id="Trend3Chart"></canvas>
        </div>
    </div>
 
    <div class="col-md-4 col-xl-4 col-lg-5">
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">연령대</h6>
            </div>
            <div class="card-body">
                <div class="chart-pie pt-4">
                    <canvas id="ageChart"></canvas>
                </div>
            </div>
        </div>
 
    </div>
</div>
 
<script>
  var ctxD = document.getElementById("ageChart").getContext('2d');
  var ageKey = <?php echo json_encode(array_keys($ageData));?>;
  var ageData = <?php echo json_encode(array_values($ageData));?>;
  var myLineChart = new Chart(ctxD, {
    type: 'doughnut',
    data: {
      labels: ageKey,
      datasets: [{
        data: ageData,
        backgroundColor: ["#F7464A""#46BFBD""#FDB45C""#949FB1""#4D5360"],
        hoverBackgroundColor: ["#FF5A5E""#5AD3D1""#FFC870""#A8B3C5""#616774"]
      }]
    },
    options: {
      responsive: true
    }
  });
</script>

 

PHP 배열을 Javascript 변수로 사용하는 방법이 핵심이다.

 

PHP에서 생성된 배열을 자바스크립트에서 사용하는 방법

<?php
$arr_php = array("a","b","c","d");
?>

위 값처럼 선언된 배열을 javascript에서 받아서 사용하려면?

var arr_js = <?php echo json_encode($arr_php)?>;

json_encode 해서 받으면 된다.

json_encode 함수를 사용하면 1차원 배열이든 2차원 배열이든 간단하게 자바스크립트 배열로 만들어 준다.

array_keys($array)  : 배열의 키값을 배열로 반환해주는 함수

array_values($array)  : 배열의 값을 배열로 반환해주는 함수
 
아래 statsClass 의 함수 구현한 것을 살펴보면 PHP 배열을 key, value 로 된 형태로 반환하는 것을 알 수 있다.
<?php
class statsClass {
    function ageCnt_array(){
        global $db;
        $sql = "select age, sum(Cnt) from stats group by age";
        $result = mysqli_query($db,$sql);
        $R=array();
        $TCNT = $this->ageTotalCnt();
        while($row = mysqli_fetch_row($result)){
            $row[0= $row[0].'('.$this->Percentage($row[1],$TCNT).'%)';
            $R[$row[0]] = $row[1];
        }
        return $R// 배열로 반환
    }
 
    function ageTotalCnt(){
        global $db;
        $sql = "select sum(Cnt) from stats";
        $result = mysqli_query($db,$sql);
        if($row = mysqli_fetch_row($result)){
            return $row[0];
        }
    }
 
    function Percentage($val,$TCNT){
        $percent = $val / $TCNT * 100;
        return round($percent,1);
    }
 
}
?>

 

 

728x90
블로그 이미지

Link2Me

,