Project

메이플스토리 썬데이 알리미 2편

robinjoon98 2021. 1. 26. 10:25

2021/01/25 - [Project] - 메이플스토리 썬데이 알리미 1편

 

메이플스토리 썬데이 알리미 1편

국내에서 가장 유명한 온라인 rpg 게임인 메이플스토리에는 매 주 일요일마다 썬데이메이플 이라는 이름의 이벤트를 한다. 이 이벤트는 최대한 빨리 아는게 무었보다 중요하다. 특정 재화의 가

robinjoon98.tistory.com

이전 편에 이어서 이번엔 썬데이메이플이 뜨면 안드로이드 어플로 알림이 오도록 구현하였다. 안드로이드의 절전로직인 DOZE 모드덕에, 백그라운드에서 직접 주기적으로 메이플스토리 홈페이지에서 직접 크롤링하도록 구현하는 것에는 한계가 있었다. 결론적으로 아예 불가능한 것은 아니지만, 상단바에 항상 아이콘이 노출되도록 해야하는 점, 스마트폰의 배터리를 많이 소모하는 점 때문에, 외부 서버에서 메이플스토리 홈페이지를 크롤링 해 적절한 시점에 안드로이드로 푸쉬알림을 보내는 방식을 채택했다. 이를 위해서는 FCM을 이용해야 한다. FCM에 대한 자세한 설명은 추후 작성해보도록 하겠다.

 

FCM을 이용하기 위해서는 Firebase 에 가입후 프로젝트를 생성해야한다. 그 후 여러 공식 문서를 참고하여 알림기능을 구현하면 된다.

1. 서버 구현 참고사항

 

Firebase 클라우드 메시징 HTTP 프로토콜

firebase.ml.naturallanguage.translate

firebase.google.com

2. 안드로이드 어플 구현 참고사항

 

Android에서 Firebase 클라우드 메시징 클라이언트 앱 설정

Firebase 클라우드 메시징 Android 클라이언트 앱을 만들려면 FirebaseMessaging API와 Gradle이 있는 Android 스튜디오 1.4 이상을 사용하세요. 이 페이지의 안내에서는 Android 프로젝트에 Firebase를 추가하는 단

firebase.google.com

아래는 안드로이드에서 알림을 수신하는 서비스이다.

public class MyFirebaseMessagingService  extends FirebaseMessagingService {
    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        Log.d("Firebase", "FirebaseInstanceIDService : " + s);
    }
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d("Firebase","send");
        sendNotification(remoteMessage);
    }

    private void sendNotification(RemoteMessage remoteMessage) {

        String title = remoteMessage.getNotification().getTitle();
        String message = remoteMessage.getNotification().getBody();

        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){

            String channel = "Maple_Sunday";
            String channel_nm = "메이플선데이";
            PendingIntent mPendingIntent = PendingIntent.getActivity(
                    getApplicationContext(),
                    0, // 보통 default값 0을 삽입
                    new Intent(getApplicationContext(),NotiClickActivity.class),
                    PendingIntent.FLAG_UPDATE_CURRENT
            );
            NotificationManager notichannel = (android.app.NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            NotificationChannel channelMessage = new NotificationChannel(channel, channel_nm,
                    android.app.NotificationManager.IMPORTANCE_DEFAULT);
            channelMessage.setDescription("메이플 선데이 이벤트에 대한 알림");
            channelMessage.enableLights(true);
            channelMessage.enableVibration(true);
            channelMessage.setShowBadge(false);
            channelMessage.setVibrationPattern(new long[]{100, 200, 100, 200});
            notichannel.createNotificationChannel(channelMessage);
            NotificationCompat.Builder notificationBuilder =
                    new NotificationCompat.Builder(this, channel)
                            .setSmallIcon(R.mipmap.ic_launcher)
                            .setContentTitle(title)
                            .setContentText(message)
                            .setChannelId(channel)
                            .setAutoCancel(true)
                            .setContentIntent(mPendingIntent)
                            .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);

            NotificationManager notificationManager =
                    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

            notificationManager.notify(9999, notificationBuilder.build());
        }
    }
}

아래는 HTTP API를 이용해 작성한 파이썬 코드이다. 서버에서 실행중이다.

from bs4 import BeautifulSoup
import requests
import json
import time
before_r = 'a'
while 1 > 0:
	url = "https://maplestory.nexon.com/News/Event/Ongoing"
	res = requests.get(url)
	soup = BeautifulSoup(res.text, 'html.parser')

	result = soup.select('dd.data a')
	
	for r in result:
		if r.text == '썬데이메이플':
			if before_r == 'a':
				before_r = r['href']
				print("before_r 최초설정 " + before_r)
				data = {
					"notification": {
						"title": "썬데이메이플 등장",
						"body": "눌러서 확인하세요",
						"priority": "high",
						"icon": "ic_launcher",
						"android_channel_id": "Maple_Sunday",
						"click_action": "notiactivity"
					},
					"to": "/topics/sunday"
				}
				json_datas = json.dumps(data)
				headers = {"Content-Type": "application/json",
						   "Authorization": "key=[인증키]"}
				res = requests.post("https://fcm.googleapis.com/fcm/send", headers=headers, data=json_datas)
				print(res.status_code)
				break
			if before_r != r['href']:
				print("before_r != r['href'] ,before_r = " + before_r+"\n")
				print("r['href'] = "+r['href'])
				data = {
					"notification": {
						"title": "썬데이메이플 등장",
						"body":  "눌러서 확인하세요",
						"priority": "high",
						"icon": "ic_launcher",
						"android_channel_id": "Maple_Sunday",
						"click_action": "notiactivity"
					},
					"to": "/topics/sunday"
				}
				json_datas = json.dumps(data)
				headers = {"Content-Type":"application/json","Authorization":"key=[인증키]"}
				res = requests.post("https://fcm.googleapis.com/fcm/send", headers=headers, data=json_datas)
				print(res.status_code)
				break
	time.sleep(15)

[인증키]라 써있는 부분은 말 그대로 각 프로젝트를 식별하는 키이다.

 

이렇게만 하면, 일단 썬데이메이플이 뜨면 스마트폰으로 알림은 간다. 중요한 것은, 알림을 클릭했을 때 바로 썬데이메이플의 상세설명이 보여야 한다. 그러나, FCM은 앱이 백그라운드에서 알림수신 시 특정 엑티비티를 실행하도록 할 수는 있지만, 그 엑티비티에 특정 정보를 전달하는 것은 불가능하다. 즉, 이 프로젝트에서 썬데이메이플의 상세페이지 url을 전달할 수 없다. 따라서, 기존의 엑티비티를 재사용할 수 없고, 알림클릭시 실행되는 엑티비티에서 직접 이벤트목록을 크롤링하고 그 중 썬데이메이플을 상세페이지를 다시 크롤링하는 방식을 사용해야 한다. 

 

아래는 알림클릭시 실행되는 NotiClickActivity 이다.

public class NotiClickActivity extends Activity {
    Handler handler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_noticick);
        ImageView imageView = findViewById(R.id.sundayimageview);
        handler =new Handler(){
            public void handleMessage(Message msg) {
                if (msg.what == 0) {
                    if (msg.obj != null) {
                        imageView.setImageDrawable((Drawable)msg.obj);
                        Log.d("image","ok");
                    }else{
                        imageView.setImageResource(R.drawable.ic_launcher_background);
                    }
                }
            }
        };
        new Thread(new Runnable() {
            @Override
            public void run() {
                final Message message = handler.obtainMessage();
                try {
                    Document doc = null;
                    doc = Jsoup.connect("https://maplestory.nexon.com/News/Event/Ongoing").get();
                    Elements sunday = doc.select("a:contains(썬데이메이플)");
                    String url = "https://maplestory.nexon.com"+sunday.get(0).attr("href");
                    doc = Jsoup.connect(url).get();
                    Elements div = doc.select("div.new_board_con");
                    Elements img = div.select("img");
                    Drawable drawable = drawableFromUrl(img.attr("src"));
                    message.what=0;
                    message.obj = drawable;
                    handler.sendMessage(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    private Drawable drawableFromUrl(String url)
            throws IOException {
        Bitmap x;

        HttpURLConnection connection =
                (HttpURLConnection) new URL(url).openConnection();
        connection.connect();
        InputStream input = connection.getInputStream();

        x = BitmapFactory.decodeStream(input);
        return new BitmapDrawable(getResources(),x);

    }
}

이제 알림이 왔을 때, 알림을 클릭하면 바로 썬데이메이플을 확인할 수 있다.