728x90

https://dart.dev/language/records 에 공식적인 설명이 되어 있다.

 

Records는 익명성, 불변성, 집계성을 가진 타입이며
다른 컬렉션 유형(List, Set, Map 등등)과 마찬가지로 여러 개체를 하나의 개체로 묶을 수 있다.
하지만 다른 컬렉션 유형과 달리 Record는 크기와 유형이 고정되어 있다.

void main() {
  final result = person({
    'name''홍길동'
    'age'25
  });
  
  print(result); // (홍길동, 25)
  
  print(result.$1); // 홍길동
  
  print(result.$2); // 25
  
  
 
  var record = ('first', a: 2, b: true'last');
 
  print(record.$1); // Prints 'first'
  print(record.a); // Prints 2
  print(record.b); // Prints true
  print(record.$2); // Prints 'last'
 
 
}
 
// 튜플로 반환하여 Type과 순서를 보장받을 수 있다.
// 튜플은 소괄호를 사용한다.
(String, int) person(Map<String, dynamic> json) {
  return (json['name'as String, json['age'as int);
}
 

 

 

 
// Returns multiple values in a record:
(String, int) userInfo(Map<String, dynamic> json) {
  return (json['name'as String, json['age'as int);
}
 
final json = <String, dynamic>{
  'name''Dash',
  'age'10,
  'color''blue',
};
 
// Destructures using a record pattern:
var (name, age) = userInfo(json);
 
/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

 

 

void main() {
  final result = getPersonWithType();  
 
  for(final item in result){
    print(item);
  }
  
  print('-----------------------');
  
  for(final item in result){
    print(item.$1);
    print(item.$2);
  }
 
}
 
List<(String name, int age)> getPersonWithType() {
  return [
    ('홍길동'25),
    ('강감찬'37),
    ('이순신'33)
  ];
}

 

 

void main() {
  final result = getPersonWithNameType(); 
 
  for(final item in result){
    print(item);
  }
  
  print('-----------------------');
  
  for(final item in result){
    print(item.name);
    print(item.age);
  }
 
}
 
List<({String name, int age})> getPersonWithNameType() {
  return [
    ( name: '홍길동', age: 25),
    ( name: '강감찬', age: 37),
    ( name: '이순신', age: 33)
  ];
}

 

 

728x90

'Flutter 앱 > Dart 언어' 카테고리의 다른 글

getter & setter  (0) 2023.12.09
Dart 직렬화(Serialization) 및 역직렬화(Deserialization)  (0) 2023.12.08
Dart Collection  (0) 2022.06.28
Dart Class(클래스)  (0) 2022.06.27
Dart Asynchronous programming(비동기 프로그래밍)  (0) 2022.06.23
블로그 이미지

Link2Me

,
728x90

자바스크립트 구조분해할당과 keys를 얻는 방법에 대해 간략하게 적어둔다.

 

 
const user = {
    name:'홍길동',
    age: 25,
    email:'link2me@gmail.com'
}
 
// 구조 분해 할당 : 필요한 변수만 지정할 수 있다.
const { name, age, email } = user;
console.log(`사용자의 이름은 ${name}입니다.`);
console.log(`${name}의 나이는 ${age}세 입니다.`);
console.log(`${name}의 이메일은 ${email}세 입니다.`);
 
/***
 * Object.keys(obj) – 객체의 키만 담은 배열을 반환.
 * Object.values(obj) – 객체의 값만 담은 배열을 반환
 * Object.entries(obj) – [키, 값] 쌍을 담은 배열을 반환
 */
const keys = Object.keys(user);
console.log(keys); // [ 'name', 'age', 'email' ]
 
console.log(user['email']);
 
const values = keys.map(key => user[key])
console.log(values); // [ '홍길동', 25, 'link2me@gmail.com' ]
 
for(let key in user){
    console.log(`${key} ${user[key]}`);
}
 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

JavaScript의 Object.assign()메서드 사용법을 알아보자.

불변성(immutable)을 지켜야 된다면 Object.assign을 사용하는 것 보다 스프레드 연산자를 사용하는 것이 좋다.
Object.assign으로 불변성을 지키려면 target에 항상 {} 빈 객체를 인자로 전달해야 한다.

const userAge = {
    name:'홍길동',
    age:25
}
 
const userEmail = {
    name:'홍길동',
    email:'link2me@gmail.com'
}
 
/***
 * Object.assign(target, ...sources);
 * target : 출처 객체의 속성을 복사해 반영한 후 반환할 객체
 * source : 목표 객체에 반영하고자 하는 속성들을 갖고 있는 객체들
 * 목표 객체의 속성 중 출처 객체와 동일한 키를 갖는 속성의 경우,
 * 그 속성 값은 출처 객체의 속성 값으로 덮어쓴다.
 */
const target = Object.assign({}, userAge, userEmail);
console.log(target);
// { name: '홍길동', age: 25, email: 'link2me@gmail.com' }
console.log(userAge);
// { name: '홍길동', age: 25 }
console.log(target === userAge); // false
 
const a = { k: 123 }
const b = { k: 123 }
console.log(a === b); // false
 

 

 

 
const userAge = {
    name:'홍길동',
    age:25
}
 
const userEmail = {
    name:'홍길동',
    email:'link2me@gmail.com'
}
 
/***
 * Object.assign(target, ...sources);
 * target : 출처 객체의 속성을 복사해 반영한 후 반환할 객체
 * source : 목표 객체에 반영하고자 하는 속성들을 갖고 있는 객체들
 * 목표 객체의 속성 중 출처 객체와 동일한 키를 갖는 속성의 경우,
 * 그 속성 값은 출처 객체의 속성 값으로 덮어쓴다.
 */
const target = Object.assign(userAge, userEmail);
console.log(target);
// { name: '홍길동', age: 25, email: 'link2me@gmail.com' }
console.log(userAge);
// { name: '홍길동', age: 25, email: 'link2me@gmail.com' }
console.log(target === userAge); // true
 
const a = { k: 123 }
const b = { k: 123 }
console.log(a === b); // false (서로 다른 객체)
 

 

728x90
블로그 이미지

Link2Me

,
728x90

한 마디로 클로저란, 함수가 선언될(생성될) 그 당시에 주변의 환경과 함께 갇히는 것을 말한다.
함수가 속한 렉시컬 스코프(Lexical Environment)를 기억하여, 함수가 렉시컬 스코프 밖에서 실행될 때도 이 스코프에 접근할 수 있게 해주는 기능이다.
렉시컬 스코프란 함수가 선언이 되는 위치에 따라서 상위 스코프가 결정되는 스코프다.
내부함수는 외부함수의 지역변수에 접근할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있는 것

 

/***
 * 자바스크립트에는 클로저라는 기능이 있다.
 * 클로저는 함수와 그 함수를 둘러싸고 있는 주변의 상태를 기억하는 기능이다.
 * 클로저는 자바스크립트 고유의 개념이 아니라
 * 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.
 * 클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
 */
 
function makeAdd2(v1) {
  // 함수 makeAdd 내에서 내부함수가 선언되고 호출되었다.
  return function (v2) {
    //  이때 내부함수는 자신을 포함하고 있는 외부함수 makeAdd 변수 v1 접근할 수 있다.
    return v1 + v2;
  };
}
 
function makeAdd(v1){
  // 함수 makeAdd 내에서 내부함수 innerFunc가 선언되고 호출되었다.
  let innerFunc = function (v2){
    //  이때 내부함수 innerFunc는 자신을 포함하고 있는 외부함수 makeAdd 변수 v1 접근할 수 있다.
    return v1 + v2;
  }
  return innerFunc;
}
 
const add3 = makeAdd(3);
console.log(add3); // [Function innerFunc]
console.log(add3(10));
const add7 = makeAdd(7);
console.log(add7(10));
 

 

 

 
<!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>closure</title>
    <script defer src="./main.js"></script>
</head>
<body>
<p>클로저를 사용한 Counting</p>
<button id="inclease">+</button>
<p id="count">0</p>
<script>
    const incleaseBtn = document.getElementById('inclease');
    const count = document.getElementById('count');
 
    const increase = (function () {
        // 카운트 상태를 유지하기 위한 변수
        let counter = 0;
        // 클로저를 반환
        return function () {
            return ++counter;
        };
    }());
 
    incleaseBtn.onclick = function () {
        count.innerHTML = increase();
    };
</script>
</body>
</html>

 

 

728x90
블로그 이미지

Link2Me

,
728x90

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

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

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

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

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

 

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

이날 매도해야 한다.

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

 

 

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

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

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

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

 

 

위 차트를 살펴보자.

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

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

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

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

 

 

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

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

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

728x90
블로그 이미지

Link2Me

,
728x90

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

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

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

 

PDO 로 작성된 코드 예시

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

 

 

MySQLi 코드로 변경하는 방법

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

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

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

 

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

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

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

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

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

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

 

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

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

 

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

 

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

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

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

 

보조지표 추가

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

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

 

 

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

선택하고 적용을 누른다.

 

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

 

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

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

 

 

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

 

 

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

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

 

rsi 라고 입력한다.

 

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

 

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

 

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

 

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

 

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

 

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

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

 

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

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

 

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

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

 

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

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

 

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

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

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

 

728x90
블로그 이미지

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번 코드생성 버튼을 누른다.

 

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

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

728x90
블로그 이미지

Link2Me

,
728x90

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

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

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

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

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

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

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

 

728x90
블로그 이미지

Link2Me

,
728x90

삼성증권 기본화면

0517 관심종목

0350 주식투자자일자

1401 종합차트

 

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

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

 

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

 

MACD 기준선 매수 신호 설정값

728x90
블로그 이미지

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가지 항목을 추가했으면 조건식 저장 버튼을 눌러서 조건명을 지정해주고 확인하면 저장된다.

 

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

 

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

728x90
블로그 이미지

Link2Me

,
728x90

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

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

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

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

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

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

 

 

jQuery 코드

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

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

 

 

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

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

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

 

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

 

 

728x90
블로그 이미지

Link2Me

,
728x90

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

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

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

 

 

728x90
블로그 이미지

Link2Me

,
728x90

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

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

 

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

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

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

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

 

 

 

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

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

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

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

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

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

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

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

 

 

파일 첨부를 인식하는 코드

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

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

 

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

신고가

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

 

기간내 거래대금

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

 

기간내 등락률

 

728x90
블로그 이미지

Link2Me

,
728x90

일목균형표

 

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

 

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

 

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

 

일목균형표 기준선 비교

728x90
블로그 이미지

Link2Me

,
728x90

일 주기 : 일봉

0봉전 : 검색 당일

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

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

 

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

 

캔들연속발생

 

주가등락률

 

주가 비교

시가보다 종가가 높게 설정

 

주가이동평균추세

 

 

평균거래대금

 

주가범위

 

체결강도

 

주가비교

 

728x90
블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/2311

 

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

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

link2me.tistory.com

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

 

statsClass.php

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

 

 

stats_db.php

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

 

 

차트 출력 파일 : accessStats.php

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

 

 

728x90
블로그 이미지

Link2Me

,