NodeJS 서버에서 안드로이드 인앱결제 유효성 검증을 하는 방법

  • 구글 안드로이드용 앱에서 인앱결제를 했을 경우에, 결제정보가 리턴되는데요.
  • 리턴된 값이 제대로된 값인지 혹시 위조된 것은 아닌지 검증할 필요가 있습니다.
  • 제가 여기저기 검색을 해본 결과, 안되는 내용을 올려놓은 포스팅들이 있더군요.
  • 2024년 4월 현재, 지금 소개하는 방법은 사용가능한 것입니다.
  • 사전에 처리해야 하는 과정은 생략할게요, 그 부분은 다른데서 검색하셔도 됩니다.
  • 서비스계정 만들고, 플레이스토어에서 계정 연동 시키는 등의 과정은 같아요.
const {google} = require('googleapis')
const path = require('path')

// 서비스 계정 만들면서 다운로드 받은 json 파일이 필요합니다
const realPath = path.join(__dirname, '../XXXXXXX.json')
const auth = new google.auth.JWT(
    "서비스계정 이메일 주소",
    null,
    require(realPath).private_key,
    // 아래 scopes 꼭 지정해야 하구요
    ['https://www.googleapis.com/auth/androidpublisher'],
    null
)

google.options({auth: auth})

const iap = google.androidpublisher('v3')
const packageName = "앱의 패키지 네임이 들어갑니다"
try {
    const resp = await iap.purchases.products.get({
        packageName: packageName,
        productId: "플레이스토어에서 등록한 상품ID",
        token: "검증하고자 하는 결제 정보의 purchaseToken 값",
    })
    if (resp.data.orderId === "결제ID") {
        // 올바른 결제 정보
    }
} catch (e) {
    console.error(e)
}

Flutter에서 google_sign_in 라이브러리 이용해서 캘린더 API 연동하기(1)

  • 구글 캘린더 많이 사용하시나요? 저는 그것 만큼 직관적인 UI를 가진 스케쥴 앱을 못 찾겠더군요.
  • 그래서 몇 년째 애용하고 있는데요. 회사에서는 팀장, 블로그와 유튜버 크리에이터 그리고 육아대디로 활동하다보니 스케쥴 만들고 수정하는 것도 하나의 스케쥴이 되어 버렸어요.
  • 저의 이런 고충을 이야기해 본 포스팅이 아래 링크로 있구요.
  • 위 포스팅에서도 이야기 했지만, 구글 캘린더에 음성인식 기술을 이용해서 이벤트 추가하는게 생각보다 어렵더군요.
  • 그래서 제가 직접 만들어보려고 합니다. 오늘은 그 중 첫번째 시간으로 제가 만들어서 관리중인 캘린더 리스트를 뽑아오는 것 까지 해보겠습니다.
  • 아래 코드를 보시면 되겠구요. 생각보다 간단합니다. 앱을 시작하면 구글 로그인을 해서 권한 설정을 하고, 모두 완료되면 콘솔에 프린트를 하도록 해놨습니다.
import 'dart:convert';


import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;


void main() {
 runApp(const MyApp());
}
class MyApp extends StatelessWidget {
 const MyApp({super.key});

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
       useMaterial3: true,
     ),
     home: const MyHomePage(title: 'Flutter Demo Home Page'),
   );
 }
}


class MyHomePage extends StatefulWidget {
 const MyHomePage({super.key, required this.title});

 final String title;


 @override
 State<MyHomePage> createState() => _MyHomePageState();
}


class _MyHomePageState extends State<MyHomePage> {
 int _counter = 0;
 List<String> items = [];
 final _googleSignIn = GoogleSignIn(
   scopes: [
     'https://www.googleapis.com/auth/calendar',
   ],
 );
 GoogleSignInAccount? _currentUser;
 late Map<String, dynamic> cals;


 @override
 void initState() {
   super.initState();
   _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) {
     setState(() {
       _currentUser = account;
       if (_currentUser != null) {
         _getCalendarList();
       }
     });
   });
   _googleSignIn.signIn().then((GoogleSignInAccount? account) {
     if (account != null) {
       setState(() {
         _currentUser = account;
         _getCalendarList();
       });
     }
   });
 }


 void _getCalendarList() async {
   http.Client client = http.Client();
   var headers = await _currentUser?.authHeaders;
   var resp = await client.get(Uri.parse("https://www.googleapis.com/calendar/v3/users/me/calendarList"), headers: headers);
   cals = jsonDecode(resp.body);
   for(var i=0;i<cals['items'].length;i++) {
     print(cals['items'][i]['summary']);
   }
 }


 void _incrementCounter() {
   setState(() {
     for(var i=0;i<cals['items'].length;i++) {
       print(cals['items'][i]['summary']);
     }
   });
 }


 @override
 Widget build(BuildContext context) {   
   return Scaffold(
     appBar: AppBar(       
       title: Text(widget.title),
     ),
     body: Center(
       child: Column(         
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           const Text(
             'You have pushed the button this many times:',
           ),
           Text(
             '$_counter',
             style: Theme.of(context).textTheme.headlineMedium,
           ),
         ],
       ),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: _incrementCounter,
       tooltip: 'Increment',
       child: const Icon(Icons.add),
     ),
   );
 }
}
  • 여기서 주목할 점은 우리가 사용하는 캘린더 상의 이름은 summary 라는 점 이에요.
  • package.yaml 파일에 아래 의존성 정의 추가하세요.
    • google_sign_in: ^6.2.1
  • 이러면 아래처럼 출력이 됩니다.
    • 이미지1

MediaPipe와 Selenium을 이용해서 웹 CCTV 영상속 고양이 포착하기

  • MediaPipe 유용하게 쓰고 있나요? 저는 이것 저것 잘 쓰고 있습니다.
  • 물체인식이나 이미지분류, 얼굴인식, 얼굴 표시게 인식 등 정말 잘 쓰고 있어요.
  • 그 중에서도 오늘은 물체인식(Object Detection)에 관한 내용입니다.
  • 예전에 Flutter를 이용해서 비슷한 이슈를 다룬적이 있으니 아래 링크 참고해주시면 감사하겠구요.
  • 오늘은 이미 만들어져 있는 DIY CCTV 웹 모니터링 화면을 Selenium Webdriver를 이용해서 캡쳐한 다음
  • 캡쳐 화면속에 고양이가 있으면 해당 화면의 사진을 저장하는 프로그램을 만들어 봤습니다.
  • 매우 간단하니까 한 번 코드리뷰를 해봐주시면 감사하겠습니다.

  • CCTV는 라즈베리파이를 이용해서 자작한 건데요. 이것과 관련한 사연은 아래 블로그 글과 유튜브 영상을 참고해주세요.
  • 원격에서 확인할 수 있도록 만들어진 CCTV 모니터링 페이지에 접속하면 이런 화면이 나타납니다.
    • 화면
    • 위와 같이 고양이가 있으면 캡쳐해서 저장하면 되는거에요.
  • 코드는 아래와 같습니다. 아주 간단하죠??
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

options = Options()
# 브라우저 창을 띄우지 않고 처리 합니다.
options.add_argument("--headless")

driver = webdriver.Chrome(options=options)

# 모니터링 페이지의 URL이 들어갑니다.
driver.get("http://")

# https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite0/int8/1/efficientdet_lite0.tflite 파일을 다운로드 해서 이름만 바꿨어요.
base_options = python.BaseOptions(model_asset_path='cat.tflite')
options = vision.ObjectDetectorOptions(base_options=base_options,
                                       score_threshold=0.5)
detector = vision.ObjectDetector.create_from_options(options)
i = 0
while True:
    # 5초마다 수행됩니다.
    sleep(5)
    driver.save_screenshot("screenshot.png")
    image = mp.Image.create_from_file("screenshot.png")
    detection_result = detector.detect(image)
    if len(detection_result.detections) > 0 and \
            detection_result.detections[0].categories is not None:
        for item in detection_result.detections[0].categories:
            # cat이 들어가면 일단 걸립니다. 판독가능한 레이블에 cat이란 단어가 들어가는건 말그대로 cat 밖에 없기 때문에 == 을 써도 무방합니다.
            # 레이블 리스트 : https://storage.googleapis.com/mediapipe-tasks/object_detector/labelmap.txt
            if 'cat' in item.category_name:
                print(item.category_name)
                i = i + 1
                driver.save_screenshot("screenshot_%s.png" % i)
                break