728x90

위꼬리 캔들이 의미하는 걸 파악할 줄 알아야 한다.

위 차트에서 1번 장대양봉 다음날부터 음봉이 출현하였다.

여기서 주의깊게 봐야 할 것은 장대양봉의 거래량 대비 50% 이하의 거래량인지 여부이다.

3일 연속 음봉이 나온날 의 거래량은 장대양봉 거래량의 10% 수준이다.

보유하고 있거나, 다음날 양봉이 출현한 날 매수시점이다.

 

2번 장대양봉 다음날 엄청난 거래량을 동반한 음봉이 출현했다.

이날 매도해야 한다.

거래량이 동반된 위꼬리 음봉은 하락의 신호이다.

 

 

위 차트를 보면 위꼬리 음봉이 나온날 거래량을 보자.

거래량이 장대양봉 거래량만큼 많다.

즉 하락의 신호이므로 이날 매도했거나 다음날 무조건 매도해야 한다.

기억하자. 장대양봉 거래량의 50%가 넘는 거래량이면서 위꼬리 음봉이면 무조건 매도 신호라는 걸....

 

 

위 차트를 살펴보자.

장대양봉 다음날 매미모양으로 시작된 음봉의 거래량이 장대양봉 거래량의 50%가 넘었다.

주가가 하락할 것이라는 신호를 암시하고 있다고 봐야 한다.

시작부터 하방으로 하락하는 음봉이면 무조건 매도해야 한다.

하락의 신호라는 걸 알지 못하고 매수하면 절대 안된다.

 

 

위 차트에서 눈여겨 봐야 할 것은 ...

2번 시초가부터 수직하방하는 음봉이 나오면 무조건 매도해야 한다.

다음날 위꼬리 길게 달린 양봉이 나왔지만 매도를 못한 기회를 주는 캔들이라고 보면 된다.

블로그 이미지

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

볼린저밴드와 RSI 강세패턴을 활용하여 키움증권 종합차트 보조지표 설정하는 방법을 적어둔다.

 

키움종합차트 화면에서 마우스 우클릭을 한다.

기존에 설정된 것이 있을 수도 있으니 기본차트로 초기화부터 한다.

다른 거 설정된 거 잘 사용하고 있는데 기본차트로 초기화를 한다고?? 걱정하지 말고 따라서 해보면 된다.

기존 사용하던 차트툴 관리명이 등록되어 있어야 한다.

이제 기본차트를 수정한다.

아래 2번으로 된 차트의 숫자 아무거나 더블클릭한다.

 

더블클릭하면 아래창이 뜬다. 여기서 기간5를 224로 변경해준다.

그리고 라인설정 탭을 누른다.

 

224일 이평선은 검은색으로 변경했다.

 

20일 이평선은 녹색으로 변경했다.

12번까지 하고 확인을 누르면....

아래 그림과 같은 결과가 나온다. 여기까지가 기본차트를 원하는 것으로 설정하는 과정이었다.

 

보조지표 추가

볼린저밴드 보조지표를 추가하는 방법이다.

마우스 우클릭을 하면 아래와 같은 그림이 나온다.

 

 

bol 이라고 입력하면 아래와 같이 Bollinger Bands 를 찾아준다.

선택하고 적용을 누른다.

 

E로 표시된 부분을 마우스로 더블클릭한다.

 

상한선만 선택하고 나머지는 선택하지 않는다.

너비를 3pt로 굵게 표시되도록 변경한다.

 

 

그러면 여기까지 선택된 차트 모양이 보일 것이다.

 

 

이제 RSI 강세/약세 패턴을 화면에 색상으로 표시되는 걸 추가하자.

차트 화면에서 마우스 우클릭을 하고 아래와 같이 "강세약세 적용" 선택한다.

 

rsi 라고 입력한다.

 

이렇게 하면 적용이 끝난다.

 

위와 같은 차트로 색상이 표시된다. 옅은 빨간색이 과열구간인데 과열구간 초입에 매수하면 된다.

 

이제 화면 설정한 결과를 저장하자.

 

차트틀 관리를 누른다. 처음 시도하면 추가된 명칭이 없을 것이다.

 

필요할 때 선택하여 재사용하기 위해서 저장명을 적어주고 등록/수정 버튼을 눌러서 저장해준다.

 

이제 그러면 추가된 것을 확인해보자.

방금 추가된 것이 아래와 같이 보인다.

 

여기까지가 보조지표를 설정하는 방법에 대한 설명이다.

다른 보조지표를 더 추가해서 원하는 대로 편하게 설정하면 된다.

 

조금 아쉬운 거 같아서 추가로 MACD 0선 상향돌파 화살표 표기하는 걸 추가해본다.

아래 번호 순서대로 하면 된다.

 

색상은 빨간색으로 두어도 되고 검은색으로 변경해도 된다.

모바일에서 보니까 검은색이 눈에 좀 더 잘 보이는 거 같아서 변경했다.

 

모바일에서 보기 위해 차트 지표를 내보내는 방법이다.

위와 아래 내용이 좀 다른 것은...

저장버튼을 눌러서 선택한 마지막 화면 내용을 내보내기하는 것이라서 다르다.

 

블로그 이미지

Link2Me

,
728x90

크롬 브라우저를 이용해 다른 PC을 원격제어하기 위해 설치해야 하는 확장 프로그램이 Chrome Remote Desktop 이다.

먼저 설치되어 있는 확장 프로그램을 확인해보자.

 

삭제를 했다.

처음부터 설치하는 과정을 아래와 같이 설명하기 위해서다.

 

먼저 아래 URL를 눌러라.

 

https://www.google.co.kr/search?q=chrome+remote+desktop+%ED%99%95%EC%9E%A5%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8&newwindow=1&sxsrf=AB5stBhNq3gLTyjxAsD6T8n4DBpqbXrMiQ%3A1691198110985&ei=nqLNZKPeO_W42roP0aSmQA&oq=chrome+remote+desktop&gs_lp=Egxnd3Mtd2l6LXNlcnAiFWNocm9tZSByZW1vdGUgZGVza3RvcCoCCAAyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsAMyChAAGEcY1gQYsANI-xRQAFgAcAF4AZABAJgBAKABAKoBALgBAcgBAOIDBBgAIEGIBgGQBgo&sclient=gws-wiz-serp 

 

🔎 chrome remote desktop 확장프로그램: Google 검색

 

www.google.co.kr

 

아래 1번을 클릭한다.

 

 

확장프로그램 추가를 누른다.

 

여기까지 하면 확장프로그램 추가는 완료된 것이다.

 

이제 사용법을 알아보자.

아래 URL을 누른다.

https://remotedesktop.google.com/

 

Chrome 원격 데스크톱

Simple 컴퓨터, Android 기기 또는 iOS 기기를 사용하여 원하는 방식으로 연결하세요. 어떤 방법을 선택하든 필요할 때 모든 컴퓨터와 파일에 간편하게 액세스할 수 있습니다.

remotedesktop.google.com

 

위 사이트에 접속하면 아래와 같은 화면이 보인다. 1번을 누른다.

 

내 화면 공유를 누른다.

 

내 컴퓨터의 화면을 상대방에게 원격으로 접속하게 허용하도록 하려면 3번 코드생성 버튼을 누른다.

 

생성된 코드를 복사하여 상대방에게 알려줘야 한다.

전화통화로 불러주거나, 카톡으로 전달해준다.

블로그 이미지

Link2Me

,
728x90

음료수 : 코카콜라. 액상과당과 설탕, 각종 감미료와 색소 등 화학물질의 총쳉이다.

              지방간의 직접적인 원인이 되며 해독작용을 일으켜 간에 무리가 된다.

튀김류 : 튀김류의 트랜스 지방은 단시간 내에 간에 염증을 유발하는 직접적인 요인이 된다.

가공육 : 가공육의 방부제와 발색제 성분은 소량으로도 간의 염증을 유발한다.

              동물성 지방 함량이 높아 담즙의 과다분비를 촉진해 간세포 손상 원인이 된다.

진통제 : 간이 나쁜 경우 진통제 섭취에 매우 신중해야 한다.

              특히 아세트아미노펜 계열의 해열진통제는 주로 간에서 대사되며 적은 양으로도 간에 무리를 준다.

 

블로그 이미지

Link2Me

,
728x90

삼성증권에서 강세 약세 화면 설정하는 법을 적어둔다.

먼저 마우스 우클릭을 하고 나서 아래와 같은 순서로 설정한다.

 

강세선 색을 하얀색으로 변경했더니 글씨 내용이 보이지 않는다.

 

MACD 기준선 매수 신호 설정값

블로그 이미지

Link2Me

,
728x90

MACD 화살표 기법에 대한 동영상 자료를 보고 화면 캡처한 것과 약간의 내용 추가한 것을 적어둔다.

 

먼저 "키움종합차트"에서 마우스 우클릭을 해서 기본차트를 눌러서 화면을 초기화한다.

기본차트는 5일선과 20일선, 224일선만 남기고 나머지는 보이지 않도록 한다.

아래 그림에서 1번을 더블클릭하면 아래와 같이 지표 설정 화면이 나온다.

기간5는 120 대신에 224로 변경해서 적어준다.

 

라인설정 탭에서 화면에 표시하고 싶은 이평선만 체크표시가 되도록 한다.

224일 이평선은 너비를 2pt 로 설정하여 굵게 표시되도록 한다.

20일선은 색상을 파란색으로 변경했다. 이평선 색상은 각자 개인 취향에 맞게 설정하면 된다.

 

이제 MACD 기준선을 설정한다.

왼쪽 검색창에서 macd 를 입력하고 아래 순서와 같이 찾아서 한다.

검색창을 못찾으면 옆에 화살표를 누르면 보일 것이다.

아래 그림은 유투브 영상에서 이평선마저 완전히 없애도록 설정하는 걸 그대로 따라하다보니 없는데 위에 설정법과 같이 하면 화면에는 남아있을 것이다. 이평선 설정한 이후에 macd 색상 표시되도록 추가하면 되는 것이다.

 

아래 그림과 같이 MACD 강세패턴과 약세패턴의 색상이 기본 설정값으로 되어 있다.

여기서 지표변수를 5, 12로 변경하고 수식 탭에서 색상을 변경해준다.

 

강세패턴 색상은 하얀색으로 변경한다. 즉 매수할 패턴은 종합차트에서 하얀색으로 보이게 하는 것이다.

 

약세패턴 색상은 하안색에서 옆으로 3개 뒤에 있는 색을 선택한 것이다.

약세패턴에서는 가능하면 주식을 매수하지 말자.

매수했던 종목을 이 차트 색상으로 설정하고 나니까 한눈에 파악되고 너무 편하다.

물론 차트 매매법을 충분히 알고 있다면 약세패턴 색상에서도 매수할 타이밍인지 여부는 본인 스스로 알 것이다.

 

그러면 아래와 같은 화면이 나올 것이다.

 

이제 MACD 0선 상향돌파 화살표가 표시되도록 해보자.

아래 그림에 보이는 번호 순서대로 하고 6번까지 하고 저장한다면 빨간색 화살표가 보일 것이다.

종목에 따라서는 화살표가 보이지 않을 수도 있으니 다른 종목을 선택해서 확인해보면 된다.

 

색상은 유투브 영상에서 추천하는 검은색으로 변경했다. 빨간색으로 두어도 되고, 파란색으로 변경해도 된다.

 

화살표가 뜬 종목을 찾아서 이수페타시스를 검색했다.

그리고 아래 그림의 화살표 표시한 저장버튼을 눌러서 설정한 값을 저장하자.

검색식 유투브 영상을 이것 저것 따라 하다보면 내가 기본적으로 설정해야 하는 값이 달라져 무척 곤혹스러울 수 있다.

영상에서는 저장하는 걸 잘 알려주지 않은 경우도 많다.

반드시 저장해서 상황에 맞게 선택하여 사용하면 매우 편하고 좋을 것이다.

 

 

MACD 화살표 차트  라고 저장명을 적어주고 등록/수정버튼을 눌러준다.

등록하고 차트 보조지료를 보는 걸 수정했을 경우에도 좋은 보조지표이면 등록/수정으로 업데이트 하면 좋을 것이다.

 

삼부토건 종목을 한번 찾아봤다.

MACD 0선 상향돌파 화살표가 표시된 이후 주가가 상승했고, 배경색이 흰색으로 표시되어 있는 걸 확인할 수 있다.

 

이제 MACD 0선 상향돌파한 종목만 찾아내는 검색식을 만들자.

영상에 나온 걸 그대로 캡쳐한 그림이다. 대상변경 제외종목은 영상보다 좀 더 설정했다.

 

MACD 0선 돌파하는 값 설정이다.

조건색 탭에서 macd 를 입력하고 엔터키를 치거나 검색을 한 후 5번 기준값 돌파를 선택한다.

단기 12 → 7, 장기 26 → 16 으로 변경한다.

 

시가총액 : 500억원 이상으로 설정한다.

 

 

주가범위

 

거래량비율

 

4가지 항목을 추가했으면 조건식 저장 버튼을 눌러서 조건명을 지정해주고 확인하면 저장된다.

 

검색식을 어떻게 만드느냐에 따라 검색되는 종목이 달라질 수 있다.

 

도움이 되셨다면 공감 꾸욱 눌러주세요.

블로그 이미지

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

신고가

최근 6개월 이내 신고가를 돌파했는가?

 

기간내 거래대금

20봉(한달) 이내에서 거래대금 30,000 이상 또는 50,000 이상

 

기간내 등락률

 

블로그 이미지

Link2Me

,
728x90

일목균형표

 

일목균형표 기준선 근접 : 주가

 

일목균형표 기준선 근접 : 시가

 

일목균형표 기준선 근접 : 저가

 

일목균형표 기준선 비교

블로그 이미지

Link2Me

,
728x90

일 주기 : 일봉

0봉전 : 검색 당일

종가1이평 : 당일 종가 또는 현재가

골든크로스 : 당일 현재가 또는 종가가 224일 이동평균선을 아래에서 위로 돌파한다는 의미

 

단기이동평균선 정배열(5일선 > 10일선 > 20일선)이면 급등하는 모습이 실전에서 자주 나온다.

 

캔들연속발생

 

주가등락률

 

주가 비교

시가보다 종가가 높게 설정

 

주가이동평균추세

 

 

평균거래대금

 

주가범위

 

체결강도

 

주가비교

 

블로그 이미지

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

실제 효과 여부를 검증해보려고 한다.

미국 나스닥이 안좋은 날은 효과가 전혀 없었다.

검색된 종목 중에서 2개는 다음날 급등 및 상승하는 걸 확인했다.

 

키움증권 RSI + MACD 조건검색식 동영상을 보고 따라하면서 화면 캡처한 것을 죽 나열했다.

2번 제외항목은 전체를 모두 선택하는 것이 더 나을 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

L 항목은 체크해제해야 검색으로 찾아지는 종목이 많다는 걸 확인했다.

 

조건검색식 유투브 영상

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

 

블로그 이미지

Link2Me

,
728x90

Flutter 에서 WebView를 사용하기 위한 설정이다.

 

Android 설정

android/app/src/main/AndroidManifest.xml 파일에 추가할 사항

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.link2me.flutter.webview">
 
    <uses-permission android:name="android.permission.INTERNET"/>
    
   <application
        android:label="webview"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        android:usesCleartextTraffic="true">

 

IOS 설정

ios/Runner/info.plist 파일 하단에 추가할 사항

    <key>CADisableMinimumFrameDurationOnPhone</key>
    <true/>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsLocalNetworking</key>
        <true/>
        <key>NSAllowsArbitraryLoadsInWebContent</key>
        <true/>
    </dict>
</dict>
</plist>

 

pubspec.yaml

name: webview
description: A new Flutter project.
 
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
version: 1.0.0+1
 
environment:
  sdk: ">=2.17.6 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
 
  cupertino_icons: ^1.0.2
  webview_flutter: ^3.0.0
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
 
flutter:
 
  uses-material-design: true
 
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

 

webview 소스코드

import 'package:flutter/material.dart';
import 'package:webview/screen/home_screen.dart';
 
void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    )
  );
}

 

lib/screen/home_screen.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
 
class HomeScreen extends StatelessWidget {
  WebViewController? controller;
  final homeUrl = 'https://m.naver.com/';
 
  HomeScreen({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('모바일 네이버'),
        centerTitle: true,
        actions: [
          IconButton(
              onPressed: () {
                if(controller != null){
                  controller!.loadUrl(homeUrl);
                }
              },
              icon: Icon(
                Icons.home,
              ))
        ],
      ),
      body: WebView(
        onWebViewCreated: (WebViewController controller) {
          this.controller = controller;
        },
        initialUrl: homeUrl,
        javascriptMode: JavascriptMode.unrestricted,
      ),
    );
  }
}

 

flutter webView 4.0 버전

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  webview_flutter: ^4.4.4

 

main.dart

import 'package:flutter/material.dart';
import 'package:webview/screen/home_screen.dart';
 
void main() {
  // Flutter 프레임워크가 앱을 실행할 준비가 될 때까지 기다린다.
  WidgetsFlutterBinding.ensureInitialized();
 
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    )
  );
}

 

lib/screen/home_detail_view.dart

 
import 'package:flutter/material.dart';
import 'dart:core';
import 'package:webview_flutter/webview_flutter.dart';
 
class HomeDetailView extends StatelessWidget {
  final String Url_path;
 
  HomeDetailView({
    Key? key,
    required this.Url_path,
  }) : super(key: key);
 
  WebViewController controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setBackgroundColor(Colors.blueGrey.shade100)
    ..setNavigationDelegate(
      NavigationDelegate(
        onProgress: (progress) {},
        onPageStarted: (url) {},
        onPageFinished: (url) {},
        onWebResourceError: (error) {},
        onNavigationRequest: (request) {
          if (request.url.startsWith('url')) {
            return NavigationDecision.prevent;
          } else {
            return NavigationDecision.navigate;
          }
        },
      ),
    );
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: WebViewWidget(
        controller: controller..loadRequest(Uri.parse(Url_path)),
      ),
    );
  }
}
 

 

- last Update : 2024.1.24일

 

 

GitHub 에 webview 4.0 코드 필요한 것만 발췌하여 올렸다.

https://github.com/jsk005/Flutter/tree/main/webview

 

GitHub - jsk005/Flutter: Flutter Study

Flutter Study. Contribute to jsk005/Flutter development by creating an account on GitHub.

github.com

 

 

 

 

블로그 이미지

Link2Me

,
728x90

접속로그 통계 구현을 위해서 로그를 쌓고 있는 테이블에서 데이터를 가공해야 한다.

접속로그 테이블에는 접속실패, 접속성공 등 모든 접속 시도 데이터를 저장해야 한다.

CREATE TABLE `rb_accessLog` (
  `uid` int(11NOT NULL,
  `ipaddr` varchar(20NOT NULL,
  `access_date` datetime NOT NULL COMMENT '접속시간',
  `userID` varchar(60NOT NULL,
  `userNM` varchar(20DEFAULT NULL,
  `OS` varchar(60DEFAULT NULL,
  `browser` varchar(30DEFAULT NULL,
  `hit` int(11NOT NULL DEFAULT 0,
  `success` int(1NOT NULL DEFAULT 1 COMMENT '성공실패',
  `date` char(8NOT NULL,
  `time` varchar(10NOT NULL,
  `YM` char(6NOT NULL,
  `MD` char(4NOT NULL,
  `year` char(4NOT NULL,
  `month` char(2NOT NULL,
  `day` char(2NOT NULL,
  `route` int(1NOT NULL DEFAULT 0 COMMENT '접속루트',
  `errCode` int(2NOT NULL DEFAULT 0 COMMENT '에러코드',
  `display` tinyint(2NOT NULL DEFAULT 1
ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
 
ALTER TABLE `rb_accessLog`
  ADD PRIMARY KEY (`uid`) USING BTREE,
  ADD KEY `date` (`date`),
  ADD KEY `YM` (`YM`),
  ADD KEY `MD` (`MD`);
 
 
ALTER TABLE `rb_accessLog`
  MODIFY `uid` int(11NOT NULL AUTO_INCREMENT;
COMMIT;
 

 

 

하지만 접속통계는 로그인 성공에 대한 것만 일자별로 사용자 기준으로 접속하는 테이블을 구현하면 될 것이다.

CREATE TABLE `rb_accessLog_tmp` (
  `uid` int(11NOT NULL,
  `date` char(8NOT NULL,
  `userID` varchar(60NOT NULL,
  `userNM` varchar(20DEFAULT NULL,
  `YM` char(6NOT NULL,
  `MD` char(4NOT NULL,
  `year` char(4NOT NULL,
  `month` char(2NOT NULL,
  `day` char(2NOT NULL,
  `hit` int(11NOT NULL DEFAULT 0,
  `display` tinyint(2NOT NULL DEFAULT 1
ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
 
ALTER TABLE `rb_accessLog_tmp`
  ADD PRIMARY KEY (`uid`) USING BTREE,
  ADD UNIQUE KEY `date_userID` (`date`,`userID`);
 
ALTER TABLE `rb_accessLog_tmp`
  MODIFY `uid` int(11NOT NULL AUTO_INCREMENT;
COMMIT;

 

 

데이터를 추출하여 넣은 SQL 쿼리문은 다음과 같다.

INSERT INTO rb_accessLog_tmp (date, userID, userNM, YM, MD, year, month, day,hit) 
(select date, userID, userNM, YM, MD, year, month, day, count(*) as hit 
from rb_accessLog where success=1 group by date, userID) 
ON DUPLICATE KEY UPDATE date = VALUES(date), 
userID = VALUES(userID), hit=VALUES(hit)

없는 자료는 Insert 하고 중복된 자료는 특정 칼럼만 업데이트한다.

달라질 칼럼은 hit 칼럼밖에 없을 것이다.

 

중복체크하는 중요한 Key는 date_userID 인덱스 설정이다.

 

이 테이블에서 다시 일별 사용자수 접속 통계, 접속수 통계를 구분하는 테이블을 만들어보자.

CREATE TABLE rb_access_Stats (
  uid int(11NOT NULL,
  date char(8NOT NULL,
  YM char(6NOT NULL,
  day char(2NOT NULL,
  IDCnt int(11NOT NULL DEFAULT 0,
  dailyCnt int(11NOT NULL DEFAULT 0
ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
ALTER TABLE rb_access_Stats
  ADD PRIMARY KEY (uid),
  ADD UNIQUE KEY date (date) USING BTREE;
 
ALTER TABLE rb_access_Stats
  MODIFY uid int(11NOT NULL AUTO_INCREMENT;
COMMIT;

 

UNIQUE KEY 설정은 필수 사항!!!

# 일일 통계 생성 
INSERT INTO rb_access_Stats (date,YM,day,IDCnt,dailyCnt) 
(select date, YM, day ,count(distinct userID) as IDCnt,sum(hit) as dailyCnt 
from rb_accessLog_tmp group by date)
ON DUPLICATE KEY UPDATE date = VALUES(date), IDCnt = VALUES(IDCnt), 
dailyCnt = VALUES(dailyCnt)

 

'SQL' 카테고리의 다른 글

MySQL foreign key 예제  (0) 2022.05.16
[MySQL] 샘플 데이터베이스 설치하기  (0) 2022.04.28
MariaDB 멀티 인덱스(index) 설정  (0) 2022.04.05
MariaDB 대소문자 구분  (0) 2022.03.14
MySQL 중복 레코드 관리 방법  (0) 2022.03.02
블로그 이미지

Link2Me

,
728x90

샘플 코드만 적어둔다.

Android 네이버 Map 라이브러리를 이용하여 검색(Search)으로 위치 이동과 더불어 지도 레벨을 지정해야 할 때 필요한 코드이다.

 

private fun LocationAt(latitude: Double, longitude: Double) {
    mNaverMap?.locationTrackingMode = LocationTrackingMode.None
    val coord = LatLng(latitude, longitude)
    val cameraPosition = CameraPosition(coord, 16.0)
    mNaverMap.cameraPosition = cameraPosition
    val cameraUpdate = CameraUpdate.scrollTo(coord).animate(CameraAnimation.Easing, 300)
    mNaverMap!!.moveCamera(cameraUpdate)
}

 

 

블로그 이미지

Link2Me

,