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/>';
            }
            
        }
    }
 
?>
 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

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

 

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

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

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

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

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

 

 

 

블로그 이미지

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>

 

블로그 이미지

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];
    }    
}
 
?>

 

 

 

 

블로그 이미지

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;
}
 

 

 

 

 

 

 

블로그 이미지

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;
}
 
?>
 

 

 

블로그 이미지

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>
 

 

 

블로그 이미지

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
 

 

 

 

블로그 이미지

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>
 

 

 

 

 

블로그 이미지

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>
 

 

 

블로그 이미지

Link2Me

,
728x90

서버 보안 설정 사항 중 몇가지를 적어둔다.

 

### 패스워드 잠금횟수 설정
vim /etc/pam.d/system-auth
auth        required       pam_tally2.so deny=5 unlock_time=120 no_magic_root account required pam_tally2.so no_magic_root reset
 
#account     required     pam_unix.so 다음 줄에 추가한다.
account     sufficient    pam_tally2.so
 
 
vim /etc/pam.d/password-auth
#auth        required      pam_tally2.so deny=5 unlock_time=120
auth        required      pam_tally2.so deny=5 unlock_time=120 no_magic_root account required pam_tally2.so no_magic_root reset
 
#account     required     pam_unix.so 다음 줄에 추가한다.
account     sufficient    pam_tally2.so
 
### /etc/hosts 파일 소유자 및 권한 설정
chown root /etc/hosts 
chmod 600 /etc/hosts
 
### /etc/(x)inetd.conf 파일 소유자 및 권한 설정
ls -al /etc/xinetd.conf
chown root /etc/xinetd.conf 
chmod 600 /etc/xinetd.conf 
 
# 파일이 존재하지 않으면
ls -alR /etc/xinetd.d/
chown -R root /etc/xinetd.d/ 
chmod -600 /etc/xinetd.d/ 
 
### /etc/syslog.conf 파일 소유자 및 권한 설정
ls -al /etc/rsyslog.conf
chown root /etc/rsyslog.conf
chmod 640 /etc/rsyslog.conf
 
### 정책에 따른 시스템 로깅 설정
vim /etc/rsyslog.conf
*.alert                                                 /dev/console
*.emerg   *
 
# rsyslog 서비스 재시작
systemctl restart rsyslog
 
 
### cron 파일 소유자 및 권한설정
ls -/usr/bin/crontab
chmod 750 /usr/bin/crontab
# crontab 파일 권한 확인
ls -al /usr/bin/crontab
 
# cron 관련 설정파일 소유자 및 권한 설정
chown root /etc/cron.deny
chmod 640 /etc/cron.deny
ls -al /etc/cron.deny
 
# 파일이 존재하면 아래 명령어 실행
ls /etc/cron.d/
chown root /etc/cron.d/cron.allow
chmod 640 /etc/cron.d/cron.allow
chown root /etc/cron.d/cron.deny
chmod 640 /etc/cron.d/cron.deny
 
 
### FTP 계정 shell 제한
cat /etc/passwd
# passwd 파일 내 로그인 쉘 설정이 /bin/false 가 아닌 경우 보안 설정
usermod -/bin/false ftp
 
 
### at 파일 소유자 및 권한 설정
ls -/usr/bin/at
chmod 0750 /usr/bin/at
 
chown root /etc/at.allow
chmod 640 /etc/at.allow
chown root /etc/at.deny
chmod 640 /etc/at.deny
 
# 파일 권한 및 사용자 확인
ls -al /etc/at.allow
ls -al /etc/at.deny
 
 
### SSH, SFTP 로그 기록 설정(상)
※ SSH
#1. /etc/profile 파일에 아래 설정 수정 및 추가
vim /etc/profile
...
function logging
{
    stat="$?"
    cmd=$(history|tail -1)
    if [ "$cmd" != "$cmd_old" ]; then
    logger -p local1.notice "[2] STAT=$stat"
    logger -p local1.notice "[1] PID=$$, PWD=$PWD, CMD=$cmd"
    fi
    cmd_old=$cmd
}
trap logging DEBUG
 
#2. rsyslog.conf 파일에 아래 설정 추가
vim /etc/rsyslog.conf
...
local1.*                                                /var/log/secure
 
#3. 변경된 환경변수 적용
source /etc/profile
 
#4. rsyslog 서비스 재시작
systemctl restart rsyslog
 
 
# sshd_config 파일 내 sftp 설정에 "-f AUTHPRIV -l VERBOSE" 설정 추가
vim /etc/ssh/sshd_config
 
# shift + g 를 눌러 최하단으로 이동한다.
 
#Subsystem sftp /usr/libexec/openssh/sftp-server
# 아래와 같이 추가한다.
Subsystem sftp /bin/false
Match LocalPort 2222  
      ForceCommand internal-sftp -f AUTHPRIV -l VERBOSE
 
# sshd 서비스 재시작
systemctl restart sshd
 
### root 계정 원격 접속 제한(상)
/PermitRootLogin 으로 찾아서
PermitRootLogin no 로 변경한다.
 
# sshd 서비스 재시작
systemctl restart sshd
# 만약의 경우를 대비하여 콘솔창을 닫으면 안된다.
# sftp 로 root 접속하는 경우 접속이 안된다.
 

 

 

 

'Web 프로그램 > Web Security' 카테고리의 다른 글

HTML Purifier 사용법 예시  (0) 2023.02.28
remote IP address  (0) 2021.03.24
파일 다운로드 공격 방지  (0) 2019.07.05
파일 업로드 공격 방지  (0) 2019.06.21
Cross-Site Scripting (XSS) 방지  (0) 2019.06.20
블로그 이미지

Link2Me

,
728x90

Web Root 디렉토리에서 설치를 위해 아래와 같이 실행한다.

# vendor/ezyang/htmlpurifier/library/ 에 자동 설치된다.
composer require ezyang/htmlpurifier

 

HTML Purifier는 해외의 보안 전문가들로부터 철저하게 검증받은 필터링 라이브러리이다.
페이지 표시할 때마다 매번 필터링하지 말고, DB에 저장하기 전에 한 번만 필터링한다.
게시물 제목이나 댓글처럼 HTML이 필요없는 경우 그냥 htmlspecialchars 또는 strip_tags 사용한다.
첨부파일 업로드를 사용한 XSS 공격은 게시물 내용과 별도로 막아줘야 한다.

 

https://stackoverflow.com/questions/2677578/php-html-purifier-hello-world-world-tutorial-striptags

 

PHP - HTML Purifier - hello w<o>rld/world tutorial striptags

I am just looking into using HTML Purifier to ensure that a user-inputed string (that represents the name of a person) is sanitized. I do not want to allow any html tags, script, markup etc - I just

stackoverflow.com

 

<?php
include_once( 'htmlpurifier/htmlpurifier/library/HTMLPurifier.auto.php');
?>
<form method="post">
<input type="text" name="fname" placeholder="first name"><br>
<input type="text" name="lname" placeholder="last name"><br>
<input type="submit" name="submit" value="submit">
</form>
        
<?php
if(isset($_POST['submit']))
{
    $fname=$_POST['fname'];
    $lname=$_POST['lname'];
    
    $config = HTMLPurifier_Config::createDefault();
    $purifier = new HTMLPurifier($config);
    $fname = $purifier->purify($fname);
    
    $config = HTMLPurifier_Config::createDefault();
    $purifier = new HTMLPurifier($config);
    $lname = $purifier->purify($lname);
 
    echo "First name is: ".$fname."<br>";
    echo "Last name is: ".$lname;
}
?>

 

본격적으로 사용해 보기 위해서 자료를 검색하고 분석하는 중이다.

'Web 프로그램 > Web Security' 카테고리의 다른 글

CentOS 7 서버 보안  (0) 2023.03.03
remote IP address  (0) 2021.03.24
파일 다운로드 공격 방지  (0) 2019.07.05
파일 업로드 공격 방지  (0) 2019.06.21
Cross-Site Scripting (XSS) 방지  (0) 2019.06.20
블로그 이미지

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;
    }
 
?>
 

 

 

 

 

블로그 이미지

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>

 

블로그 이미지

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);
    }
 
}
?>

 

 

블로그 이미지

Link2Me

,
728x90

strtotime()는 두 날짜를 UNIX 시간으로 변환하고 그로부터 초 수를 계산한다.

먼저 new DateTime()을 사용하여 날짜를 선언한다. 
그런 다음 첫 번째 날짜의 DateInterval() 오브젝트 diff()를 사용하여 차이의 정수 값을 가져오고 두 번째 날짜를 매개 변수로 전달한다. 년은 y 객체를 사용, 월은 m 객체, 일은 d 객체를 사용하면 된다.

<?php
$firstDate  = new DateTime("2022-01-01");
$secondDate = new DateTime("2022-04-24");
$intvl = $firstDate->diff($secondDate);
 
echo $intvl->y . " 년, " . $intvl->m." 월 and ".$intvl->d." 일";
 
// 날짜수를 계산
echo $intvl->days . " days ";
?>

 

DB와 연동하여 신청일자, 폐지일자 데이터를 가져와서 유형을 구분하고 DB 저장하기 위한 로직 예제이다.

<?php
 
$sql = "select * from customer ";
$result =  mysqli_query($db$sql);
while($R = mysqli_fetch_array($result)){
 
    $regdate = preg_replace("/[^0-9]/","",$R['reg_date']);
    $revodate  = preg_replace("/[^0-9]/","",$R['revo_date']);
    $datediff = getDateDiff($regdate$revodate);
 
}
mysqli_close($db); // 디비 접속 닫기
 
 
// 문자열 날짜를 입력받아 날짜 차이를 계산한다.
function getDateDiff($regDate$revoDate){
    date_default_timezone_set('Asia/Seoul');
    $date1 = new DateTime(date("Y-M-d", strtotime($regDate)));
    if($revoDate == '99991231'){
        $date2 = new DateTime(date("Y-M-d")); // 유지시 현재 날짜 반환
    } else {
        $date2 = new DateTime(date("Y-M-d", strtotime($revoDate)));
    }
 
    $intvl = $date2->diff($date1);
    $GapDate = $intvl->days;
    return DateType($GapDate);
}
 
// 날짜 타입 구분
function DateType($GapDate){
    
    if($GapDate >= 180){
        return "6개월";
    } else if($GapDate >= 90){
        return "3개월";
    } else if($GapDate >= 60){
        return "2개월";
    } else if($GapDate >= 30){
        return "1개월";
    } else if($GapDate >= 10){
        return "10일이상";
    } else {
        return "10일이내";
    }
}
 
?>

 

 

 

 

블로그 이미지

Link2Me

,
728x90

먼저 https://getbootstrap.com/docs/4.6/components/pagination/ 사이트에서 기본적인 Paging 처리에 대한 예제를 살펴본다.

또는 https://www.w3schools.com/bootstrap4/bootstrap_pagination.asp 에서 Pagination 항목을 선택해서 봐도 된다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Bootstrap4 Pagination Example</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<?php
/*
    // bootstrap4 에서 px 에서 em 으로 변경
    m : Margin, p : Padding
    t : top, b : bottom, l : left, r : right,
    x : x축 -> left , right, y : y축 -> top , bottom
 
    0 : 0
    1 : .25rem( font-size가 16px이면, 4px) 크기
    2 : .5rem( font-size가 16px이면, 8px) 크기
    3 : 1rem( font-size가 16px이면, 16px) 크기
    4 : 1.5rem( font-size가 16px이면, 24px) 크기
    5 : 3rem( font-size가 16px이면, 48px) 크기
    auto : margin 자동 세팅
 
    n : negative을 의미
    n1 : -.25rem( font-size가 16px이면, -4px) 크기
    n2 : -.5rem( font-size가 16px이면, -8px) 크기
    n3 : -1rem( font-size가 16px이면, -16px) 크기
    n4 : -1.5rem( font-size가 16px이면, -24px) 크기
    n5 : -3rem( font-size가 16px이면, -48px) 크기
 
    my- = it sets margin-left and margin-right at the same time on y axes
*/
?>
<div class="container mt-2">
    <div id="paging" class="mt-2">
        <ul class="pagination">
            <li class="page-item"><a class="page-link" href="#">Previous</a></li>
            <li class="page-item"><a class="page-link" href="#">1</a></li>
            <li class="page-item"><a class="page-link" href="#">2</a></li>
            <li class="page-item"><a class="page-link" href="#">3</a></li>
            <li class="page-item"><a class="page-link" href="#">Next</a></li>
        </ul>
    </div>
</div>
 
</body>
</html>

 

 

ul 태그의 pagination 클래스, li 태그의 page-item 클래스 등 페이징 처리를 위한 구조를 보고 PHP 코드로 변환 준비를 한다.

PHP 코드는 MySQL DB와 연동하여 전제 페이지 수, 현재 페이지 등의 정보를 고려하여 코드를 구현한다.

<?php
class bbsClass extends DBDataClass {
 
    function PageLinkView($link_url$totalcnt$rowsPage$curPage$m) {
        echo '<div id="paging">';
        echo '<ul class="pagination">';
        $Info = $this->PageList($totalcnt$rowsPage$curPage'');
        if ($Info['current_block'> 2) {
            echo "<li class='page-item'><a class='page-link' href='" . $link_url . "?p=1&$m'>◀</a></li> ";
        }
        if ($Info['current_block'> 1) {
            echo "<li class='page-item'><a class='page-link' href='" . $link_url . "?p=" . $Info['prev'] . "&$m'>◁</a></li> ";
        }
        foreach ($Info['current'as $w) {
            if ($curPage == $w) {
                echo "<li class='act'><a class='page-link' href='" . $link_url . "?p=" . $w . "&$m'><span style='color:red;'>" . $w . "</span></a></li> ";
            } else {
                echo "<li class='page-item'><a class='page-link' href='" . $link_url . "?p=" . $w . "&$m'>" . $w . "</a></li> ";
            }
        }
        if ($Info['current_block'< ($Info['total_block'])) {
            echo "<li class='page-item'><a class='page-link' href='" . $link_url . "?p=" . $Info['next'] . "&$m'>▷</a></li> ";
        }
        if ($Info['current_block'< ($Info['total_block'- 1)) {
            echo "<li class='page-item'><a class='page-link' href='" . $link_url . "?p=" . $Info['totalPage'] . "&$m'>▶</a></li> ";
        }
        echo '</ul>';
        echo '</div>';
    }
 
    // $curPage : 현재 페이지, $totalcnt : 총 게시물수
    // $block_limit : 한 화면에 뿌려질 게시글 개수
    function PageList($totalcnt,$rowsPage,$curPage,$block_limit) {
        $block_limit = $block_limit ? $block_limit : 10;  // 한 화면에 보여줄 개수 기본 10으로 설정
 
        // 총 페이지수 구하기
        $totalPage = ceil($totalcnt/$rowsPage);
        if($totalPage == 0) {
            ++$totalPage;
        }
        $total_block = ceil($totalPage / $block_limit); //전체 블록 갯수
 
        $curPage = $curPage ? $curPage : 1// 현재 페이지
 
        // 현재 블럭 : 화면에 표시될 페이지 리스트
        $current_block=ceil($curPage/$block_limit);
        // 현재 블럭에서 시작페이지
        $fstPage = (((ceil($curPage/$block_limit)-1)*$block_limit)+1);
        // 현재 블럭에서 마지막 페이지
        $endPage = $fstPage + $block_limit -1;
        if($totalPage < $endPage) {
            $endPage = $totalPage;
        }
 
        // 시작 바로 전 페이지
        $prev_page = $fstPage - 1;
        // 마지막 다음 페이지
        $next_page = $endPage + 1;
 
        foreach(range($fstPage$endPageas $val) {
            $row[] = $val;
        }
        // 배열로 결과를 돌려준다.
        return array(
            'total_block' => $total_block,
            'current_block' => $current_block,
            'totalPage' => $totalPage,
            'fstPage' => $fstPage,
            'endPage' => $endPage,
            'prev' => $prev_page,
            'next' => $next_page,
            'current' => $row
        );
    }
 
    //총페이지수
    function getTotalPage($num,$rec){
        return @intval(($num-1)/$rec)+1;
    }
 
}//end class boardClass

 

 

아래 Class 는 PDO(PHP Data Object) 기반으로 구현한 코드이다.

Legacy PHP 기반의 Class 는 https://link2me.tistory.com/1112 를 참조하면 된다.

 
<?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 getDbDelete($table$where$params) {
        if ($this->isDataExisted($table$where$params)) {
            try {
                $this->db->beginTransaction();
                $sql = "delete from " . $table . ($where ? ' where ' . $where : '');
                $stmt = $this->db->prepare($sql);
                $status = $stmt->execute($params);
                if ($status) {// action 실행여부에 대한 결과이지 실제 데이터 삭제와는 무관하네.
                    $this->db->commit();
                    return 1// 삭제 성공
                } else {
                    return 0// 삭제 실패
                }
            } catch (PDOException $pex) {
                $this->db->rollBack();
                echo "에러 : " . $pex->getMessage();
            }
        } else {
            return 0// 삭제할 데이터가 없음
        }
 
    }
 
    // 삭제할 데이터의 존재 유무 파악
    public function isDataExisted($table$where$params) {
        $sql = 'select * from ' . $table . ($where ? ' where ' . $where : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        if ($row = $stmt->fetch()) {
            return $row;
        } else {
            return false;
        }
    }
 
    //SQL필터링
    public function getSqlFilter($sql) {
        //$sql = preg_replace("/[\;]+/","", $sql); // 공백은 제거 불가
        return $sql;
    }
 
    // Simple function to handle PDO prepared statements
    function sql($sql$params) {
        // $params : array 를 사용해야 한다.
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(); // foreach 문으로 결과 처리
    }
 
    // 검색조건에 일치하는 데이터 가져오기
    public function putDbData($sql,$params$returntype = '') {
        // $params : array 를 사용해야 한다.
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        if ($returntype == 1) {
            return $stmt->fetch(PDO::FETCH_ASSOC);
        } else {
            return $stmt->fetch();
        }
    }
 
    public function putDbArray($sql$params) {
        // $params : array 를 사용해야 한다.
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(); //foreach 문과 연동하여 결과처리
    }
 
    public function putDbArrayALL($sql) {
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        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();
        }
    }
 
    // 검색조건에 일치하는 데이터 가져오기
    public function getDbDataAll($table$where$column) {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        return $stmt->fetchAll();
    }
 
    // DB Query result 함수
    function getDbresult($table,$where,$column) {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        return $stmt->fetchAll();
    }
 
    // 검색조건에 일치하는 데이터 가져오기
    public function fetchDbData($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();
        } else {
            return $stmt->fetchAll();
        }
    }
 
    // table 결과 조회 용도
    public function fetchDbArray($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 문과 연동하여 결과처리 하면 됨
    }
 
    // 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;
        return $stmt->fetchAll(); //foreach 문과 연동하여 결과처리 하면 됨
    }
 
    // WhereArgs 조건에 일치하는 선택 DbData 가져오기(ok)
    public function getDbDataFromIdx($table$whereArgs$idx) {
        $sql = 'select * from ' . $table . ' where ' . $whereArgs . '=?';
        $stmt = $this->db->prepare($sql);
        $stmt->execute(array($idx));
        return $stmt->fetch();
    }
 
    // DB 레코드 총 수(ok)
    public function getDbRows($table$where) {
        $sql = 'select count(*) from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        if ($row = $stmt->fetch()) {
            return $row[0];
        } else {
            return false;
        }
    }
 
}
?>

 

<?php
define('_DBHOST','localhost');
//define('_DBHOST','192.168.1.240'); // PHP 서버와 DB서버가 분리된 경우
define('_DBNAME',''); // DB 명
define('_DBUSER',''); // DB 사용자
define('_DBPASS'''); // DB 패스워드
define('_DBTYPE','mysql'); // 데이터베이스 종류
define('_DSN',_DBTYPE.':host='._DBHOST.';dbname='._DBNAME.';charset=utf8');
?>

 

블로그 이미지

Link2Me

,

MDB4 select

Web 프로그램/MDB 2022. 2. 12. 21:34
728x90

mdbootstrap4 기반 select

https://mdbootstrap.com/docs/b4/jquery/forms/select/

 

Bootstrap Select - examples & tutorial. Basic & advanced usage

Bootstrap select is a form control which after a click displays a collapsable list of multiple values which can be used in forms, menus or surveys.

mdbootstrap.com

<select class="browser-default custom-select">
  <option selected>Open this select menu</option>
  <option value="1">One</option>
  <option value="2">Two</option>
  <option value="3">Three</option>
</select>

 

 

MySQL DB 기반 select

 

<?php
require_once 'config.php';
require_once 'dbconnect.php';
 
$sql = "SELECT DISTINCT YM FROM customer ORDER BY YM DESC LIMIT 3";
$result = mysqli_query($db$sql);
?>
 
<form id="form" class="text-center border border-light p-5">
 
    <p class="h4 mb-4">Data 업데이트</p>
 
    <select class="browser-default custom-select" name="ym">
      <option value="" selected>-년월-</option>
    <?php
    while($R = mysqli_fetch_row($result)):
        echo '<option value="'.$R[0].'">'.$R[0].'</option>';
    endwhile;
    ?>
    </select>
 
    <div>
        <button type="submit" id="btnSubmit" class="btn btn-outline-default">전송</button>
        <button type="reset" id="btnReset" class="btn btn-outline-default">취소</button>
    </div>
    <br/>
</form>
 
<script>
$('#form').submit(function(e) {
    e.preventDefault();
    var yearmonth = $("select[name=ym]").val();
    if(yearmonth == '') {
        alert('연월을 선택하세요');
        $("select[name=ym]").focus();
        return false;
    }
 
    $.ajax({
        url: phpUpfilename,
        type: 'POST',
        data: { YM : yearmonth },
        dataType: 'text',
        success: function (data) {
            console.log(data);
            alert(data);
        },
        error: function(data) {
            alert("error in ajax form submission");
        }
    });
});
</script>
 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

Vue.js 기본 제공된 소스를 수정하기 위해서 먼저 해주어야 할 사항이다.

 

커맨드 창에서 npm install vue-router --save

 

https://bootstrap-vue.org/ 사이트에 접속하여 Get Started 버튼을 눌러서 순서대로 진행한다.

 

커맨드 창에서 아래 명령어를 입력한다.

yarn add vue bootstrap bootstrap-vue

 

 

위와 같은 에러 메시지가 나오면 CMD 창을 관리자 권한으로 열어서 명령어를 입력해야 한다.

관리자 권한으로 CMD 창 여는 방법

Window 키 + R 누르고 메시지 창에 cmd 입력하고 Ctrl + Shift + Enter키 누른다.

 

 

 

 

Vue.js 기초를 이해하기 위해서 "한시간만에 끝내는 Vue.js" 유투브 강좌를 듣고 실습한 걸 토대로 구글링해서 내용을 좀 더 추가하여 적어둔다.

https://www.youtube.com/watch?v=sqH0u8wN4Rs 

 

작업한 파일 구조

- 추가한 폴더 : layout 폴더, views 폴더

- 추가한 파일 : Header.vue, Home.vue, About.vue, router.js

- 수정한 파일 : App.vue, main.js

 

 

소스코드

<!--App.vue-->
<template>
  <div id="app">
    <Header />
    <div id="content" class="content">
      <router-view></router-view>
    </div>
 
  </div>
</template>
 
<script>
import Header from './components/layout/Header.vue'
 
export default {
  name'App',
  components: {
    Header,
  }
}
</script>
 
<style>
</style>
 

<Header /> 에서 />로 넣어주는 것을 self closing tag 라고 한다.

<br /> 가 예전에는 <br> 이었는데 self closing tag 를 추가하는 것이 최신 문법이다.

 

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
 
// Make BootstrapVue available throughout your project
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)
 
// Import Bootstrap an BootstrapVue CSS files (order is important)
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
 
Vue.config.productionTip = false
 
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
 

 

// router.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "./views/Home";
import About from "./views/About";
 
Vue.use(VueRouter);
 
const router = new VueRouter({
    mode: "history",
    routes: [{
        path: "/",
        component: Home
    },
        {
            path: "/about",
            component: About
        },
    ]
});
 
export default router;
 

 

 
<!--Header.vue-->
<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">NavBar</b-navbar-brand>
 
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
 
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item href="#">Link</b-nav-item>
          <b-nav-item href="#" disabled>Disabled</b-nav-item>
        </b-navbar-nav>
 
        <!-- Right aligned nav items -->
        <b-navbar-nav class="ml-auto">
          <b-nav-form>
            <b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
            <b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
          </b-nav-form>
 
          <b-nav-item-dropdown text="Lang" right>
            <b-dropdown-item href="#">EN</b-dropdown-item>
            <b-dropdown-item href="#">ES</b-dropdown-item>
            <b-dropdown-item href="#">RU</b-dropdown-item>
            <b-dropdown-item href="#">FA</b-dropdown-item>
          </b-nav-item-dropdown>
 
          <b-nav-item-dropdown right>
            <!-- Using 'button-content' slot -->
            <template #button-content>
              <em>User</em>
            </template>
            <b-dropdown-item href="#">Profile</b-dropdown-item>
            <b-dropdown-item href="#">Sign Out</b-dropdown-item>
          </b-nav-item-dropdown>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
  </div>
</template>
 
<script>
export default {
  name"Header"
}
</script>
 
<style scoped>
 
</style>

 

<!--Home.vue-->
<template>
<div>
  <h2>Welcome to {{ title }}!</h2>
  <input type="text" v-model="input1">
  <button @click="getData">Get</button>
  <button @click="setData">Set</button>
  <div><p></p></div>
  <select class="form-control" v-model="region" @change="changeRegion">
    <option :value="d.v" v-for="(d,i) in optins" :key="i">{{d.data}}</option>
  </select>
  <div><p></p></div>
  <table class="table table-bordered" v-if="tableShow">
    <tr v-for="(d,i) in optins" :key="i">
      <td>{{d.v}}</td>
      <td>{{d.data}}</td>
    </tr>
  </table>
</div>
</template>
 
<script>
export default {
  name"Home",
  data(){
    return {
      // v-model 을 통해서 데이터 바인딩 처리
      title: "MySite",
      input1: "abc",
      optins: [
        {v:"S", data:"서울"},
        {v:"B", data:"부산"},
        {v:"D", data:"대전"},
      ],
      region: "D",
      tableShow: false,
      // v-show 는 랜더링을 하지만, v-if는 랜더링 자체를 하지 않는다.
    }
  },
  watch:{
    input1(){
      // 특정 데이터를 모니터링하고 있다가
      // 캐치해서 새로운 작업 수행 처리
      console.log(this.input1);
    },
  },
  methods: {
    getData(){
      alert(this.input1);
    },
    setData(){
      this.input1 = "12345";
    },
    changeRegion(){
      alert(this.region);
    },
  },
  // Vue LifeCycle
  beforeCreate(){
    // Vue 인스턴스가 초기화된 직후에 발생한다.
    // 컴포넌트가 DOM에 추가되기도 전이다.
    // data, event, watcher에 접근하기 전이라 data, methods에도 접근할 수 없다.
    console.log("beforeCreate");
  },
  created(){
    // beforeCreate() => data() => created() 순으로 실행된다.
    // data에 직접 접근이 가능하지만, 아직 템플릿과 가상DOM에는 접근할 수 없다.
    // 이제 data와 events가 활성화되어 접근할 수 있다.
    console.log("created");
  },
  beforeMount(){
    console.log("beforeMount")
  },
  mounted(){
    // 가상 DOM의 내용이 실제 DOM에 부착되고 난 이후에 실행되므로,
    // data, computed, methods, watch 등 모든 요소에 접근이 가능하다.
    console.log("mounted");
  },
  beforeUpdate(){
    // DOM이 패치되기 전에 데이터가 변경될 때 호출된다.
    console.log("beforeUpdate");
  },
  updated(){
    // 데이터가 변경되어 가상 DOM이 다시 렌더링되고 패치된 후에 호출된다.
    console.log("updated");
  },
  beforeDestroy(){
    // 해당 인스턴스가 소멸(뷰 컴포넌트 제거)되기 직전에 beforeDestroy훅이 호출된다.
    console.log("beforeDestroy");
  },
  destroyed(){
    // 인스턴스가 소멸되고 난 직후에 destroyed 훅이 호출된다.
    // Vue의 모든 디렉티브(v-)가 바인딩 해제되고
    // 모든 이벤트 리스너가 제거되며 모든 하위 Vue 컴포넌트도 삭제된다.
    console.log("destroyed");
  },
 
}
</script>
 
<style scoped>
 
</style>

 

<!--About.vue-->
<template>
<div>
  <h1>About Page</h1>
</div>
</template>
 
<script>
export default {
  name"About"
}
</script>
 
<style scoped>
 
</style>
 

 

 

'Web 프로그램 > Vue' 카테고리의 다른 글

Vue.js 환경 설정  (0) 2022.01.13
블로그 이미지

Link2Me

,
728x90

Vue.js 는 웹프론트엔드 프레임웍 중 하나이다.

프론트엔드 프레임웍은 React, Vue.js, Angular 가 있고 대세는 React 라고 한다.

2021년도에는 Javascript/jQuery 가 많이 사용되었는데 React 가 1위로 올라섰다고 한다.

 

Vue.js 는 컴포넌트 기반의 SPA(Single Page Application)를 구축할 수 있게 해주는 프레임워크이다.

 

Python Django 강의를 듣는데 Front-End 로 Vue.js 를 사용하고 있어서 간단하게 알아두어야 할 거 같아서 유투브 동영상을 보고 간략하게 기록해 두고 있다.

 

Vue 는 node.js 기반으로 되어있기 때문에 node가 설치되어야 한다. 

https://nodejs.org/ko/ 에서 LTS 버전을 다운로드 받아 C:\ProgramFiles\nodejs 폴더에 설치한다. (default 설치 폴더)

 

커맨드 창에서 npm install --global yarn 을 실행하여 yarn을 설치를 한다.

yarn global add @vue/cli 를 입력하여 vue.js 를 설치한다. (npm install -g @vue/cli 명령어와 동일)

 

npm install vue 는 하지 말라. 위에 yarn global add @vue/cli 만 하면 된다.

vue --version 을 했을 때 2.9.6 과 같이 2.X 버전이 설치되어 있다면 지워야 한다.

npm r -g vue-cli  를 하면 지워진다.

 

 

NPM: npm-cli.js not found when running npm 메시지가 나오면, node.js 설치파일을 다시 실행하여 Repair를 하면 해결되더라.

 

VSCode 에서 작업하는 과정은 생략한다.

 

프로젝트 생성

WebStorm 또는 PHPStorm 에서 아래 그림과 같이 선택하고 Create 버튼을 클릭한다.

또는 커맨드 창에서 

vue create {프로젝트명} // 프로젝트명이 폴더로 생성된다.

vue create blog

 

 

생성된 프로젝트는 아래 그림과 같다.

동작구조를 파악하기 위해 빨간색으로 표시된 부분을 주의깊게 보자.

 

 

프로젝트 구조

  • package.json : 지정된 의존성이 node_modules에 설치 된다.
  • node_modules : 앱 개발과 배포에 필요한 npm 패키지들이 저장 된다.
  • src : 개발자가 작성한 코드 디렉토리
  • public
    • 공용으로 접근 가능한 정적 파일이 저장 된다.
    • 배포버전을 빌드할 때 필요한 파일이 저장 된다.
  • dist
    • 빌드한 결과물이 저장 된다. (운영 서버에 배포할 파일이다.)

 

yarn serve 개발용 버전으로 웹 실행 (npm run server 와 동일 명령어)

터미널 창을 누르고 yarn serve 를 입력하고 엔터키를 치면....

 

 

위 그림의 빨간색을 누르면 아래와 같이 Web 브라우저가 구동되며 화면이 보이게 된다.

 

여기까지가 Vue.js 기본 환경 설정 방법이다.

 

build 배포용 버전으로 dist 생성
yarn build

'Web 프로그램 > Vue' 카테고리의 다른 글

Vue.js 기본 화면 수정하기  (0) 2022.01.14
블로그 이미지

Link2Me

,