Project

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

robinjoon98 2021. 1. 25. 12:36

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

그래서, 홈페이지를 크롤링 하여 스마트폰으로 알림이 오게 구현하고자 한다. 보너스로, 현재 진행중인 이벤트목록을 모바일에서 바로 볼 수 있는 기능도 추가하고자 한다.

우선, 메이플스토리 홈페이지의 html을 분석해보았다. 현재 진행중인 이벤트 목록은 이 링크로 확인할 수 있다.

 

이벤트 목록 사진

메이플 홈페이지에는 이렇게 이벤트리스트가 있고, 각 이미지를 누르면 해당 이벤트 설명페이지로 이동된다. html을 분석하기 위해 개발자페이지를 이용해보자.

이벤트 목록 html

<div class="event_board"></div> 태그 안에 <ul></ul>태그가 있고, 그 안의 각각의 <li> 태그내부에 이벤트 이미지와, 상세페이지 url이 들어가있다. 메이플스토리 이벤트리스트는 SSR형태로 불러오므로, python의 BeautifulSoup 라이브러리나 java의 jsoup를 이용해 간단히 크롤링 할 수 있다.

우선, 안드로이드 어플에서, 이벤트 목록을 가져오는 기능을 구현해보았다.

안드로이드의 java class들

우선, 크롤링한 정보를 담기 위한 클래스인 Event 클래스를 만들었다. 이 클래스는 이벤트 이름, 이벤트 상세페이지 링크, 이벤트 썸네일, 이벤트 기간을 저장한다.

import android.graphics.drawable.Drawable;
import android.media.Image;

public class Event {
    private String title;
    private String date;
    private Drawable thumbnail;
    private String data;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public Drawable getThumbnail() {
        return thumbnail;
    }

    public void setThumbnail(Drawable thumbnail) {
        this.thumbnail = thumbnail;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

메인 엑티비티의 xml은 어차피, 이벤트개수만큼 이미지뷰가 들어가야 하므로, 미리 작성하는게 의미가 없다. 그냥 대충 작성했다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/text1" />
</LinearLayout>

다음은 메인엑티비티 자바 코드이다.

public class MainActivity extends Activity {
    Handler handler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.text1);
        tv.setMovementMethod(new ScrollingMovementMethod());
        handler = new Handler(){
            public void handleMessage(Message msg){
                if(msg.what==0){
                    if(msg.obj!=null) {
                        ScrollView scrollView = new ScrollView(getApplicationContext());
                        LinearLayout linearLayout = new LinearLayout(getApplicationContext());

                        LinearLayout.LayoutParams layoutparams = new LinearLayout.LayoutParams(
                                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
                        linearLayout.setLayoutParams(layoutparams);
                        linearLayout.setOrientation(LinearLayout.VERTICAL);
                        ArrayList<Event> events = (ArrayList<Event>)msg.obj;
                        for(Event event : events){
                            TextView title = new TextView(getApplicationContext());
                            TextView date = new TextView(getApplicationContext());
                            ImageView imageView = new ImageView(getApplicationContext());
                            DisplayMetrics metrics = getResources().getDisplayMetrics();
                            int w = metrics.widthPixels;
                            int a = w/285;
                            int h = a*120;
                            LinearLayout.LayoutParams layoutParams2 = new LinearLayout.LayoutParams(w,h);
                            title.setText(event.getTitle());
                            title.setGravity(Gravity.CENTER_HORIZONTAL);
                            date.setText(event.getDate());
                            date.setGravity(Gravity.CENTER_HORIZONTAL);
                            imageView.setImageDrawable(event.getThumbnail());
                            imageView.setLayoutParams(layoutParams2);
                            imageView.setOnClickListener(new View.OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    Intent intent = new Intent(MainActivity.this,EventViewActivity.class);
                                    intent.putExtra("url",event.getData());
                                    startActivity(intent);
                                }
                            });
                            linearLayout.addView(title);
                            linearLayout.addView(date);
                            linearLayout.addView(imageView);
                        }
                        scrollView.addView(linearLayout);
                        setContentView(scrollView);
                    }else{
                        tv.setText("test");
                    }
                }
            }
        };
        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 div = doc.select("div.event_board");
                    Elements ul = div.select("ul");
                    Elements li = ul.select("li");
                    ArrayList<Event> eventlist = new ArrayList<>();
                    for(Element e : li){
                        Elements dd_date = e.select("dd.date");
                        Elements dd_data = e.select("dd.data");
                        Elements image = e.select("img");
                        Elements a = e.select("a");
                        Event ev = new Event();
                        ev.setDate("▽ ▼ ▽ ▼     "+dd_date.text()+"      ▽ ▼ ▽ ▼");
                        ev.setTitle(dd_data.text());
                        ev.setThumbnail(drawableFromUrl(image.attr("src")));
                        ev.setData("https://maplestory.nexon.com"+a.attr("href"));
                        eventlist.add(ev);
                    }
                    message.what=0;
                    message.obj = eventlist;
                    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);

    }
}

안드로이드에서는, 네트워크 통신작업을 메인쓰레드에서 할 수 없고, 화면작업을 메인쓰레드가 아닌 쓰레드에서 할 수 없다. 따라서, 핸들러와 메시지라는 것을 이용하여 네트워크 작업을 하는쓰레드와 메인쓰레드 사이에 통신을 해야 한다.

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 div = doc.select("div.event_board");
                    Elements ul = div.select("ul");
                    Elements li = ul.select("li");
                    ArrayList<Event> eventlist = new ArrayList<>();
                    for(Element e : li){
                        Elements dd_date = e.select("dd.date");
                        Elements dd_data = e.select("dd.data");
                        Elements image = e.select("img");
                        Elements a = e.select("a");
                        Event ev = new Event();
                        ev.setDate("▽ ▼ ▽ ▼     "+dd_date.text()+"      ▽ ▼ ▽ ▼");
                        ev.setTitle(dd_data.text());
                        ev.setThumbnail(drawableFromUrl(image.attr("src")));
                        ev.setData("https://maplestory.nexon.com"+a.attr("href"));
                        eventlist.add(ev);
                    }
                    message.what=0;
                    message.obj = eventlist;
                    handler.sendMessage(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

위 코드는, Jsoup를 이용해 크롤링을 하는 코드이다. 처음에 html을 분석한 것을 토대로 크롤링하면 된다. 

크롤링한 정보를 Event 객체에 담고, 이를 ArrayList에 담은 후 이 ArrayList를 핸들러에게 전달한다.

handler = new Handler(){
            public void handleMessage(Message msg){
                if(msg.what==0){
                    if(msg.obj!=null) {
                        ScrollView scrollView = new ScrollView(getApplicationContext());
                        LinearLayout linearLayout = new LinearLayout(getApplicationContext());

                        LinearLayout.LayoutParams layoutparams = new LinearLayout.LayoutParams(
                                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
                        linearLayout.setLayoutParams(layoutparams);
                        linearLayout.setOrientation(LinearLayout.VERTICAL);
                        ArrayList<Event> events = (ArrayList<Event>)msg.obj;
                        for(Event event : events){
                            TextView title = new TextView(getApplicationContext());
                            TextView date = new TextView(getApplicationContext());
                            ImageView imageView = new ImageView(getApplicationContext());
                            // 미관을 위해, 이미지뷰의 크기를 화면크기에 맞게 가공해준다. 
                            DisplayMetrics metrics = getResources().getDisplayMetrics();
                            int w = metrics.widthPixels;
                            int a = w/285; // 원본 이미지의 가로가 285라 화면의 가로를 285로 나눈다.
                            int h = a*120; // 원본 이미지의 세로가 120이라 화면의 세로를 120으로 나눈다.
                            LinearLayout.LayoutParams layoutParams2 = new LinearLayout.LayoutParams(w,h); // 레이아웃 파라미터를 생성한다.
                            title.setText(event.getTitle());
                            title.setGravity(Gravity.CENTER_HORIZONTAL);
                            date.setText(event.getDate());
                            date.setGravity(Gravity.CENTER_HORIZONTAL);
                            imageView.setImageDrawable(event.getThumbnail());
                            imageView.setLayoutParams(layoutParams2); // 이미지뷰에 레이아웃 파라미터를 지정한다.
                            imageView.setOnClickListener(new View.OnClickListener() { // 이미지를 터치했을 때 새로운 액티비티를 실행한다.
                                @Override
                                public void onClick(View v) {
                                    Intent intent = new Intent(MainActivity.this,EventViewActivity.class); // 이벤트의 상세페이지를 보여주는 엑티비티를 인텐트에 지정한다.
                                    intent.putExtra("url",event.getData()); // 인텐트에 url을 넘겨준다
                                    startActivity(intent); // 엑티비티를 실행한다
                                }
                            });
                            linearLayout.addView(title);
                            linearLayout.addView(date);
                            linearLayout.addView(imageView);
                        }
                        scrollView.addView(linearLayout);
                        setContentView(scrollView);
                    }else{
                        tv.setText("test");
                    }
                }
            }
        };

핸들러는, 전달받은 ArrayList의 Event들을 화면에 적절히 출력해준다. 

 

아래는 여기까지의 실행화면이다.

 

이제, 이벤트의 상세페이지를 불러오는 엑티비티를 작성해보았다. 우선, 메이플스토리 이벤트 상세페이지를 분석했다.

이벤트 상세페이지

우선, 기본적으로 모든 이벤트 상세페이지는 이미지로 작성되어있다. 대부분의 이벤트의 경우 이미지 한개로 되어있지만, 설명할 분량이 많은 경우, 여러 이미지에 나눠져있다. 따라서, 이번에도 xml은 사실상 의미가 없다. 이미지뷰가 몇개가 필요한지 미리 알 수 없기 때문이다. 어쨋든, 분석결과 <div class="qs_text"></div> 태그 내에 <img> 태그로 이벤트 상세설명 이미지가 존재한다. 아래는 이벤트 상세페이지를 보여주는 엑티비티인 EventViewActivity.java 이다.

public class EventViewActivity extends Activity {
    Handler handler = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        Intent intent = new Intent(this.getIntent());
        String url = intent.getStringExtra("url");
        Log.d("eventurl",url);
        ScrollView scrollView = new ScrollView(getApplicationContext());
        LinearLayout linearLayout = new LinearLayout(getApplicationContext());
        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        scrollView.setLayoutParams(layoutParams);
        linearLayout.setLayoutParams(layoutParams);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        handler = new Handler(){
            public void handleMessage(Message msg){
                if(msg.what==1){
                    Elements images = (Elements)msg.obj;
                    for(Element e : images){
                        ImageView view = new ImageView(getApplicationContext());
                        ImageLoadTask task = new ImageLoadTask(e.attr("src"),view);
                        task.execute();
                        linearLayout.addView(view);
                    }
                    scrollView.addView(linearLayout);
                    setContentView(scrollView);
                }
            }
        };
        new Thread(new Runnable() {
            @Override
            public void run() {
                final Message message = handler.obtainMessage();
                try {
                    Document doc = null;
                    doc = Jsoup.connect(url).get();
                    Elements elements = doc.select("div.qs_text");
                    Elements image = elements.select("img");
                    message.what=1;
                    message.obj = image;
                    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);

    }
}

메인엑티비티와 별 다른 건 없다. 아래는 실행화면이다.

 

썬데이메이플 이벤트 알림을 위한 내용은 다음 포스팅에서 이어서 작성하겠다.