728x90

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

 

notification 기능도 구현하기 나름인것같다.

firebase의 remote notification만 있으면

만능일줄 알았건만

 

local notification을 쓸일이 생겨

지웠던 라이브러리를 다시 설치하고

셋팅하는 상황이 발생했다.

 

워낙 간단했지만

그래도 필요한 이들을 위해서 공유한다.

(혹은, 미래의 나를 위해...)

 

먼저, remote notification이

구현되어 있음에도 local notification이 필요했던 이유는

앱 안에서 채널톡 SDK를 통해

채널톡이 구현되어있는데...

 

운영자가 답장을 해줘도 사용자가 답장이 왔는지

알길이 없는 상황에 봉착했다.

(알림을 주던지, 카톡을 주던, 문자를 보내주던

무언가는 있어야지

사용자가 답변이 왔음을 알것이다.)

 

그래서 나는

setBackgroundMessagehandler에서

ChannelIO.isChannelPushNotification 메소드로

채널톡 메시지가 수신되었음이 감지됐을때

local에서 notification을 발생시켜

사용자에게 알려주는 전략을 취하기로했다.

 

// https://developers.channel.io/docs/mobile-channel-io#ischannelpushnotification

import messaging from '@react-native-firebase/messaging';
import {ChannelIO} from 'react-native-channel-plugin';

componentDidMount() {
    this.backgroundMessageHandler = messaging().setBackgroundMessageHandler(
      async (remoteMessage) => {
        ChannelIO.isChannelPushNotification(remoteMessage.data).then((result) => {
            if (result) {
              ChannelIO.receivePushNotification(remoteMessage.data).then((_) => { });
            } else {
              // TODO : Your FCM code
            }
          },
        );
      },
    );
}

componentWillUnmount() {
  this.backgroundMessageHandler();
}

 

채널톡에서도 그럴때 쓰라고 만들어 둔것인지

이렇게 예제를 제공하기도 한다.

 

result === true일때 

local notification이 작동되도록 코드를 설정했으며

아래와 같이

local notification을 세팅할수있다.

 

1. 설치 ( >= 0.60)

// react-native-push-notification 설치
npm install --save react-native-push-notification

// @react-native-community/push-notification-ios 설치
npm i @react-native-community/push-notification-ios --save

// pod file download
cd ios && pod install && cd..
// --- android ---

// android/app/src/main/AndroidManifest.xml
.....
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application ....>
        <!-- local notification code -->
        <meta-data  android:name="com.dieam.reactnativepushnotification.notification_foreground" android:value="false"/>
        <meta-data  android:name="com.dieam.reactnativepushnotification.notification_color" android:resource="@color/white"/> <!-- or @android:color/{name} to use a standard color -->

        <!-- common notification code -->
        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.QUICKBOOT_POWERON" />
                <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
            </intent-filter>
        </receiver>

        <!-- common notification code -->
        <service
            android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
            android:exported="false" >
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
     .....
// ios

// AppDelegate.h
// --- local notification code ---
#import <UserNotifications/UNUserNotificationCenter.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
UNUserNotificationCenterDelegate <-- 이거 추가됨




// AppDelegate.m
// --- local notification code ---
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>


// --- local notification code ---
// Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
 [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Required for the notification event. You must call the completion handler after handling the remote notification.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
  [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
// Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
 [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error];
}
// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler
{
  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  // Define UNUserNotificationCenter
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  center.delegate = self;

  return YES;
}


//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}

 

 

2. 코드 작성

import PushNotificationIOS from '@react-native-community/push-notification-ios';
import PushNotification from 'react-native-push-notification';

type NotiOptoin = {
  title: string;
  message: string;
}

export class LocalNotification {
  private channel: any;

  constructor() {
    this.init();
  }

  private init() {
  // Must be outside of any component LifeCycle (such as `componentDidMount`).
    PushNotification.configure({
      // (optional) Called when Token is generated (iOS and Android)
      onRegister: function (token) {
        // console.log('TOKEN:', token);
      },

      // (required) Called when a remote is received or opened, or local notification is opened
      onNotification: function (notification) {
        // console.log('NOTIFICATION:', notification);

        // process the notification

        // (required) Called when a remote is received or opened, or local notification is opened
        notification.finish(PushNotificationIOS.FetchResult.NoData);
      },

      // (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android)
      onAction: function (notification) {
        // console.log('ACTION:', notification.action);
        // console.log('NOTIFICATION:', notification);

        // process the action
      },

      // (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS)
      onRegistrationError: function (err) {
        console.error(err.message, err);
     },

      // IOS ONLY (optional): default: all - Permissions to register.
      permissions: {
        alert: true,
        badge: true,
        sound: true,
      },

      // Should the initial notification be popped automatically
      // default: true
      popInitialNotification: false,

      /**
      * (optional) default: true
      * - Specified if permissions (ios) and token (android and ios) will requested or not,
      * - if not, you must call PushNotificationsHandler.requestPermissions() later
      * - if you are not using remote notification or do not have Firebase installed, use this:
      *     requestPermissions: Platform.OS === 'ios'
      */
      requestPermissions: false,
    });

    PushNotification.createChannel({
      channelId: 'com.myApp',
      channelName: '앱이름',
    }, (created) => {
      console.log('noti channeld is created')
    })
  }

  fire = (option: NotiOptoin) => {
    PushNotification.localNotification({
      title: option.title,
      message: option.message,
      largeIcon: "ic_launcher",
      smallIcon: "ic_launcher",
      bigLargeIcon: "ic_launcher",

      /* Android Only Properties */
      channelId: "com.myApp", // (required) channelId, if the channel doesn't exist, notification will not trigger.
      vibrate: true,
      vibration: 300, // vibration length in milliseconds, ignored if vibrate=false, default: 1000
      priority: 'high',
      
      /* iOS and Android properties */
      id: 0, // (optional) Valid unique 32 bit integer specified as string. default: Autogenerated Unique ID
      playSound: true, // (optional) default: true
      soundName: "default", // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played)
    });
  }
}

export {PushNotification};

* 주의 : 채널을 생성해주지 않으면 알림이 뜨지 않습니다...

 

 

3. 코드 연동

const localNoti = new LocalNotification();
messaging().setBackgroundMessageHandler(async remoteMessage => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
  console.log('Message handled in the background!', remoteMessage);
  
  ChannelIO.isChannelPushNotification(remoteMessage.data).then(result => {
    if (result) {
      localNoti.fire({ title: '제목', message: '메시지' });
    } else {
      // TODO : Your FCM code
    }
  });
});

참고로,

Platform.OS === 'ios' && Vibration.vibrate([400]);

이 부분은 이 전편에서 다루었던 내용으로

ios에서 백그라운드 알림에 대한

진동이 울리지않아서

트릭용으로 추가한 코드입니다.

 

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

728x90
반응형
728x90

ios에서 App이 종료된 상태에서

push notification이 왔을대

메시지를 클릭해 앱을 여는 과정에서

앱이 splash screen 상태에서 멈춰버렸다가

결국 뻗어버리는 이슈를 경험했다.

 

삽질에 삽질을 더한결과

notification 문제는 아니고

react-native-splash-screen의 이슈였다.

 

https://github.com/crazycodeboy/react-native-splash-screen/issues/397

 

위 링크의 가이드가

99% 같은 내용이라

내게도 해결책이 될것이라고 판단했다.

 

(그외 참고 자료들)

https://github.com/crazycodeboy/react-native-splash-screen/issues/488

https://stackoverflow.com/questions/66598301/react-native-firebase-push-notification-crash-report-problem

https://ios-development.tistory.com/384

 

 

그러나 또

적용 과정에서 문제가 발생했으니...

바로 앱을 실행하면 바로 뻗어버리는 현상이었다.

 

왜 그럴까?

이슈를 찾다가 자꾸 시간이 지연되어

 

그냥 라이브러리없이

splashscreen을 구현하는 방향으로

돛을 돌렸다.

 

 

728x90
반응형
728x90

xcode에 디바이스 연결 후

테스트를 하려다보니

빌드는 성공적으로 완료되었는데

 

failed to get the task for process

라는 에러가 등장했다.

 

[ 솔루션 ]

Build Settings -> code signing identity -> debug -> "ios Develpoer"

 

"ios Developer" 부분이 핵심이다

저 부분이 "ios Distribution"으로 되어있을때 에러가 발생했었다.

 

728x90
반응형
728x90

*** nginx는 이미 설치되어있다는 가정

 

 

php 설치

// php install
yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm 
yum -y install epel-release yum-utils
yum-config-manager --enable remi-php74
yum install php php-mysql php-fpm

// set address
nano /etc/php-fpm.d/www.conf

listen=localhost:9000 -> listen=/var/run/php-fpm/php-fpm.sock

// start
systemctl start php-fpm
systemctl enable php-fpm.service

// nginx setting
server {
    server_name my_domain;
    client_max_body_size 5G;

    #charset koi8-r;
    access_log  /var/log/nginx/my_service.access.log;

    root /usr/share/nginx/html/my_service;

    location / {
        index index.html index.htm index.php;
    }

    location = /50x.html {
        root /usr/share/nginx/html;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/my_service$fastcgi_script_name;
        include fastcgi_params;
    }
}

systemctl restart nginx

ERR 403 Page

// 403 Page error
2021/12/20 23:38:06 [error] 2157#2157: *29 "/usr/share/nginx/html/my_service/index.php" is forbidden (13: Permission denied), client: 218.235.68.160, server: my_domain, request: "GET / HTTP/1.1", host: "my_domain"

// dir 및 하위 파일 권한 체크
ls -lZd /service_root/my_service

// dir 및 하위 파일 권한 변경
chcon -R -t httpd_sys_content_t /service_root/my_service

 

ERR mysql :: Connect Error: Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

// mysql install
yum -y install http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
yum -y install mysql-community-server 
systemctl start mysqld
systemctl enable mysqld

// find your  temporary password (ex: XcheTa.X-5o1)
/var/log/mysqld.log

// change db password
ALTER USER 'root'@'localhost' IDENTIFIED BY 'XcheTa.X-5o1';
FLUSH PRIVILEGES;
use mysql;
UPDATE user set authentication_string=password('newpassword') where user='root';
-> policy error?
  SET GLOBAL validate_password_policy=LOW;
  UPDATE user set authentication_string=password('newpassword') where user='root';
FLUSH PRIVILEGES;

// mysql exit
quit;

 

필요시 mysql 계정 생성
db생성 하여 쓰면됩니다.

 

ERR -> /install/ajax.install.check.php

// 버전 이슈일 확률이 높음 -> php 7.4 재설치 진행

 

ERR unix:/var/run/php-fpm.sock failed (13: permission)

// /etc/php-fpm.d/www.conf <-- 약간씩 다를 수 있음
user = nginx
group = nginx

listen.owner = nginx
listen.group = nginx
listen.mode = 0660

// 실행
systemctl restart nginx
systemctl restart php-fpm

 

ERR 로그인 에러 / data 접근 권한 에러

// SElinux 에러
// SElinux는 특정 서비스에 대한 권한을 필요한 만큼 허용하고 이외에는 모두 차단하는 정책을 가지고있어, 서비스의 취약성을 이용한 공격이 발생해도 관계된 프로세스나 파일 시스템에 쉽게 접근하지 못하도록 사전에 차단하는 역할을 함.
// 따라서, 완정 중지를 하게되면 보안상 문제가 발생할 수 있음
// 아래와 같이 임시 비활성화 처리가 가능하다.
// Permissive -> 0, Enforcing -> 1
setenforce 0

 

ERR php mb_string

// 관련 내용: https://zetawiki.com/wiki/CentOS_php-mbstring_%EC%84%A4%EC%B9%98


// 확인
php -r "mb_substr();"
-> PHP Fatal error:  Call to undefined function mb_substr() in Command line code on line 1

rpm -qa php-mbstring
yum info php-mbstring | grep Repo
-> Repo        : base

// 설치
yum install php-mbstring -y

// 설치자료 확인
rpm -qa php-mbstring
-> php-mbstring-5.3.3-3.el6_2.8.x86_64


// 재실행
systemctl restart nginx
systemctl restart php-fpm
728x90
반응형
728x90

"늘 그랬듯이"

라는 생각은 개발자에게 치명적이다.

 

그 현상을 더 이상

해결해야될 문제로 보지않고

안주하게 만들어버린다.

 

나에게 server.js 스크립트는

그런 대상이다.

 

수 많은 MVP 모델을 만들면서

server.js를 어떻게 구조화할지는

크게 고민하지않앗다.

 

express가 너무나도 쉽게 서버를 만들어주기에

필요할때 route만 더 붙여준다던지

cors를 설정해준다던지 하면 끝이었다.

 

// server.js
const express = require('express');
const api_v1 = require('../api/v1');

const app = express();
const http = require('http').createServer(app);

app.use('/api/v1/', api_v1);
http.listen(process.env.PORT, () => {
  console.log('server is listening on', process.env.PORT);
});

 

간단하게는

이정도면 충분히 훌륭한 앱서버가 된다.

 

문제는

서버에서 담당하는 역할이 많아지면서

여러가지가 붙다보면

코드가 지저분해지고

더이상 보기 싫은 스크립트가 되어버린다.

 

// server.js
const express = require('express');
const { Server } = require('socket.io');
const api_v1 = require('../api/v1');
const config = require('./config/server');

const app = express();
const http = require('http').createServer(app);
const io = new Server(http, {});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser());

app.use(function (req, res, next) {
  const allowedOrigins = config.allowedOrigin;
  const origin: any = req.headers.origin;
  if (allowedOrigins.indexOf(origin) > -1) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.header('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,PATCH');
  res.header('Access-Control-Allow-Credentials', 'true');
  req.method === 'OPTIONS' ? res.sendStatus(200) : next();
});

app.use('/api/v1/', api_v1);

io.sockets.on('connection', (socket) => {
  console.log('socket is connected..!');
});

http.listen(process.env.PORT, () => {
  console.log('server is listening on', process.env.PORT);
});

 

이정도만 되어도 보기싫어지기 시작한다.

우리는 class로 스크립트를 새로 짜보도록 하자.

 

// server.js
const express = require('express');
const { Server, createServer } = require('http');
const { Server as ioServer } = require('socket.io');
const api_v1 = require('../api/v1');
const config = require('./config/server');

const app = express();
const http = require('http').createServer(app);
const io = new Server(http, {});

class Server {
  private app: express.Aplication;
  private server: Server;
  private io: ioServer;
  
  constructor() {
    this.createApp();
    this.createServer();
    this.sockets();
    this.configure();
    this.routes();
    this.listen();
  }
  
  private createServer = (): void => {
    this.server = createServer(this.app);
  }
  
  private createApp = (): void => {
    this.app = express();
  }
  
  private sockets = (): void => {
    this.io = new ioServer(this.server);
  }
  
  private routes = (): void => {
    app.use('/api/v1/', api_v1);
  }
  
  private configure = (): void => {
    app.use(express.urlencoded({ extended: true }));
    app.use(express.json());
    app.use(cookieParser());

    app.use(function (req, res, next) {
      const allowedOrigins = config.allowedOrigin;
      const origin: any = req.headers.origin;
      if (allowedOrigins.indexOf(origin) > -1) {
        res.setHeader('Access-Control-Allow-Origin', origin);
      }
      res.header('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type, Accept');
      res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,PATCH');
      res.header('Access-Control-Allow-Credentials', 'true');
      req.method === 'OPTIONS' ? res.sendStatus(200) : next();
    });
  }
  
  private listen = (): void => {
    this.server.listen(process.env.PORT, () => {
      console.log('server is listening on', process.env.PORT);  
    });
    
    io.sockets.on('connection', (socket) => {
      console.log('socket is connected..!');
    });
  }
}

const App = new Server();

 

이렇게 정리해놓고 보니

목적에 맞는 코드끼리 분리되어

유지보수가 확실히 수월해진듯한 느낌이든다.

 

코드를 짤때는 내 코드를

함께 공유할 개발자도 배려하는 자세를 갖도록 해야겠다.

728x90
반응형
728x90

객체지향 공부에 이어

자료구조에 대해 간단히 짚어보았다.

 

공부했던 객체지향 개념을 적용해서

직접 스택을 구현해본 결과

 

interface Stack {
  readonly size: number;
  pop(): string;
  push(value: string): void;
}
  class StackTest1 implements Stack {
    private obj: any = {};
    private _size: number = 0;

    get size(): number {
      return this._size;
    }

    push = (value: string) => {
      this.obj[this.size.toString()] = value;
      this._size++;
    };

    pop = () => {
      this._size--;
      const value = this.obj[this._size.toString()];
      delete this.obj[this._size.toString()];
      return value;
    };
  }

  const stack = new StackTest1();
  stack.push("jk");
  stack.push("zl");
  stack.push("steve");
  while (stack.size !== 0) {
    console.log(stack.pop());
  }
type StackNode = {
    value: string;
    next?: StackNode;
  };

  class StackTest2 implements Stack {
    private _size: number = 0;
    private head?: StackNode;

    get size(): number {
      return this._size;
    }

    push = (value: string) => {
      const node = { value, next: this.head };
      this.head = node;
      this._size++;
    };

    pop = (): string => {
      if (this.head == null) {
        throw new Error("no more data");
      }

      const node = this.head;
      const value = this.head.value;
      this.head = node.next;
      this._size--;

      return value;
    };
  }

  const stack2 = new StackTest2();
  stack2.push("value1");
  stack2.push("value2");
  stack2.push("value3");
  while (stack2.size !== 0) {
    console.log(stack2.pop());
  }

위 두가지 방식으로 해보게됐다.

첫번쨰 방식은

올바른 방식이라기 보다는

이렇게도 할 수 있다 정도로만 봐야될것같다.

 

2번은 단일 연결 list라는 자료구조의 개념을 도입하여

스택을 구현한 예제이다.

728x90
반응형
728x90

객체지향 프로그래밍을

"잘" 해보기위해

처음부터 다시 공부중이다.

 

상속을 쓰다보면

늘 "객체지향"라는 개념이 혼동되었다.

 

내 기억속의 "객체지향"은

마치 자동차를 만드는 작업과 같았다.

 

예를들면,

스팅어가 탄생하는 과정을 한번 그려보자.

 

[상속의 개념을 적용한 예시]

a: "우리 스팅어라는 자동차를 만들거야,

아주 빠르고 스포티한 자동차지"

b: "그러면 엔진인 gv80에서 상속받고

타이어랑 엔진은 g70에서 상속받고...

뼈대는 탄탄하게 모하비에서 상속받도록 할까요?"

 

하지만, 실제로는 그렇지않다.

아주 작은 부품들이 모여서 하나의 덩어리 부품를 만들고.

그 덩어리 부품들이 모여서 하나의 자동차를 만드는 것이다.

 

이렇게 부품을 만들고

부품을 가지고 새로운 부품을 "구성" 하는것은

Composition(구성) 이라고 한다.

 

상속이 쓸모 없느것은 아니지만

먼저 어떻게 Composition을 할지 생각해보고

다음에 그러면 공통적인 무언가만 상속하는 형태로

가져가는게 좋지 않을까

하는 개발론적인 생각을 해보았다.

 

그럼, Composition(구성)의 개념을 적용하여

스팅어와 소나타를 만드는 과정을 코드로 살펴보자.

Github링크: https://github.com/jaekwangLee/oop_js_study

{
  interface Engine {
    run(): boolean;
    stop(): boolean;
    pressure: string; // 'weak' | 'strong' | 'normal';
  }

  interface Mission {
    mode: string; // auto | manual
    change(gear: number): number;
    clutch(): void;
    getGear(): number;
  }

  interface Frame {
    material: string; // Materials
    strength(): string;
    size(): { length: number; width: number; height: number };
    crash(): string;
  }

  interface Tire {
    brand: string;
    purpose: string;
    count: number;
    role(): void;
    pause(): void;
  }

  type Materials = "stainless" | "steel" | "aluminium";
  type TirePurpose = "winter" | "summer" | "for season";

  class GDI implements Engine {
    constructor(private hp: number, private torque: number) {}

    pressure = "strong";

    private heating = () => {
      console.log("heating ...♨");
    };

    private cooling = () => {
      console.log("cooling ...❄️");
    };

    run = () => {
      this.heating();
      console.log("engine is working with ", this.hp, this.torque);
      console.log("use power ", this.pressure);
      return true;
    };

    stop = () => {
      this.cooling();
      console.log("engine is stopping");
      console.log("power to zero ");
      return false;
    };
  }

  class MPI implements Engine {
    constructor(private hp: number, private torque: number) {}

    pressure = "weak";

    private heating = () => {
      console.log("heating ...♨");
    };

    private cooling = () => {
      console.log("cooling ...❄️");
    };

    run = () => {
      this.heating();
      console.log("engine working with ", this.hp, this.torque);
      console.log("use power ", this.pressure);
      return true;
    };

    stop = () => {
      this.cooling();
      console.log("engine is stopping");
      console.log("power to zero ");
      return false;
    };
  }

  class CVT implements Mission {
    mode = "";
    constructor(private gear: number, mode: "manual" | "auto") {
      this.mode = mode;
    }

    clutch = () => {
      console.log("press clutch!! ䷮");
    };

    change = (nextGear: number) => {
      if (nextGear > this.gear) return this.gear;
      if (nextGear <= 0) return this.gear;

      this.clutch();

      this.gear = nextGear;
      return this.gear;
    };

    getGear = () => {
      return this.gear;
    };
  }

  class BodyOnFrame implements Frame {
    constructor(
      private length: number,
      private width: number,
      private height: number
    ) {}

    material = "steel";

    strength = () => {
      return "hard";
    };

    size = () => {
      return {
        length: this.length,
        width: this.width,
        height: this.height,
      };
    };

    crash = () => {
      const strength = this.strength();
      switch (strength) {
        case "weak":
          return "die";
        case "hard":
          return "safe";
        default:
          return "injury";
      }
    };
  }

  class UniBodyFrame implements Frame {
    constructor(
      private length: number,
      private width: number,
      private height: number
    ) {}

    material = "aluminum";

    strength = () => {
      return "normal";
    };

    size = () => {
      return {
        length: this.length,
        width: this.width,
        height: this.height,
      };
    };

    crash = () => {
      const strength = this.strength();
      switch (strength) {
        case "weak":
          return "die";
        case "hard":
          return "safe";
        default:
          return "injury";
      }
    };
  }

  // 작업중
  class FourSeasonTire implements Tire {
    brand = "";
    purpose = "";
    count = 0;
    characters = [];

    constructor(brand: string, purpose: string, count: number) {
      this.brand = brand;
      this.purpose = purpose;
      this.count = count;
    }

    role = () => {
      console.log("rolling ...🚗");
    };

    pause = () => {
      console.log("pause with break ...🛑");
    };
  }

  class SummerTire implements Tire {
    brand = "";
    purpose = "";
    count = 0;

    constructor(
      brand: string,
      purpose: string,
      count: number,
      private characters?: Array<string>
    ) {
      this.brand = brand;
      this.purpose = purpose;
      this.count = count;
    }

    role = () => {
      console.log("rolling ...🚗");
    };

    pause = () => {
      console.log("pause with break ...🛑");
    };
  }

  class Car {
    constructor(
      private tire: Tire,
      private frame: Frame,
      private engine: Engine,
      private mission: Mission,
      private name: string
    ) {}

    turnOn = () => {
    console.log('Hi, myname is ', this.name);
      this.engine.run();
    };

    turnOff = () => {
      this.engine.stop();
    };

    start = () => {
      if (this.mission.mode === "manual" && this.mission.getGear() <= 0) {
        console.log("please, change the gear");
      } else {
          console.log("car is starting ~~ 🚗");
        this.tire.role();
      }
    };

    stop = () => {
      if (this.mission.mode === "manual" && this.mission.getGear() > 0) {
        this.mission.clutch();
        this.mission.change(0);
      }

      console.log("car is stopping ~~ 🚗");
      this.tire.pause();
    };
  }

  // make car !
  // tires
  const normalTires = new FourSeasonTire("Nexen", "four season", 4);
  const summerTires = new SummerTire("Kumho", "summer", 4);
  // engine
  const gdi = new GDI(320, 52);
  const mpi = new MPI(180, 23);
  // mission
  const cvt4 = new CVT(6, "auto");
  const cvt16 = new CVT(12, "auto");
  // frame
  const unibody = new UniBodyFrame(4950, 1900, 1720);
  const bodyon = new BodyOnFrame(4920, 1870, 1700);

  const sonata = new Car(normalTires, bodyon, mpi, cvt4, "sonata");
  const stinger = new Car(summerTires, unibody, gdi, cvt16, "stinger");

  sonata.turnOn();
  sonata.start();
  console.log('---------------')
  stinger.turnOn();
  stinger.start();
}

조금 길어보이지만 Composition의 개념을 통해

엔진/미션/타이어/뼈대로 구성된 자동차를 만드는 과정이다.

 

코드에서 알수 있듯이

각 부품멸 "설계도"에 해당하는 interface를 만들고

"설계도"를 가지고 부품(class)를 만들었다.

 

위에서는 두가지정도씩 부품을 만들었지만,

설계도를 가지고 얼마든지 다양한 부품을 찍어낼수있다.

 

그리고 각 설계도면을 가지고 만든 부품을

Car 라는 최종 부품, 산출물이 받게된다.

 

Car는

각 부품이 interface로 정의되어

아주 유연하게 설계되었다.

마치 그 유명한 "붕어빵 기계"와 같다.

 

위 코드를 실행한 결과

Hi, myname is  sonata
heating ...♨
engine working with  180 23
use power  weak
rolling ...🚗
car is starting ~~ 🚗
---------------
Hi, myname is  stinger
heating ...♨
engine is working with  320 52
use power  strong
rolling ...🚗
car is starting ~~ 🚗

이렇게 귀여운

자동차 두대의 동작을 살펴볼 수 있다.

728x90
반응형
728x90
Data Class Xcode Key Raw Info.plist Key
Apple Music Privacy - Media Library Usage Description NSAppleMusicUsageDescription
Bluetooth Privacy - Bluetooth Peripheral Usage Description NSBluetoothPeripheralUsageDescription
Calendar Privacy - Calendars Usage Description NSCalenedarUsageDescription
Camera Privacy - Camera Usage Description NSCameraUsageDescription
Contacts Privacy - Contact Usage Description NSContactsUsageDescription
Health Privacy - Health Share Usage
Description Privacy - Health Update Usage Description
NSHealthShareUsageDescription
NSHealthUpdateUsageDescription
Home Privacy - HomeKit Usage Description NSHomeKitUsageDescription
Location Privacy - Location Always Usage
Description Privacy - Location When In Use Usage Description
NSLocationAlwaysUsageDescription
NSLocationWhenInUseUsageDescription 
Microphone Privacy - Microphone Usage Description NSMicrophoneUsageDescription
Motion Privacy - Motion Usage Description NSMotionUsageDescription
Photos Privacy - Photo Library Usage Description NSPhotoLibraryUsageDescription
Reminders Privacy - Reminders Usage Description NSRemindersUsageDescription
Siri Privacy - Siri Usage Description NSSiriUsageDescription
Speech
Recognition
Privacy - Speech Recognition Usage Description NSSpeechRecognitionUsageDescription
TV Provider
Account
Privacy - TV Provider Usage Description NSVideoSubscriberAccountUsageDescription

 

NSAppleMusicUsageDescription : 미디어 라이브러리 접근

NSBluetoothPeripheralUsageDescription : 블루투스 인터페이스 접근

NSCalenedarUsageDescription : 달력 접근

NSCameraUsageDescription : 카메라 접근

NSContactsUsageDescription : 연락처에 접근

NSHealthShareUsageDescription : 헬스 데이터에 접근

NSHealthUpdateUsageDescription : 건강 데이터에 접근

NSHomeKitUsageDescription : 홈킷 설정 데이터에 접근

NSLocationAlwaysUsageDescription : 위치정보 접근 (항시 허용)
NSLocationWhenInUseUsageDescription : 위치정보 접근 (사용시 허용)

NSMicrophoneUsageDescription : 마이크 접근

NSPhotoLibraryUsageDescription : 사진 라이브러리 접근

NSSiriUsageDescription : 시리 접근

 

 

예전과 다르게

앱 배포할때 필수적으로 세팅해야되는 사항들이

많아졌다.

 

개인정보의 가치가

점점 소중해지고

쾌적한 앱 환경 조성을 위해

발전하고 있는 과정이라고 생각한다.

 

(개발자에겐 불편)

728x90
반응형
728x90

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 아이콘이 왜 안나와?

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

 

 

푸시 알림이

ㄱ) foreground

ㄴ) background

ㄷ) quit state

에서 모두 정상적으로 수신되는것을

분명히 확인했었다

 

라고 착각을 하고있었는데

앱 배포 직전

검토 과정에서

iOS에 알림이 수신은 되지만

진동이 울리지 않음을 발견하였다.

 

위 문제를 해결하기 위해

아래와 같은 방법으로 접근하였다.

 

1. 내가 설정을 덜 한것은 없는지

2. rnfirebase, github에 올라온 issues중에 같은 내용은 없는지

3. vibration 기능의 접근 권한이 문제인지

4. setBackgounrMessageHandler 수신 부분이 문제인지

5. 푸시 발송관련 서버 코드에 문제가 있는것인지

 

1. 내가 설정을 덜 한것은 없는지

rnfirebase docs를 통해 체크한 결과

headless 상태에 대한 처리가 미흡해 보였다.

// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';

messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Message handled in the background!', remoteMessage);
});

messaging()
  .getIsHeadless()
  .then(isHeadless => {
    // do sth with isHeadless
  });
  
function HeadlessCheck({ isHeadless }) {
  if (isHeadless) {
    // App has been launched in the background by iOS, ignore
    return null;
  }

  return <App />;
}

function App() {
  // Your application
}

AppRegistry.registerComponent('app', () => HeadlessCheck);
// appDelegate.m

// add this import statement at the top of your `AppDelegate.m` file
#import "RNFBMessagingModule.h"

// in "(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions" method
// Use `addCustomPropsToUserProps` to pass in props for initialization of your app
// Or pass in `nil` if you have none as per below example
// For `withLaunchOptions` please pass in `launchOptions` object
NSDictionary *appProperties = [RNFBMessagingModule addCustomPropsToUserProps:nil withLaunchOptions:launchOptions];

// Find the `RCTRootView` instance and update the `initialProperties` with your `appProperties` instance
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                             moduleName:@"nameOfYourApp"
                                             initialProperties:appProperties];

위 세가지 설정들에 대해서 다시 한번

검토를 하였다.

 

2. rnfirebase, github에 올라온 issues중에 같은 내용은 없는지

제법 많은 비슷한 내용들이 있었다.

그 중에서 내가 찾은 솔루션은

https://github.com/invertase/react-native-firebase/issues/5656

위의 방법인데

이것은  3,4,5의 과정을 걸쳐서 찾은 솔루션이다.

 

3. vibration 기능의 접근 권한이 문제인지

react-native에 vibration이라는 기능이 내포되어있다.

이 마저도 해보려고하니 잘못된 정보들이 너무나도 많았다.

임시 버튼을 만들어

기능이 잘 동작하는지만 확인하였다.

import { Platform, Vibration } from 'react-native';

// test code
const umm = () => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

 

4. setBackgounrdMessageHandler 수신 부분이 문제인지

화면을 열어보면 푸시가 와있어서

setBackgounrdMessageHandler 리스너에

문제가 있으리라고는 생각해보지 않았었다.

 

하지만, 디바이스에서는 수신했더라도

앱 내에 setBackgounrdMessageHandler 리스너가

실제로 잘 동작하고 있는지는 검토해볼 필요가 있었다.

messaging().setBackgroundMessageHandler(async remoteMessage => {
  umm(); // vibration
  console.log('Message handled in the background!', remoteMessage);
});

const umm = () => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

 

위와 같이 임시 코드를 작성해서

진동이 오는지 체크해본 결과

위의 리스너가 제대로 동작하고 있지 않음을 발견했다.

 

리스너가 제대로 동작하지 않는다면

설정의 문제 혹은 발송의 문제

둘중에 하나라고 결론을 냈다.

 

그런데 아까 분명히

설정의 문제는 확인을 완료했다.

그러면 발송의 문제일 것이다.

 

5. 푸시 발송관련 서버 코드에 문제가 있는것인지

2번에서 언급했던

https://github.com/invertase/react-native-firebase/issues/5656

위의 링크에서 알맞는 발송 포맷을 발견했다.

 

추후, 가이드를 다시보니 업데이트가 된것인지

위 github 링크와 동일한 내용으로 발송 포맷이 나와있었다.

 

github 마지막 코멘트 업데이트 일자가

patels9-vf commented 7 hours ago

겨우 7시간전이 아닌가...

위 이슈가 최근까지도 이어지고있었나보다.

 

공식 가이드에서는

아래와 같이 푸시메시지를 발송하라고 한다.

admin.messaging().send({
  data: {
    //some data
  },
  apns: {
    payload: {
      aps: {
        contentAvailable: true,
      },
    },
    headers: {
      'apns-push-type': 'background',
      'apns-priority': '5',
      'apns-topic': '', // your app bundle identifier
    },
  },
  //must include token, topic, or condition
  //token: //device token
  //topic: //notification topic
  //condition: //notification condition
});

 

다만, 위의 메시지는

data-only에 해당하는 것이기도 하고

보다 많은 케이스에 대비하고자

github의 가이드를 따랐다.

 

const message: any = {
  token,
  notification: {
    title,
    body,
  },
  data: {
    title,
    body,
  },
  android: {
    priority: 'high',
    notification: {
      title,
      body,
      sound: 'default'
    }
  },
  apns: {
    headers: {
      'apns-priority': '5',
    },
    payload: {
      aps: {
        sound: 'default',
        // contentAvailable: 1,
        'content-available': 1,
      }
    }
  }
};
admin.messaging().send(message);

 

위 방법으로 문제없이 메시지가 수신됨을 확인하였다.

 

 

** 번외 **

나는 애플워치를 사용하는데

핸드폰에서 알림이 안울리고

계속 애플워치에서 알림이 울려서

블루투스를 끊고 핸드폰에서 알림이 오는지에 대한

확인이 필요하였다.

 

테스트 결과 수신은 됐다.

다만, 어느정도의 딜레이가 발생했다.

 

가이드에 따르면 priority 설정된

푸시 메시지는

8seconds 정도의 간격으로만

발송이 가능한듯하다.

 

 

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 아이콘이 왜 안나와?

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

728x90
반응형
728x90

[ react-native-code-push 적용하기 ]

https://honeystorage.tistory.com/290

 

[ MS, appcenter 공식 Document ]

https://docs.microsoft.com/ko-kr/appcenter/distribution/codepush/

 

지난 포스팅에 이어

실무에서 사용하기 위한

몇가지 업데이트 사항을 알아보고자 한다

 

1. Mandatory (필수 적용 옵션)

배포 내용에 따라 주요 업데이트때와 마찬가지로

필수적으로 업데이트를 했으면 하는

경우가 발생할 수 있다.

 

배포간에 Mandatory옵션을 사용하면

"업데이트 / 나중에"와 같이 선택지를 주는게 아니라

"업데이트" 선택지 하나만 줄 수 있다.

 

// 일반적인 배포
appcenter codepush release-react -a username/appname -d Production

// 필수사항 배포
appcenter codepush release-react true -a username/appname -m -d Production

"-m" 부분이 추가되었다.

 

코드상에서 updateDialog를 수정해서

사용자에 보여줄 텍스트를 커스텀 할 수도있다.

codePush.sync({
 updateDialog: {
   titie: '업데이트 하세요!',
   mandatoryUpdateMessage: '필수 업데이트입니다. 진행해주세요.',
   mandatoryContinueButtonLabel: '업데이트',
 },
 mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
});

 

 

*** 기타 내용 업데이트 예정

728x90
반응형