안드로이드폰을 미러링으로 윈도우10에 연결하는 방법은 무척 쉬워졌다. (전용앱이 있지 아마???) 하지만 맥은 아직 유료 앱이 아니면 사용이 힘든 것으로 보인다. 라고 생각을 했는데... 약간의 수고를 하면 무료로 미러링을 할 수 있는 방법이 있네~

참조 URL : https://github.com/Genymobile/scrcpy

설치 조건

더보기

macOS Catalina Ver. 10.15.5
MacBook Pro 16인치 2019. i9 2.3GHz 8 core. RAM 32G

homebrew 설치

설치 스크립트

// 설치
brew install scrcpy

// adb 관련 PATH 추가
brew cask install android-platform-tools

안드로이드 USB 연결 (모바일 설정 : 개발자 옵션 On - USB 디버깅 on)

실행

// 실행
scrcpy

실행 중 첫 화면

곧 바로 연결이 된다. 신기하네. 하나 문제가 있다. 핸드폰 커버를 닫는 등으로 핸드폰 자체가잠금모드 상태가 되면 마우스가 동작을 하지 않는다. 이럴 경우에는 직접 잠금 모드를 해제 해야 한다.

마우스를 사용하려면 화면 위에서 오른쪽 버튼을 클릭한다. 이럴 경우 내 폰은 화면이 깜~빡! 하고 나서 부터 마우스가 사용이 가능하더라. 좋네 좋아.

설치시 문제점

#1. 설치 중 오류 메시지 이후 사망

// 오류 메시지
Error: Permission denied @ apply2files - /usr/local/lib/node_modules/vue/src/platforms/weex/.DS_Store

저 메시지 후에 설치가 된건지 안된건지는 실제로는 잘 모르겠다. 어쩌면 중간에 android-platform-tools에서 오타가 났을 거 같다는 생각도 있다. 어찌됐건 간에, 오류는 났고 멈춘거 같은 느낌이라 일단은 저 메시지를 없애려는 사명감만 있었다.

#1. 설치 오류 해결책

brew remove scrcpy

// 고전적인 엔진 설치
brew install sdl2 ffmpeg
brew install pkg-config meson
brew tap homebrew/cask-versions

// 이후 다시 재설치

 


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

설치

npm install amcharts3 --save

사용 샘플 소스

<template>
	<div id="chartdiv" style="width: 100%; height: 400px;" ref="chartdiv" />
</template>

<script>
import 'amcharts3'

import AmSerial from 'amcharts3/amcharts/serial'



export default {

	name: 'AmCharts',
	data: () => ({
		chartConfig: {
			type: 'serial',
			categoryField: 'type',

			chartCursor: {},

			graphs: [
			
	{
				
	type: 'column',
					title: 'Pizza types',

					valueField: 'sold',

					fillAlphas: 0.8

				}

			],

			dataProvider: [

				{ type: 'Margherita', sold: 120 },

				{ type: 'Funghi', sold: 120 },

				{ type: 'Capricciosa', sold: 120 },
				{ type: 'Quattro Stagioni', sold: 120 }
 
			]
		
}
	}),
	created() {
		AmCharts.makeChart(this.$refs.chartdiv, this.chartConfig)
	}
}
</script>

오류 수정

import AmCharts from 'amcharts3'

위 형태로 하면 AmCharts.makeChart() 함수를 찾을 수 없다는 오류가 나와서 수정

참조 URL

https://github.com/developerKumar/AmchartsUse


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

wsl2를 설치하고 docker desktop을 설치하려다 실패를 했는데, 사이드 이펙트로 VirtualBox가 정상적으로 동작하지 않는다. ㅠ,.ㅠ;

에러 메시지

Raw-mode is unavailable courtesy of Hyper-V.

해결 방법

// bcdedit 실행
bcdedit

// hypervisorlauchtype 확인
hypervisorlaunchtype = On

// Off 변경
bcdedit /set hypervisorlaunchtype off

// hyervisorlaunchtype 확인
// 리붓

리붓을 한 후에 VirtualBox를 다시 실행해 본다.


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

https://picory.com/entry/Laravel-Web-Push-%EB%B0%9C%EC%86%A1%EA%B8%B0

얼마 전에 테스트 중이던 웹 푸시 소스가 사용자를 일정 수준으로 넘어서면서 인지, 아니면 원래 그런 건지 모르겠지만, 메시지는 전송했다는데, 테스트 하는 사람들은 받지 못했다고 하는 상황이 자주 연출이 되었다. 전송했다는데? 왜 안오지? 나는 잘되던데... 하다가 나도 받지 못하는 상황이 생기니... 이거 너무 불안정한데?? 싶어서 FCM을 사용하기로 하고 다시 정리를 해본다.

설치

composer require brozot/laravel-fcm

config/app.php 설정

'providers' => [
	// ...

	LaravelFCM\FCMServiceProvider::class,
]

'aliases' => [
	...
	'FCM'      => LaravelFCM\Facades\FCM::class,
	'FCMGroup' => LaravelFCM\Facades\FCMGroup::class, // Optional
]
php artisan vendor:publish --provider="LaravelFCM\FCMServiceProvider"

.env 설정

FCM_SERVER_KEY=my_secret_server_key
FCM_SENDER_ID=my_secret_sender_id

Firebase Console[링크] 에서 프로젝트를 생성 후 [프로젝트 개요] 옆의 톱니바퀴(Settings) > 클라우드 메시징 페이지에 서버 키와 발신자 ID 값을 복사해서 추가

컨트롤러 샘플 코드

use LaravelFCM\Message\OptionsBuilder;
use LaravelFCM\Message\PayloadDataBuilder;
use LaravelFCM\Message\PayloadNotificationBuilder;
use FCM;

$optionBuilder = new OptionsBuilder();
$optionBuilder->setTimeToLive(60*20);

$notificationBuilder = new PayloadNotificationBuilder('my title');
$notificationBuilder->setBody('Hello world')
				    ->setSound('default');

$dataBuilder = new PayloadDataBuilder();
$dataBuilder->addData(['a_data' => 'my_data']);

$option = $optionBuilder->build();
$notification = $notificationBuilder->build();
$data = $dataBuilder->build();

// You must change it to get your tokens
$tokens = MYDATABASE::pluck('fcm_token')->toArray();

$downstreamResponse = FCM::sendTo($tokens, $option, $notification, $data);

$downstreamResponse->numberSuccess();
$downstreamResponse->numberFailure();
$downstreamResponse->numberModification();

// return Array - you must remove all this tokens in your database
$downstreamResponse->tokensToDelete();

// return Array (key : oldToken, value : new token - you must change the token in your database)
$downstreamResponse->tokensToModify();

// return Array - you should try to resend the message to the tokens in the array
$downstreamResponse->tokensToRetry();

// return Array (key:token, value:error) - in production you should remove from your database the tokens present in this array
$downstreamResponse->tokensWithError();

웹 푸시용 디바이스 토큰 수집 방법

console.firebase > settings > 일반 > 내 앱 섹션 > 앱 추가 > 웹앱 추가 시 웹 js 샘플 코드(snippet) 확인

<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.15.1/firebase-app.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/7.15.1/firebase-analytics.js"></script>

<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "[APIKEY]",
    authDomain: "[AUTHDOMAIN]",
    databaseURL: "[DATABASEURL]",
    projectId: "[PRODUCTID]",
    storageBucket: "[STORAGEBUCKET]",
    messagingSenderId: "[SENDERID]",
    appId: "[APPID]",
    measurementId: "[MEASUREID]"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
  firebase.analytics();
</script>

설정 후에 사용자 브라우저(디바이스)에 메시지를 전송하기 위해 토큰을 수집하는 작업을 하기 위해 ServiceWorker를 등록하는 작업을 해야 한다. 그리고 토큰 수집을 위해서 VAPID 키가 필요한데, 해당 키는 console.firebase > Settings > 클라우드 메시징 > 웹 구성 > 웹 푸시 인증서 에서 키를 생성해서 획득한다.

// 메시지 객체 획득
const messaging = firebase.messaging();

// 웹 푸시 인증 (VAPID 키)
messaging.usePublicVapidKey("BKagOny0KF_2pCJQ3m....moL0ewzQ8rZu");

// 현재 등록 토큰 검색
messaging.getToken().then((currentToken) => {
  if (currentToken) { // 토큰이 있으면 서버로 전송
    sendTokenToServer(currentToken);
    updateUIForPushEnabled(currentToken);
  } else {
    // Show permission request.
    console.log('No Instance ID token available. Request permission to generate one.');
    // Show permission UI.
    updateUIForPushPermissionRequired();
    setTokenSentToServer(false);
  }
}).catch((err) => {
  console.log('An error occurred while retrieving token. ', err);
  showToken('Error retrieving Instance ID token. ', err);
  setTokenSentToServer(false);
});


// 토큰 갱신 모니터링
messaging.onTokenRefresh(() => {
  messaging.getToken().then((refreshedToken) => {
    console.log('Token refreshed.');
    // Indicate that the new Instance ID token has not yet been sent to the
    // app server.
    setTokenSentToServer(false);
    // Send Instance ID token to app server.
    sendTokenToServer(refreshedToken);
    // ...
  }).catch((err) => {
    console.log('Unable to retrieve refreshed token ', err);
    showToken('Unable to retrieve refreshed token ', err);
  });
});

위 내용을 기준으로 메시지 전송을 위해서 Service Worker 용 소스를 정리한다.

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js')
      .then((registration) => {
          // 서버스 워커를 사용한다고 알림
          messaging.useServiceWorker(registration);

          // 이미 웹푸시를 승인한 회원의 경우 토큰이 바뀌었는지 확인
          messaging.onTokenRefresh(() => {
              getToken();
          });

          requestPermission();
      });
}

// 웹푸시 권한 요청
const requestPermission = () => {
    Notification.requestPermission()
      .then((permission) => {
          if (permission === 'granted') {
              // 웹푸시를 허용한 경우
              getToken();
          } else if (permission === 'denied') {
              // 웹푸시를 거절한 경우
          } else {
              console.log('push default');
          }
      });
};

const getToken = () => {
  // 토큰 수집
  messaging.getToken().then((currentToken) => {
    if (currentToken) {
      // 토큰이 있으면 추가 액션
    } else {
      // 없으면???
    }
  }).catch((err) => {
    console.log('An error occurred while retrieving token. ', err);
  });
}

참조 URL

https://developers.google.com/web/fundamentals/codelabs/push-notifications
https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko

 


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

history.back()
this.$router.go(-1)

UI 강제 업데이트
this.$forceUpdate()

// string replace all
let replaceAll = function (source, needle, target) {
    return source.split(needle).join(target)
}
// &lt; &gt;를 < > 변환
let decodeHTML = function (html) {
    let txt = document.createElement('textarea')
    txt.innerHTML = html
    return txt.value
}

 


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

6월 부터인가? 갑자기 카카오톡에 특정 사이트 URL을 전송하면 보여주던 이미지와 제목에서 이미지는 정상인데 제목이 문제가 되었다. 제목이 URL로만 보인다. 잘되는 것이 안되니 호기심이 생기네... 카카오톡은 어떤 것을 참조해서 제목을 가져오는 걸까?

일시적인 거지만 제목 대신 URL이 나오다가 다시 한글로 잘 나오더라.

카카오톡의 테스트 URL? 개발자 센터

일단 카카오톡 메시지로 전송이 되면 어떻게 되는지 미리 알수 있는 방법은 카카오톡 개발자 센터에 있다. 생뚱맞기는 하지만 카카오 스토리 > RESTAPI > 웹 페이지 스크롤링에서 테스트를 제공한다. [페이지 링크]

테스트 페이지

테스트는 간단하지만 개발자 센터에서 API 테스트기 때문에 액세스 토큰을 발급받아야 한다. 카카오톡 아이디만 있으면 되는 걸로 알고 있으니, 귀찮겠지만 테스트를 위해서 개발자센터에 등록 후 토큰을 발급 받으시면 된다.

테스트 결과. 정상으로 모두 나온다.

이제는 잘 나는 거 같다. 처음에 확인할때만 해도 title에 url이 나오고 있었는데... description은 공백이었고... 일찍 캡쳐해 놓을 껄...

여튼 저렇게 수집하는 기준은 어떤걸까? 바로 og (OpenGraph) 메타태그 이다.

카카오 개발자센터에 있는 웹 페이지 스크롤 관련 설명

이 페이지는 카카오톡 URL 전송이 정상적으로 안되는 경우 테스트하는 페이지라 og 관련 상세 설명은 생략한다. 페이스 북에서 시작을 했고 지금은 SEO(Search Engine Optimization, 검색엔진최적화)로 쓰이기 때문에 웹사이트에서는 매우 중요하다.

페이스 북이라고 했으니, 페이스북도 테스트URL이 있겠지? 당연히???? 있다. [페이지북 OG 테스트 페이지] 물론 여기도 개발자 센터이기 때문에 계정인증은 받아야 할 것이다.

해당 웹페이지에서 URL이 나온 이유와 해결 방안

카카오톡 수집기가 EUC-KR 문자셋을 지원하지 않거나 기본 UTF-8 세팅으로 변경이 된 것으로 보인다. 해당 사이트는 기본 EUC-KR 문자셋의 웹사이트 였는데, UTF-8로 바뀌었고, 웹 스크랩에서도 정상 동작을 하고 있으니, charset 이슈로 보인다.

아주 빠른 변경은 칭찬해~~

참조 URL

카카오톡 개발자 센터 > 웹 스크랩 테스트 : https://developers.kakao.com/tool/rest-api/open/v1/api/story/linkinfo/get

페이스북 개발자 센터 > 공유 디버거 : https://developers.facebook.com/tools/debug/?q=https%3A%2F%2Fbetanews.net%2Farticle%2F1184743


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

개발 중 자주 쿼리를 확인하는 일이 있다. 그런데 ->toSql()로 하게 되면 바인딩 전 쿼리만 나와서 해당 값이 정상 들어갔는지 확인이 어려울 수 있다.

이번에는 mysql 4.x old 버전에 쿼리 질의를 할 일이 생겼는데, php 5.x 버전 부터 mysql 4.x 버전을 지원하지 않고, 어느 언어를 사용하더라도 4.x 버전을 미지원이라는 장벽을 맞이하게 되었다. 결국 원격으로 php 5.x 저 버전에 raw 쿼리를 던져서 질의 하도록 미들웨어 개념의 api를 만들었다. 기본 어드민 툴은 laravel에서 사용을 해야 하니 raw query를 생성해서 던지는 작업을 해야하는 경우가 생긴 것이다.

머 이유가 어찌 됐건 일단 바인딩된 최종 raw query 획득을 위한 작업을 진행하기 위해서 설정 작업이 필요하다. Laravel에서 eloquent orm 이나 query builder로 쿼리를 실행하게 되면 QueryExecuted 이벤트가 발생을 한다. 해당 이벤트 발생시 실행된 쿼리를 수집하기 위해서는 AppServiceProvider.php에 추가를 한다.

Event 추가

// app/Provides/AppServiceProvider.php

use App\Http\Models\Query; // 추가한 Query class

    public function register()
    {
        //
        \Event::listen('Illuminate\Database\Events\QueryExecuted', function ($query) {
            Query::set($query);
        });
    }

 

Query Class 추가

class Query
{
    static $query;

    //
    static function get()
    {
        return self::debug_query();
    }

    static function set($query)
    {
        self::$query = $query;
    }
    
     /**
     * query build 내용을 raw query로 변경
     * @param $query
     * @return mixed|string
     */
    static function debug_query() {
        $queryRaw = self::$query;

        $query = vsprintf(str_replace('?', '%s', $queryRaw->sql), collect($queryRaw->bindings)->map(function ($binding) {
            return is_numeric($binding) ? $binding : "'{$binding}'";
        })->toArray());

        $double_linebreak_words = ['(', ')'];
        $double_linebreak_words_replace = array_map(function($str){ return PHP_EOL . $str . PHP_EOL; }, $double_linebreak_words);
        $query = str_replace($double_linebreak_words, $double_linebreak_words_replace, $query);


        $mysql_keywords = ['ADD', 'ALL', 'ALTER', 'ANALYZE', 'AND', 'AS', 'ASC', 'AUTO_INCREMENT', 'BDB', 'BERKELEYDB', 'BETWEEN', 'BIGINT', 'BINARY', 'BLOB', 'BOTH', 'BTREE', 'BY', 'CASCADE', 'CASE', 'CHANGE', 'CHAR', 'CHARACTER', 'CHECK', 'COLLATE', 'COLUMN', 'COLUMNS', 'CONSTRAINT', 'CREATE', 'CROSS', 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DATABASE', 'DATABASES', 'DAY_HOUR', 'DAY_MINUTE', 'DAY_SECOND', 'DEC', 'DECIMAL', 'DEFAULT', 'DELAYED', 'DELETE', 'DESC', 'DESCRIBE', 'DISTINCT', 'DISTINCTROW', 'DIV', 'DOUBLE', 'DROP', 'ELSE', 'ENCLOSED', 'ERRORS', 'ESCAPED', 'EXISTS', 'EXPLAIN', 'FALSE', 'FIELDS', 'FLOAT', 'FOR', 'FORCE', 'FOREIGN', 'FROM', 'FULLTEXT', 'FUNCTION', 'GEOMETRY', 'GRANT', 'GROUP', 'HASH', 'HAVING', 'HELP', 'HIGH_PRIORITY', 'HOUR_MINUTE', 'HOUR_SECOND', 'IF', 'IGNORE', 'INDEX', 'INFILE', 'INNER', 'INNODB', 'INSERT', 'INTEGER', 'INTERVAL', 'INTO', 'JOIN', 'KEY', 'KEYS', 'KILL', 'LEADING', 'LEFT', 'LIKE', 'LIMIT', 'LINES', 'LOAD', 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCK', 'LONG', 'LONGBLOB', 'LONGTEXT', 'LOW_PRIORITY', 'MASTER_SERVER_ID', 'MATCH', 'MEDIUMBLOB', 'MEDIUMINT', 'MEDIUMTEXT', 'MIDDLEINT', 'MINUTE_SECOND', 'MOD', 'MRG_MYISAM', 'NATURAL', 'NOT', 'NULL', 'NUMERIC', 'ON', 'OPTIMIZE', 'OPTION', 'OPTIONALLY', 'ORDER', 'OUTER', 'OUTFILE', 'PRECISION', 'PRIMARY', 'PRIVILEGES', 'PROCEDURE', 'PURGE', 'READ', 'REAL', 'REFERENCES', 'REGEXP', 'RENAME', 'REPLACE', 'REQUIRE', 'RESTRICT', 'RETURNS', 'REVOKE', 'RIGHT', 'RLIKE', 'RTREE', 'SELECT', 'SET', 'SHOW', 'SMALLINT', 'SOME', 'SONAME', 'SPATIAL', 'SQL_BIG_RESULT', 'SQL_CALC_FOUND_ROWS', 'SQL_SMALL_RESULT', 'SSL', 'STARTING', 'STRAIGHT_JOIN', 'STRIPED', 'TABLE', 'TABLES', 'TERMINATED', 'THEN', 'TINYBLOB', 'TINYINT', 'TINYTEXT', 'TO', 'TRAILING', 'TRUE', 'TYPES', 'UNION', 'UNIQUE', 'UNLOCK', 'UNSIGNED', 'UPDATE', 'USAGE', 'USE', 'USER_RESOURCES', 'USING', 'VALUES', 'VARBINARY', 'VARCHAR', 'VARCHARACTER', 'VARYING', 'WARNINGS', 'WHEN', 'WHERE', 'WITH', 'WRITE', 'XOR', 'YEAR_MONTH', 'ZEROFILL', 'INT', 'OR', 'IS', 'IN'];
        $mysql_keywords = array_map(function($str){ return " $str "; }, $mysql_keywords);
        $mysql_keywords_lc = array_map(function($str){ return strtolower($str); }, $mysql_keywords);
        $query = str_replace($mysql_keywords_lc, $mysql_keywords, $query);


        $linebreak_before_words = ['INNER JOIN', 'LEFT JOIN', 'OUTER JOIN', 'RIGHT JOIN', 'WHERE', 'FROM', 'GROUP BY', 'SELECT'];
        $linebreak_before_words_replace = array_map(function($str){ return PHP_EOL . $str; }, $linebreak_before_words);
        $query = str_replace($linebreak_before_words, $linebreak_before_words_replace, $query);


        $linebreak_after_words = [','];
        $linebreak_after_words_replace = array_map(function($str){ return $str . PHP_EOL; }, $linebreak_after_words);
        $query = str_replace($linebreak_after_words, $linebreak_after_words_replace, $query);

        $query = str_replace('select ', 'SELECT ', $query);

        return $query;
    }
 }

사용 방법

namespace App\Http\Models;

use User;
use Illuminate\Database\Eloquent\Model;

class test extends Model
{
    public index()
    {
        $user = new User;
        
        $user->first();
        
        $query = Query::get();
        
        echo $query;
    }
}

결과

SELECT * FROM `users` LIMIT 1

 

 

 


WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret

n일 이전 생성 파일 보기 및 삭제

# n일 이전 생성 파일 보기
find 폴더/ -mtime +n
find 폴더/ -mtime +n -print

# n일 이전 생성 파일 삭제
find 폴더/ -mtime +n -delete
find 폴더/ -mtime +n -exec rm {} \;

WRITTEN BY
비트센스
뷰파인더로 보는 프로그래머의 세상 페이스북 @bitsense 트위터 @picory 스카이프 picory MSN drawhalf@dreamwiz.com

트랙백  0 , 댓글  0개가 달렸습니다.
secret