우리는 유투브 강의를 보고 따라서 Vampire Survivor라이크류 게임을 만들어보았다. 이제 우리만의 스테이지와 몹을 만들고 이를 통해 데이터를 모아서 AI모델을 통해 유저의 생존시간과 시도 횟수를 예측해보기로 하였다.
 
1. 우리만의 스테이지
기존의 강의에서는 스테이지가 하나밖에 없어서 여러 스테이지를 깨고 그 다음스테이지를 예측해야하는 우리에게는 여러가지 스테이지가 필요했다. 그렇게 여러 스테이지를 생성하는 법을 공부하였는데 이때 사용하는 것이 맵을 게임오브젝트로 만들어 두고 GameManager에서 Stages라는 Gameobject 배열로 만들고 이곳에 여러 스테이지를 저장한 뒤 기존의 스테이지가 끝나면 스테이지맵의 게임오브젝트 Active를 false로 바꿔주고 다음  스테이지맵의 게임 오브젝트 Active를 true로 만들어 주면 된다. 
Stages[stageIndex].SetActive(false);
stageIndex++;
Stages[stageIndex].SetActive(true);
 
이러한 코드를 GameManager 코드에 NextStage() 메서드로 구현해두었다.  또한 Player의 위치를 다시 원점으로 돌려서 스테이지를 플레이할 수 있게 
player.transform.position = Vector3.zero;
 
이 코드도 함께 넣어 구현해 주었다. 

Backyard Top-Down Tileset(FREE)
https://assetstore.unity.com/packages/2d/environments/backyard-top-down-tileset-53854

 

Backyard Top-Down Tileset | 2D 주변환경 | Unity Asset Store

Elevate your workflow with the Backyard Top-Down Tileset asset from Kittens and Elves at Work. Find this & more 주변환경 on the Unity Asset Store.

assetstore.unity.com

https://assetstore.unity.com/packages/2d/characters/top-down-2d-rpg-assets-pack-188718

 

Top-Down 2D RPG Assets Pack | 2D 캐릭터 | Unity Asset Store

Elevate your workflow with the Top-Down 2D RPG Assets Pack asset from Goldmetal. Find this & more 캐릭터 on the Unity Asset Store.

assetstore.unity.com

 
 
골드메탈님의 강의에서 무한맵처럼 보이게 맵을 이동하는 것을 코드로 구현했었는데 이 코드는 그대로 사용하고 각 스테이지별 기존 맵과 동일한 크기의 타일맵을 생성하여서 스테이지가 달라지는 것을 알 수 있도록 구현하였다. 
타일맵을 생성하는데에는 위의 두 Asset을 활용하여 만들었다.
 
private void OnTriggerExit2D(Collider2D collision)
{
 
    switch (transform.tag)
    {
        case "Ground":
            float diffX = playerPos.x - myPos.x;
            float diffY = playerPos.y - myPos.y;
            float dirX = diffX < 0 ? -1 : 1;
            float dirY = diffY < 0 ? -1 : 1;
            diffX=Mathf.Abs(diffX);
            diffY=Mathf.Abs(diffY);
            if (diffX > diffY)
            {
                transform.Translate(Vector3.right * dirX * 40);
            }
            else if (diffX < diffY)
            {
                transform.Translate(Vector3.up * dirY * 40);
            }
            break;
    }

}

만약 플레이어 밑에 있는 Area 오브젝트가 Ground의 끝을 만났을때 맵을 그 길이만큼 왼쪽이나 오른쪽 또는 위 아래로 움직여주는 코드이다. 
 
2.우리만의 몹 만들기
기존의 강의에서는 몹이 2개만 생성가능했기 때문에 우리는 총 4개의 스테이지에 맞는 여러 몬스터를 더 넣고자 했다. 

https://assetstore.unity.com/packages/2d/characters/2d-monster-undead-spum-premium-addon-pack-200884

 

2D Monster Undead - SPUM Premium Addon Pack | 2D 캐릭터 | Unity Asset Store

Elevate your workflow with the 2D Monster Undead - SPUM Premium Addon Pack asset from soonsoon. Find this & more 캐릭터 on the Unity Asset Store.

assetstore.unity.com

Undead Survivor에 맞게 좀비 Asset을 찾아서 넣어주었다. 가격은 $9.99정도이다. 이 Asset을 처음 받았을 때 기존에 강의에서 활용하던 몬스터 Asset과 구조가 많이 달라서 사용법을 찾는데 오래 걸렸다. 기존에 Asset은 제일 상위 오브젝트에 Animator을 붙이고 이를 통해 Animation이 작동했었는데 이 Asset은 하위에 UnitRoot가 있고 이 아래에 몬스터의 몸을 이루는 부분들이 있어서 이 UnitRoot를 통해 해당 몬스터의 Animation이 작동하는 것을 알았다. 그래서 이 UnitRoot에 기존에 가지고 있던 Animator에 Hit라는 Trigger과 Dead라는 Bool 값을 넣어서 만약 유저에게 데미지를 입었을때의 Animation과 몬스터의 체력이상의 공격을 당했을 때 Dead에 해당하는 Animation이 작동하도록 했다.
void OnTriggerEnter2D(Collider2D collision)
{
    if (!collision.CompareTag("Bullet"))
        return;
    health -= collision.GetComponent<Bullet>().damage;
    StartCoroutine(KnockBack_1());
    if (health > 0)
    {
        anim.SetTrigger("Hit");
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Hit);
    }
    else
    {
        isLive = false;
        coll.enabled = false;
        rigid.simulated = false;
        anim.SetBool("Dead", true);
        StartCoroutine(Dead());
        GameManager.instance.kill++;
        GameManager.instance.GetExp();

        if (GameManager.instance.isLive)
            AudioManager.instance.PlaySfx(AudioManager.Sfx.Dead);
    }
}
 
여기서 Bullet은 유저의 무기이다. 

 
 
Enemy에 들어갈 Animator도 수정하였다. 또한 Spawner에 spawn 메서드를 통해 몬스터가 생성되는데 Stage에 따라 시작 몬스터와 몇초마다 다른 몬스터가 생성되는지를 코드로 구현하여 스테이지마다 몬스터가 바뀔수 있도록 구현하였다.
 
 void spawn()                        //0번째부터시작 1,2번째는 총알이라 놔두기 
 {
     GameObject enemy;
     if (GameManager.instance.stageIndex == 0)           //1스테이지는 레벨에 따라
     {
         if (level == 0)
         {
             enemy = GameManager.instance.pool.Get(level);    //SpawnData 인스펙터창
         }
         else
         {
             enemy = GameManager.instance.pool.Get(level + 2);    //SpawnData 인스펙터창
         }
         enemy.transform.position = spawnPoint[Random.Range(1, spawnPoint.Length)].position;
         enemy.GetComponent<Enemy_new>().Init(spawnData[level]);
     }
     else
     {                                                           //2스테이지부터는 레벨+스테이지 인덱스번째의 몹부터 소환
         if (level == 0)
         {
             enemy = GameManager.instance.pool.Get(level+2+GameManager.instance.stageIndex);    //SpawnData 인스펙터창
         }
         else
         {
             enemy = GameManager.instance.pool.Get(level +2+ GameManager.instance.stageIndex-1);    //SpawnData 인스펙터창
             if((level + 2 + GameManager.instance.stageIndex) == GameManager.instance.pool.prefabs.Length)
             {
                 enemy = GameManager.instance.pool.Get(GameManager.instance.pool.prefabs.Length-1);    //SpawnData 인스펙터창
             }
         }
         enemy.transform.position = spawnPoint[Random.Range(1, spawnPoint.Length)].position;
         enemy.GetComponent<Enemy_new>().Init(spawnData[level]);
     }
 }
 
또한 몬스터마다 체력, 스폰시간, 체력, 속도등을 정하여 입력해 주었다. 

 

※문제설명

휴대폰의 자판은 컴퓨터 키보드 자판과는 다르게 하나의 키에 여러 개의 문자가 할당될 수 있습니다. 키 하나에 여러 문자가 할당된 경우, 동일한 키를 연속해서 빠르게 누르면 할당된 순서대로 문자가 바뀝니다.

예를 들어, 1번 키에 "A", "B", "C" 순서대로 문자가 할당되어 있다면 1번 키를 한 번 누르면 "A", 두 번 누르면 "B", 세 번 누르면 "C"가 되는 식입니다.

같은 규칙을 적용해 아무렇게나 만든 휴대폰 자판이 있습니다. 이 휴대폰 자판은 키의 개수가 1개부터 최대 100개까지 있을 수 있으며, 특정 키를 눌렀을 때 입력되는 문자들도 무작위로 배열되어 있습니다. 또, 같은 문자가 자판 전체에 여러 번 할당된 경우도 있고, 키 하나에 같은 문자가 여러 번 할당된 경우도 있습니다. 심지어 아예 할당되지 않은 경우도 있습니다. 따라서 몇몇 문자열은 작성할 수 없을 수도 있습니다.

이 휴대폰 자판을 이용해 특정 문자열을 작성할 때, 키를 최소 몇 번 눌러야 그 문자열을 작성할 수 있는지 알아보고자 합니다.

1번 키부터 차례대로 할당된 문자들이 순서대로 담긴 문자열배열 keymap과 입력하려는 문자열들이 담긴 문자열 배열 targets가 주어질 때, 각 문자열을 작성하기 위해 키를 최소 몇 번씩 눌러야 하는지 순서대로 배열에 담아 return 하는 solution 함수를 완성해 주세요.

단, 목표 문자열을 작성할 수 없을 때는 -1을 저장합니다.

=> keymap- 키보드, targets 치려고 하는 문자열 

키보드를 누르면서 targets를 만들수 있는 최소의 개수 찾기 불가능 하다면  -1 

 

●오답코드

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<string> keymap, vector<string> targets) {
    vector<int> answer;
    
    int index;
    for(int i=0;i<targets.size();i++){
        answer.push_back(0);
        for(int j=0;j<targets[i].size();j++){
            char c=targets[i][j];
            int len=keymap[i].size()+1;
            for(int x=0;x<keymap.size();x++){
                for(int y=0;y<keymap[x].size();y++){
                    if(keymap[x][y]==c){
                        len=min(len,y+1);
                        break;
                    }
                }
            }
             answer[i]+=len;
        }
    }
    for(int i=0;i<answer.size();i++){
        if(answer[i]%answer.size()==0){
            answer[i]=-1;
        }
    }
    return answer;
}

 

★ 어려웠던 점 

targets 와 keymap을 돌면서 최소의 인덱스를 저장하면 된다는 것은 알겠는데 여기서 어떤 값과 비교를 해야될지와 targets의 문자가 keymap에 없었을 때의 처리의 문제가 어려웠다.

=> 1. 비교값은 keymap의 제한사항에서의 최대 길이보다 1 크게 해서 min값을 계속 갱신하여 줄 수 있도록 하였다.  

2. 1,0의 값으로 문자열이 같은게 있었는지 없었는지 판단할 수 있도록 하였다. 

★정답코드

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<string> keymap, vector<string> targets) {
    vector<int> answer;
    
    int index;
    for(int i=0;i<targets.size();i++){
        answer.push_back(0);
        for(int j=0;j<targets[i].size();j++){
            char c=targets[i][j];
            int len=101;
            int h=1;
            for(int x=0;x<keymap.size();x++){
                for(int y=0;y<keymap[x].size();y++){
                    if(keymap[x][y]==c){
                        len=min(len,y+1);
                        h=0;
                        break;
                    }
                }
            }
            if(h){
                answer[i]=-1;
                break;
            }
             answer[i]+=len;
        }
    }
    return answer;
}

내가 코딩을 배우고 코딩으로 무엇인가를 만드는 걸 목표로 하게 되었을 때부터 내 오랜 목표는 누구나 쉽고 재밌게 즐길 수 있는 게임(RPG)를 만드는 것이다. 그리고 나는 현실처럼 많은 상호작용을 게임안에서 할 수 있고 또 다른 세상을 유저가 만들어가기도 하고 게임을 진행하는 데 있어서 하나의 루트만 존재하는 것이 아니라 유저에 따라 여러루트가 존재할 수 있는 그런 게임을 만들고 싶다.

그렇게 꿈을 향해서 나아가던 중에 몰입에 있어서 중요한 역활을 하는 게임 난이도를 AI가 바꿀 수 있으면 어떨까라는 생각을 친구와 나누게 되었고 이 아이디어를 실제로 학기 중에 공부하고 구현해보자 라는 계획을 세우게 되었다. 그래서 나와 친구는 학교에 직접 한 학기의 수업을 만들어서 진행할 수 있는 것이 있어서  유저의 게임실력을 받아서 게임의 난이도를 유저에 맞게 변경해주는 것을 만들기로 하였다. 

처음에는 rpg게임으로 구현을 해보려 했지만 생각보다 시간이 많이 소요될 것 같고 게임보다는 실제로 작동하는 AI모델을 만들어 보려고했기때문에 이러한 데이터를 더 잘 받을 수있는 뱀서라이크 게임을 만들어서 모델에 사용하기로 하였다. 그래서 뱀서라이크 게임을 만드는 영상을 찾다가 골드메탈님의 영상을 보게 되었고 이것을 바탕으로 우리의 팀만의 차별성(여러스테이지, 몬스터종류의 차별성)을 조금 넣어서 모델에 넣어보자는 결론이 나왔다. 

 

밑에는 해당 강좌의 시작 영상이 있다.

https://www.youtube.com/watch?v=MmW166cHj54&list=PLO-mt5Iu5TeZF8xMHqtT_DhAPKmjF6i3x&index=1&t=12s

 

 

 

이번에는 경남드리의 부가기능과 그것을 구현하면서 배운 점에 대해 적어보려고 한다.
주요한 경남드리의 부가기능에는 회원가입, 좋아요 및 리뷰가 있다. 

1)회원가입 

회원가입은 FireBase의 공식문서를 참고하여 만들었다. 
https://firebase.google.com/docs/auth/android/start?hl=ko&authuser=2

Android에서 Firebase 인증 시작하기     |  Firebase Authentication

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 Android에서 Firebase 인증 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하

firebase.google.com

이 문서에 보면 신규 사용자 가입이라는 항목이 있다. 이 항목 코드를 가져와서 사용해주는데 만약 이름이나 사진을 넣어주고 싶다면 가입하는 코드 내에서 회원가입이 정상적으로 이루진 후에  유저의 정보에 이름과 사진을 추가해주는 코드가 들어가도록 구현해주면 된다. 
 

mAuth.createUserWithEmailAndPassword(email, password)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    // Sign in success, update UI with the signed-in user's information
                    Log.d(TAG, "createUserWithEmail:success");
                    FirebaseUser user = mAuth.getCurrentUser();
                    updateUI(user);
                } else {
                    // If sign in fails, display a message to the user.
                    Log.w(TAG, "createUserWithEmail:failure", task.getException());
                    Toast.makeText(EmailPasswordActivity.this, "Authentication failed.",
                            Toast.LENGTH_SHORT).show();
                    updateUI(null);
                }
            }
        });

 

 

이 email과 password에 우리의 값을 넣어서 isSuccessful 시에 회원가입 시 성공인 화면이 나올 수 있도록 만들어 보겠다. 
 
일단 회원가입에 유저정보를 받을 수 있게 틀을 xml로 작성해주자 

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#AED2FF">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/back"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="@drawable/backspaceimage"
                android:textColor="@color/purple_700"
                android:textSize="20dp" />

            <TextView
                android:id="@+id/signup"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:layout_marginBottom="20dp"
                android:layout_weight="2"
                android:gravity="center"
                android:text="회원 가입"
                android:textColor="@color/purple_700"
                android:textSize="25dp" />
        </LinearLayout>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="30dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="30dp"
                android:orientation="vertical">


                <TextView
                    android:id="@+id/signIDtext"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:text="닉네임"
                    android:textColor="@color/purple_700"
                    android:textSize="20dp"
                    android:textStyle="bold" />

                <EditText
                    android:id="@+id/signID"
                    android:layout_width="wrap_content"
                    android:layout_height="40dp"
                    android:layout_marginBottom="20dp"
                    android:background="@color/white"
                    android:ems="10"
                    android:hint=" 닉네임"
                    android:inputType="textPersonName"
                    android:text=""
                    android:textColor="@color/black"
                    android:textColorHint="#60000000" />


                <TextView
                    android:id="@+id/signmailtext"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:text="이메일"
                    android:textColor="@color/purple_700"
                    android:textSize="20dp"
                    android:textStyle="bold" />

                <EditText
                    android:id="@+id/signmail"
                    android:layout_width="wrap_content"
                    android:layout_height="40dp"
                    android:layout_marginBottom="20dp"
                    android:background="@color/white"
                    android:ems="10"
                    android:hint="이메일"
                    android:inputType="textPersonName"
                    android:text=""
                    android:textColor="@color/black"
                    android:textColorHint="#60000000" />

                <TextView
                    android:id="@+id/signPWtext"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:text="비밀번호"
                    android:textColor="@color/purple_700"
                    android:textSize="20dp"
                    android:textStyle="bold" />


                <EditText
                    android:id="@+id/signPW"
                    android:layout_width="wrap_content"
                    android:layout_height="40dp"
                    android:layout_marginBottom="20dp"
                    android:background="@color/white"
                    android:ems="10"
                    android:hint=" 비밀번호"
                    android:inputType="textPassword"
                    android:text=""
                    android:textColor="@color/black"
                    android:textColorHint="#60000000" />

                <TextView
                    android:id="@+id/signPW2text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:text="비밀번호 확인"
                    android:textColor="@color/purple_700"
                    android:textSize="20dp"
                    android:textStyle="bold" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal">

                    <EditText
                        android:id="@+id/signPW2"
                        android:layout_width="wrap_content"
                        android:layout_height="40dp"
                        android:layout_marginBottom="20dp"
                        android:background="@color/white"
                        android:ems="10"
                        android:hint=" 비밀번호 다시 입력"
                        android:inputType="textPassword"
                        android:text=""
                        android:textColor="@color/black"
                        android:textColorHint="#60000000" />

                    <androidx.appcompat.widget.AppCompatButton
                        android:id="@+id/pwcheckbutton"
                        android:layout_width="wrap_content"
                        android:layout_height="40dp"
                        android:layout_marginLeft="10dp"
                        android:background="@drawable/cate_button_selector"
                        android:text="확인"
                        android:textColor="@color/white"
                        android:textSize="16sp"
                        android:textStyle="bold" />
                </LinearLayout>

                <androidx.appcompat.widget.AppCompatButton
                    android:id="@+id/signupbutton"
                    android:layout_width="wrap_content"
                    android:layout_height="40dp"
                    android:layout_gravity="center"
                    android:layout_marginTop="20dp"
                    android:background="@drawable/cate_button_selector"
                    android:text="가입"
                    android:textColor="@color/white"
                    android:textSize="16sp"
                    android:textStyle="bold" />

            </LinearLayout>
        </ScrollView>

    </LinearLayout>
</FrameLayout>

 
유저 닉네임과 아이디 비밀번호를 EditText로 유저가 직접 입력할 수 있도록 하였다. 그리고 비밀번호 확인버튼과 뒤로가기 버튼, 회원가입 버튼을 만들어서 누를 수 있게 구현하였다. 
밑은 실제로 구현된 화면이다.
 

 
이 EditText에 있는 값을 가져온 다음 위에 있던 함수와 똑같이 작성해준다음 매개변수만 우리의 값으로 대체해 준다.

String mypw=pw.getText().toString();
String mypw2=pw2.getText().toString();
String myemail=email.getText().toString();
String myname=name.getText().toString();
if(mypw.equals(mypw2)){
    mFirebaseAuth.createUserWithEmailAndPassword(myemail, mypw)
            .addOnCompleteListener(signUp.this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    if (task.isSuccessful()) {
                        // Sign in success, update UI with the signed-in user's information
                        Toast.makeText(signUp.this, "회원가입성공",Toast.LENGTH_LONG).show();
                        FirebaseUser user = mFirebaseAuth.getCurrentUser();
                        UserProfileChangeRequest profileUpdates = new UserProfileChangeRequest.Builder()
                                .setDisplayName(myname).build();
                        user.updateProfile(profileUpdates)
                                .addOnCompleteListener(new OnCompleteListener<Void>() {
                                    @Override
                                    public void onComplete(@NonNull Task<Void> task) {
                                        if (task.isSuccessful()) {

                                        }else{
                                            Log.d(TAG, "실패.");
                                        }
                                    }
                                });

                        onBackPressed();
                    } else {
                        // If sign in fails, display a message to the user.
                        Toast.makeText(signUp.this, "회원가입 실패",Toast.LENGTH_LONG).show();
                    }
                }
            });
}else{
    Toast.makeText(signUp.this, "비밀번호가 중복체크 실패",Toast.LENGTH_LONG).show();
}

2)로그인 

로그인 또한 Firebase 위의 링크의 공식문서에 잘 작성돼있다. 코드를 보자면 

mAuth.signInWithEmailAndPassword(email, password)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    // Sign in success, update UI with the signed-in user's information
                    Log.d(TAG, "signInWithEmail:success");
                    FirebaseUser user = mAuth.getCurrentUser();
                    updateUI(user);
                } else {
                    // If sign in fails, display a message to the user.
                    Log.w(TAG, "signInWithEmail:failure", task.getException());
                    Toast.makeText(EmailPasswordActivity.this, "Authentication failed.",
                            Toast.LENGTH_SHORT).show();
                    updateUI(null);
                }
            }
        });
 

 
이 email과 password에 우리의 값을 넣어서 isSuccessful 시에 로그인 성공인 화면이 나올 수 있도록 만들어 보겠다. 
 
일단 기반이 되는 xml 파일을 보자면 회원가입과 동일하게 입력받는 부분이 EditText로 구현되어 있다. 또한 로그인 할 수 있는 버튼과 회원가입화면으로 넘어갈 수 있는 회원가입 글자도 클릭가능하게 만들어 주었다. 

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#AED2FF"
    tools:context=".Login">


    <TextView
        android:id="@+id/loginbackbutton"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_margin="20dp"
        android:layout_weight="1.7"
        android:background="@drawable/backspaceimage"
        android:textColor="@color/purple_700"
        android:textSize="20dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="40dp"
        android:layout_marginTop="200dp"
        android:layout_marginRight="30dp"
        android:layout_marginBottom="30dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/logintext"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20dp"
            android:gravity="center_horizontal"
            android:text="회원 로그인"
            android:textAlignment="center"
            android:textColor="@color/purple_700"
            android:textSize="25dp"
            android:textStyle="bold" />

        <EditText
            android:id="@+id/my_Id"
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:layout_marginBottom="10dp"
            android:background="@color/white"
            android:ems="10"
            android:hint="아이디"
            android:inputType="textEmailAddress"
            android:text=""
            android:textColor="@color/black"
            android:textColorHint="@color/gray" />

        <EditText
            android:id="@+id/my_pw"
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:background="@color/white"
            android:ems="10"
            android:hint="비밀번호"
            android:inputType="textPassword"
            android:textColor="@color/black"
            android:textColorHint="@color/gray" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/loginbutton"
            android:layout_width="wrap_content"
            android:layout_height="35dp"
            android:layout_gravity="center"
            android:layout_marginTop="20dp"
            android:background="@drawable/cate_button_selector"
            android:text="로그인"
            android:textColor="@color/white"
            android:textSize="13sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/signin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="10dp"
            android:text="회원가입"
            android:textColor="@color/black"
            android:textSize="13dp"
            android:textStyle="bold" />

    </LinearLayout>
</FrameLayout>

 
실제로 코드로 구현된 화면이다.

 
로그인 버튼을 눌렀을 때 위의 공식문서에서의 코드를 바탕으로 EditText의 값을 받아와서 함수에 넣어주면 된다.

public void onClick(View view) {
    String email=my_Id.getText().toString();
    String password=my_pw.getText().toString();
    mAuth.signInWithEmailAndPassword(email, password)
            .addOnCompleteListener(Login.this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    if (task.isSuccessful()) {
                        Toast.makeText(Login.this,"로그인 성공",Toast.LENGTH_LONG).show();
                        FirebaseUser user = mAuth.getCurrentUser();
                        Intent intent=new Intent(getApplicationContext(),HomeActivity.class);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |  Intent.FLAG_ACTIVITY_CLEAR_TOP);//액티비티 스택제거
                        startActivity(intent);
                    } else {
                        Toast.makeText(Login.this, "로그인 실패", Toast.LENGTH_SHORT).show();
                    }
                }
            });
}

 

3)리뷰 및 좋아요

사실 리뷰 및 좋아요 기능을 구현하려고 할 때 어려웠던 점이 리뷰가 삭제도 가능해야하고 각 관광지의 리뷰와 각 유저간의 리뷰도 구분하여 가져올 수 있어야 하는데 어떻게 구현해야할 지 방식이 생각이 잘 나지않았다. 여러번의 시도 끝에 나는 Firestore에서 문서를 삭제할 때 문서의 ID가 있어야 되기 때문에 리뷰를 저장할 때 생성과 동시에 저장된 리뷰데이터를 불러올 때 문서의 아이디를 저장해주는 방식으로 구현하였고 유저, 관광지 구분을 위해 관광지 이름과 유저의 이메일도 같이 저장하였다. 
 
처음으로 리뷰가 보이게될 틀이 될 xml을 구현해 보았다. 이 리뷰페이지는 관광지 이름을 위에 띄우고 리스트뷰로 리뷰가 리스트형태로 유동적으로 나올 수 있게 만들었다. 그리고 제일 밑에는 리뷰를 작성할 수있게 EditText, 저장할 수 있게 버튼을 붙여 주었다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerHorizontal="false"
    android:background="#FFFFF0"
    android:orientation="vertical"
    android:verticalScrollbarPosition="left">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/cityName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/gray"
            android:gravity="center"
            android:text="관광지 이름"
            android:textColor="#000000"
            android:textSize="34sp"
            android:textStyle="bold" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <FrameLayout
                android:id="@+id/routeFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="2">

                <ListView
                    android:id="@+id/review_rlist"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />
            </FrameLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:orientation="horizontal">

                <EditText
                    android:id="@+id/reviewtextbox"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginRight="10dp"
                    android:hint="여기에 리뷰를 작성해 주세요"
                    android:inputType="text"
                    android:padding="15dp"
                    android:textColor="#000000"
                    android:textColorHint="#8B000000" />

                <Button
                    android:id="@+id/reviewbutton"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:scrollIndicators="none"
                    android:text="등록"
                    android:textColor="@color/white" />
            </LinearLayout>

        </LinearLayout>
    </LinearLayout>


</LinearLayout>

 
실제로 구현된 화면이다. 

 
등록버튼을 눌렀을 때 로그인이 돼있는 경우 로그인된 유저의 정보를 Firebaseuser 을 통해 받아온 다음 EditText에 있는 값을 받아오고 유저이메일과 관광지이름을 FireStore에 저장해주면 된다.

user = FirebaseAuth.getInstance().getCurrentUser();
if (user == null) {
    Toast.makeText(getApplicationContext(),"로그인을 하세요",Toast.LENGTH_LONG).show();
    Intent go_intent=new Intent(ReviewActivity.this, Login.class);
    startActivity(go_intent);
} else {
    String email=user.getEmail();
    reviewtextbox=findViewById(R.id.reviewtextbox);
    Map<String, Object> adddata = new HashMap<>();
    adddata.put("review_text", reviewtextbox.getText().toString());
    adddata.put("user_email",email);
    adddata.put("con_name",name);



    db.collection("Review_Data")
            .add(adddata)
            .addOnSuccessListener(new OnSuccessListener<DocumentReference>() {
                @Override
                public void onSuccess(DocumentReference documentReference) {
                    Toast.makeText(getApplicationContext(),"리뷰등록완료!",Toast.LENGTH_LONG).show();
                    review.clear();
                    db.collection("Review_Data")
                            .whereEqualTo("con_name",name)
                            .get()
                            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                                @Override
                                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                                    if (task.isSuccessful()) {
                                        adddata.put("document_id",documentReference.getId());
                                        db.collection("Review_Data").document(documentReference.getId())
                                                .set(adddata)
                                                .addOnSuccessListener(new OnSuccessListener<Void>() {
                                                    @Override
                                                    public void onSuccess(Void aVoid) {

                                                    }
                                                })
                                                .addOnFailureListener(new OnFailureListener() {
                                                    @Override
                                                    public void onFailure(@NonNull Exception e) {
                                                    }
                                                });


                                        for (QueryDocumentSnapshot document : task.getResult()) {
                                            review.add(document.get("review_text",String.class));
                                        }
                                        ReviewListAdapter=new List_Adapter_Review(ReviewActivity.this,review);
                                        review_List.setAdapter(ReviewListAdapter);
                                    } else {
                                        Log.d(TAG, "Error getting documents: ", task.getException());
                                    }
                                }
                            });
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    Log.w(TAG, "Error adding document", e);
                }
            });




}

'Project > 경남드리' 카테고리의 다른 글

[Android][JAVA]2장. 경남드리  (0) 2023.10.18
1장. 학기에서 공모전으로  (4) 2023.10.17
0장. 시작  (0) 2023.10.17

처음에 앱을 만들고 Rest API 를 사용하려고 방법을 찾고 공부해봤을 때 어려운 점이 많았어서 내가 배운 것을 정리할 겸 이 글을 보고 Rest API 를 조금 더 쉽게 사용할 수 있도록 이 글을 적어 보려고 한다.
 
일단 첫 글에는 네이버 지도 API 인 Maps에서 Rest API 를 사용하는 법에 대해 예를 들어 적어보려고 한다. 

1) Directions 5 API

https://guide.ncloud-docs.com/docs/maps-direction5-api

 

Directions 5 API

 

guide.ncloud-docs.com

Directions 5는 네이버에서 지원하는 Rest API 중 하나로 출발지 목적지 및 경유지(선택 사항 최대 5개)의 정보를 보내면 응답으로 경로 데이터배열을 받는다. 

1-1)사용법

Rest API의 사용법에서 가장 중요한 것은 요청 파라미터와 헤더를 Interface로 구현하고 응답바디를 Class로 구현 하는 것이다.
Directions 5 공식문서에 보면 

이런 식으로 요청 파라미터, 요청헤더, 요청 바디가 있다.
요청 파라미터와  헤더를 살펴보자

요청 파라미터
요청헤더

보통 헤더는 해당 사이트의 Rest API를 사용하는 경우라면 비슷하거나 같은 경우가 많은 것 같다. 그래서 우리가 중요하게봐야하는 항목은 파라미터 부분이다. 파라미터에 이름들을 전부 공식문서의 이름들과 똑같이 해주어야 한다.  오타나 다르게 하면 작동하지 않는다.. 또한 필수 여부에 Y라고 되어있는 파라미터는 필수적으로 넣어줘야하고 부가적으로 더 얻고 싶은 정보가 있다면 넣어주면 된다. 또한 Rest API는 전부 String 형태로 서버에 요청을 보내기 때문에 파라미터의 자료형은 String으로 맞춰주면 된다. 

public interface RouteFind_nomid {
    @GET("v1/driving")
    Call<RoutePath> getData(@retrofit2.http.Header("X-NCP-APIGW-API-KEY-ID")String ClientID,
                            @Header("X-NCP-APIGW-API-KEY") String Secret,
                            @Query("start") String start, @Query("goal") String goal)
                            ;
}

 
이건 내가 만든 Driving5의 요청 Interface이다. 여기서 Call은 Rest API를 호출할때 사용되는 명으로 나중에 다루기로 한다. 또한 RoutePath는 응답바디를 구성하는 Class 명이다. 그리고 getData라는 메소드를 만들어서 이를 통해 서버에 요청을 보낼 수 있다.
@Get 부분은 서버에 정보를 요청하는 부분인데 @GET("API URL에서 끝에 들어가는부분 ")이다 
예를 들어

https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving

에서에서는 끝부분에 해당하는 v1/dirving 이다
 
●헤더부분은 @Header ("헤더명") 자료형 변수명 으로 구현
● 파라미터 부분은 @Query("파라미터명") 자료형 변수명 으로 구현
 

■요청바디

요청바디 또한 공식문서를 보면 

요청 바디

요청바디는 Class로 구현하는데 필수여부로 돼있는 부분은 필수적으로 다 구현해주어야한다.  또한 이름도 응답바디의 속성명과 똑같이 해주어야 한다. 그리고 변수별로 get메서드를 적어주어야 한다. 타입을 보면 이해하기 어려울 수 있는데 이때는 응답 예시를 보는게 쉬울 수 있다. 예시 응답 또한 공식문서에 첨부되어 있다. 

 
String처럼 자료형이 정해져있는 경우엔 그대로 쓰면 되지만 route 같이 자료형이 이해하기 어려운 경우엔 응답 예시를 보면 route 안에 trafast가 있다. 이 trafast는 위의 요청 파라미터에서 무엇을 요청했는지에 따라 달라지는데 기본값은  traoptimal 이다. 그리고 trafast안에는 summary path 가 있다. 이러한 것을 Class로 구현하려면 Class를 여러개로 만들어야한다.

public class RoutePath {
    @SerializedName("code")
    @Expose
    private int code;
    @SerializedName("messge")
    @Expose
    private String messge;

    @SerializedName("currentDateTime")
    @Expose
    private String currentDateTime;

    @SerializedName("route")
    @Expose
    private Route route;

    public int getCode() {
        return code;
    }

    public Route getRoute() {
        return route;
    }

    public String getCurrentDateTime() {
        return currentDateTime;
    }

    public String getMessge() {
        return messge;
    }

}

class Route{
    @SerializedName("traoptimal")
    @Expose
    private List<Traoptimal> traoptimal;

    public List<Traoptimal> getTraoptimal() {
        return traoptimal;
    }
}

class Traoptimal{
    @SerializedName("summary")
    @Expose
    private Summary summary;
    @SerializedName("path")
    @Expose
    private List<List<Double>> path;

    public Summary getSummary() {
        return summary;
    }

    public List<List<Double>> getPath() {
        return path;
    }
}

class Summary{
    @SerializedName("start")
    @Expose
    private Start start;
    @SerializedName("goal")
    @Expose
    private Goal goal;

    @SerializedName("distance")
    @Expose
    private int distance;

    public Goal getGoal() {
        return goal;
    }

 
이게 예시 응답과 공식문서를 참고하여 만든 Class이다. 보면 Route라는 Class를 만들어서 자료형 처럼 사용하고 있고 그 밑에 여러 Class 및 기본 자료형을 두어서 필수 여부에 Y가 돼있는 것을 다 만들어 주고 Get 메서드까지 달아 주었다. 
이렇게 Interface와 Class를 만들어 주었다면 이제 실제로 Retrofit을 통해 호출해 보자 

■Retrofit
Retrofit은 Rest API를 사용할 때 쓰는 것으로 사용방법은 
1) 의존성 추가 

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.4'

 
gradle에 의존성을 추가해주고 
2) 인터넷 권한 설정

<uses-permission android:name="android.permission.INTERNET" />

Manifests에 권한 설정을 추가해주면 된다. 
 
이제 실제로 호출하는 코드를 보자.

Retrofit retrofit0 = new Retrofit.Builder()
                        .baseUrl("https://naveropenapi.apigw.ntruss.com/map-direction/")
                        .addConverterFactory(GsonConverterFactory.create(gson))
                        .build();
                RouteFind_nomid routeFind0 = retrofit0.create(RouteFind_nomid.class);
                Call<RoutePath> call0 = routeFind0.getData(NavaApIKey,secret,start_lng+","+start_lot,end_lng+","+end_lot);        //네이버 길찾기 rest api 시작 출발점 찍으면 됨
                call0.enqueue(new Callback<RoutePath>() {
                    @Override
                    public void onResponse(Call<RoutePath> call, Response<RoutePath> response) {
                        if(response.isSuccessful()) {
                            RoutePath routePath=response.body();
                            List<List<Double>> path=routePath.getRoute().getTraoptimal().get(0).getPath();
                            PathOverlay line_path=new PathOverlay();       //길 선 표시할 path 배열
                            for(int i=0;i<path.size();i++) {
                                list.add(new LatLng(path.get(i).get(1), path.get(i).get(0)));
                            }
                            markers[0]=new Marker();
                            markers[0].setPosition(new LatLng(start_lot,start_lng));
                            markers[0].setCaptionText("출발지");
                            markers[0].setMap(naverMap);
                            markers[1]=new Marker();
                            markers[1].setPosition(new LatLng(end_lot,end_lng));
                            markers[1].setCaptionText("목적지");
                            markers[1].setMap(naverMap);
                            line_path.setCoords(list);
                            line_path.setMap(naverMap);

                        }
                    }

                    @Override

                    public void onFailure(Call<RoutePath> call, Throwable t) {
                        Log.d(TAG,"실패");
                    }
                });

 
이 코드는 Rest API를 통해 서버에 출발 지 목적지 위경도를 주어서 거기까지의 경로를 선으로 그리고 출발지 목적지에 마커를 찍는 코드이다.

사용할 때 Retrofit 을 선언해주고 .baseUrl 부분에 서버에 요청을 보낼 baseUrl

즉 v1/driving 앞에 부분을 넣어주고 RouteFind_nomid 클래스를 선언해준 Retrofit의 .create 메서드 안에 넣어준다.

그리고 Call 을 선언해서 앞에 선언한 Retrofit이 Interface의 getData의 값을 받게된다.

getData안에 헤더값과 요청하고자 하는 값을 넣어주면 선언 해준 call0로 enque를 보내서 response를 통해 결과값

즉 응답값을 받게 된다.

RoutePath 우리가 선언해준 Class에 response.body() 즉 응답바디를 넣으면 우리가 선언해둔 값들을 사용할 수 있게 되는 것이다. 

결과는 이렇게 잘 나온 것을 볼 수 있다.

경남드리는 경상남도 관광앱으로 사용자가 경상남도의 지역을 출발지, 목적지에 입력하고 테마를 선택하여 경로 만들 수 있게 하는 사용자 맞춤형 여행 설계 앱이다. 
경남드리의 주요 기능에는 여행지 설계 기능, 리뷰기능, 여행지 추천기능이 있다. 이 기능들을 구현하면서 배운 것들에 대해 적어보려고 한다.

1. 여행지 설계기능 

1-1. 홈화면

 처음 홈화면에는 AutoCompleteTextView로 출발지와 도착지를 타이핑할 수 있게 만들었으며, RadioGroup을 활용하여 원하는 테마를 선택할 수 있게 만들었다.
여기서 AutoCompleteTextView에 자동완성이 뜨게 하려면 Adapter 를 붙여 주어야한다 일단 xml에 AutoCompleteTextView를 만든다음

<AutoCompleteTextView
    android:id="@+id/StartPoint"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:background="@drawable/border_radius"
    android:ems="10"
    android:hint="출발지"
    android:inputType="text"
    android:padding="5dp"
    android:textColor="#000000"
    android:textColorHint="@color/black" />

<AutoCompleteTextView
    android:id="@+id/EndPoint"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:background="@drawable/border_radius"
    android:ems="10"
    android:hint="도착지"
    android:inputType="text"
    android:padding="5dp"
    android:textColor="#000000"
    android:textColorHint="@color/black" />

<Button
    android:id="@+id/MapPoint"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:background="#27005D"
    android:text="여행경로만들기"
    android:textColor="@color/white" />

<HorizontalScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RadioGroup
        android:id="@+id/rg"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <RadioButton
            android:id="@+id/nature_check"
            android:layout_width="130dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="10dp"
            android:buttonTint="#000000"
            android:text="자연관광"
            android:textAlignment="center"
            android:textColor="@color/black"
            android:textSize="16sp" />

        <RadioButton
            android:id="@+id/leisure_check"
            android:layout_width="130dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="10dp"
            android:buttonTint="#000000"
            android:text="레저관광"
            android:textAlignment="center"
            android:textColor="@color/black"
            android:textSize="16sp" />

        <RadioButton
            android:id="@+id/culture_check"
            android:layout_width="130dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="10dp"
            android:buttonTint="#000000"
            android:text="문화관광"
            android:textAlignment="center"
            android:textColor="@color/black"
            android:textSize="16sp" />
    </RadioGroup>
</HorizontalScrollView>

 
Java코드에서 AutoCompleteTextView를 불러와서 자동완성에서 뜨게할 목록을 ArrayList에 add한 다음 setAdapter로 붙여 주면 된다. 

searchList = new ArrayList<>();
settingList();
StartPoint = findViewById(R.id.StartPoint);
StartPoint.setAdapter(new ArrayAdapter<String>(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, searchList));

EndPoint = findViewById(R.id.EndPoint);
EndPoint.setAdapter(new ArrayAdapter<String>(this, androidx.appcompat.R.layout.support_simple_spinner_dropdown_item, searchList));
private void settingList() {
    searchList.add("창원시");
    searchList.add("진주시");
    searchList.add("통영시");
    searchList.add("사천시");
    searchList.add("김해시");
    searchList.add("밀양시");
    searchList.add("거제시");
    searchList.add("양산시");
    searchList.add("의령군");
    searchList.add("함안군");
    searchList.add("창녕군");
    searchList.add("고성군");
    searchList.add("남해군");
    searchList.add("하동군");
    searchList.add("산청군");
    searchList.add("함양군");
    searchList.add("거창군");
    searchList.add("합천군");
}

여기서는 settingList()라는 함수를 만들어서 searchList에 값을 추가해서 각 출발 목적지 자동완성뷰에 넣어주었다.


1-2.경로선택화면

 출발지와 도착지를 설정하고 버튼을 클릭했을 때 테마별 가중치를 적용한 다 익스트라 알고리즘을 통해 계산된 경유지와 도착지에 대한 관광지의 목록이 나오게 된다. 상단에는 관광지의 테마별 카테고리를 선택할  수 있도록  HorizontalScrollView안에 RadioGroup을 두어 선택할 수 있게 하였다.
 관광지목록이 뜨는 것은 FrameLayout 안에 ListView를 두어 유동적으로 ListView안의 리스트가 변하는 것에 따라 목록이 바뀔 수 있도록했다.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FFFFF0"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/routeFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="2">

        <ListView
            android:id="@+id/search_list"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

        </ListView>
    </FrameLayout>
</LinearLayout>

1-2-1. 관광지선택
 

처음에는 관광지 선택과 정보보기 둘다 버튼으로 구현하려고 했지만 두개 다 버튼으로 구현했을 때 버튼의 Onclick 이벤트가 작동하지 않아서 관광지 선택은 TextView로 구현하고  리스트뷰를 클릭했을 때 인벤트가 작동하도록 만들어서 선택과 정보보기가 둘다 가능하도록 만들었다. 
또한 선택했을 때 위의 Texview에 경유지가 추가되고 선택해제 했을 때 다시 없어질 수 있도록 구현 하였으면 이때 해당 관광지의 위경도 값도 같이 ArrayList에 추가하여  다음 지도표시화면에 사용할 수 있도록하였다.

int item_n=mid_data.indexOf(item);
double d1=Double.parseDouble(mid_lat.get(item_n));
result_lat.add(d1);
double d2=Double.parseDouble(mid_long.get(item_n));
result_long.add(d2);
Route_List.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long l) {
        TextView text_title=view.findViewById(R.id.title);
        TextView route_choice=view.findViewById(R.id.route_choice);

        if(is_Checked.get(position)==false){
            is_Checked.set(position,true);
            route_choice.setBackgroundColor(Color.parseColor("#AED2FF"));
            String item=text_title.getText().toString();
            result_name.add(item);
            if((rot.length-2)>0){
                int item_n=mid_data.indexOf(item);
                double d1=Double.parseDouble(mid_lat.get(item_n));
                result_lat.add(d1);
                double d2=Double.parseDouble(mid_long.get(item_n));
                result_long.add(d2);
            }else {
                int item_n=route_data.indexOf(item);
                double d1=Double.parseDouble(route_lat.get(item_n));
                result_lat.add(d1);
                double d2=Double.parseDouble(route_long.get(item_n));
                result_long.add(d2);
            }

            route_text.append(" "+item);
        }
        else{
            is_Checked.set(position,false);
            route_choice.setBackgroundColor(Color.parseColor("#FFFFF0"));
            String item=text_title.getText().toString();
            result_name.remove(item);
            String remove_item=route_text.getText().toString();
            remove_item=remove_item.replaceAll(item,"");
            route_text.setText(remove_item);
            if((rot.length-2)>0){
                int item_n=mid_data.indexOf(item);
                result_lat.remove(Double.parseDouble(mid_lat.get(item_n)));
                result_long.remove(Double.parseDouble(mid_long.get(item_n)));
            }else {
                int item_n=route_data.indexOf(item);
                result_lat.remove(Double.parseDouble(route_lat.get(item_n)));
                result_long.remove(Double.parseDouble(route_long.get(item_n)));
            }
        }

    }
});

1-2-2. 정보보기 

정보 보기를 클릭했을 때의 관광지 화

관광지 정보보기는 FireStore상에서 그 관광지에 대한 사진, 설명을 쿼리를 작성하여 관광지페이지에 전달해서 관광지화면을 띄울 수 있도록 했다. 
코드가 길어서 일부분만 첨부한다. 

inform_b.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        fileurl.clear();
        data_con.clear();
        String i=title.getText().toString();
        db.collection("nature_data")
                .whereEqualTo("data_title",i)
                .get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {

                            for (QueryDocumentSnapshot document : task.getResult()) {
                                fileurl.add(document.get("fileurl1",String.class));
                                data_con.add(document.get("data_content",String.class));
                            }
                            if(fileurl.size()>0&&data_con.size()>0){
                                fileurl1=fileurl.get(0);
                                data_content=data_con.get(0);
                                Intent intent=new Intent(context, City_Page_Activity.class);
                                if(data_content.length()>0 && fileurl1.length()>0 ) {
                                    data_content = data_content.replaceAll("<(/)?([a-zA-Z]*)(\\s[a-zA-Z]*=[^>]*)?(\\s)*(/)?>", "");
                                    data_content = data_content.replaceAll("<[^>]*>", " ");
                                    data_content = data_content.replace("/(<([^>]+)>)/", "");
                                    intent.putExtra("fileurl1", fileurl1);
                                    intent.putExtra("data_content", data_content);
                                    intent.putExtra("name", i);
                                    context.startActivity(intent);
                                }

3. 여행지 길찾기 및 지도 기능

경유지와 출발지 도착지의 위경도 값을 전의 Activity에서 받은 뒤 Rest api로 Naver Map api중 하나인  Driving 5를 호출하여 지도 상에 길선을 그리고 마커를 찍는다. 

-3-1지도기능

●Driving 5
Rest api에 관한 내용을 설명이 길어서 나중에 글을 작성할 예정이지만 여기서 짧게나마 설명하고 넘어가려고 한다.
Rest api를 사용하는데 중요한 2가지 요소는 
해당 서버에 Get요청을 넣을 Interface와 Get 요청을 했을 때 결과값을 받을 Class 이다.
-1 Interface
 Interface는 보통 해당 api 공식문서에 보면 어떻게 적어야하는지 잘 나와있다. 
https://api.ncloud-docs.com/docs/ai-naver-mapsdirections-driving

driving

api.ncloud-docs.com

요청 바디
요청헤더

요청해더는 아래와 같이 @Header를 통해 작성하고 요청바디는 @Query로 작성하고 필수여부에서 Y인 부분을 다 적고 나머지 필요한 것은 추가적으로 적으면 된다.

public interface RouteFind {
    @GET("v1/driving")
    Call<RoutePath> getData(@Header ("X-NCP-APIGW-API-KEY-ID")String ClientID,
                            @Header("X-NCP-APIGW-API-KEY") String Secret,
                            @Query("start") String start,@Query("goal") String goal,
                            @Query("waypoints")String waypoints);
}

-2 Class
 

응답 바디는 필수적인 부분을 Class로 받을 수 있게 공식문서에 나온 타입을 사용하여 작성한다. 또한 get메소드를 작성해서 값을 가져다 쓸 수 있게한다. 

package com.example.k_contest;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import java.nio.DoubleBuffer;
import java.util.List;

public class RoutePath {
    @SerializedName("code")
    @Expose
    private int code;
    @SerializedName("messge")
    @Expose
    private String messge;

    @SerializedName("currentDateTime")
    @Expose
    private String currentDateTime;

    @SerializedName("route")
    @Expose
    private Route route;

    public int getCode() {
        return code;
    }

    public Route getRoute() {
        return route;
    }

    public String getCurrentDateTime() {
        return currentDateTime;
    }

    public String getMessge() {
        return messge;
    }

}

class Route{
    @SerializedName("traoptimal")
    @Expose
    private List<Traoptimal> traoptimal;

    public List<Traoptimal> getTraoptimal() {
        return traoptimal;
    }
}

class Traoptimal{
    @SerializedName("summary")
    @Expose
    private Summary summary;
    @SerializedName("path")
    @Expose
    private List<List<Double>> path;

    public Summary getSummary() {
        return summary;
    }

    public List<List<Double>> getPath() {
        return path;
    }
}

class Summary{
    @SerializedName("start")
    @Expose
    private Start start;
    @SerializedName("goal")
    @Expose
    private Goal goal;

    @SerializedName("distance")
    @Expose
    private int distance;

    public Goal getGoal() {
        return goal;
    }

    public Start getStart() {
        return start;
    }

    public int getDistance() {
        return distance;
    }
}
class Start
{
    @SerializedName("location")
    @Expose
    private List<Double> location;

    public List<Double> getLocation() {
        return location;
    }
}

class Goal
{
    @SerializedName("location")
    @Expose
    private List<Double> location;

    public List<Double> getLocation() {
        return location;
    }
}

Retrofit retrofit0 = new Retrofit.Builder()
        .baseUrl("https://naveropenapi.apigw.ntruss.com/map-direction/")
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build();
RouteFind_nomid routeFind0 = retrofit0.create(RouteFind_nomid.class);
Call<RoutePath> call0 = routeFind0.getData(NavaApIKey,secret,start_lng+","+start_lot,end_lng+","+end_lot);        //네이버 길찾기 rest api 시작 출발점 찍으면 됨
call0.enqueue(new Callback<RoutePath>() {
    @Override
    public void onResponse(Call<RoutePath> call, Response<RoutePath> response) {
        if(response.isSuccessful()) {
            RoutePath routePath=response.body();
            List<List<Double>> path=routePath.getRoute().getTraoptimal().get(0).getPath();
            PathOverlay line_path=new PathOverlay();       //길 선 표시할 path 배열
            for(int i=0;i<path.size();i++) {
                list.add(new LatLng(path.get(i).get(1), path.get(i).get(0)));
            }
            markers[0]=new Marker();
            markers[0].setPosition(new LatLng(start_lot,start_lng));
            markers[0].setCaptionText("출발지");
            markers[0].setMap(naverMap);
            markers[1]=new Marker();
            markers[1].setPosition(new LatLng(end_lot,end_lng));
            markers[1].setCaptionText("목적지");
            markers[1].setMap(naverMap);
            line_path.setCoords(list);
            line_path.setMap(naverMap);

        }
    }

Rest api를 호출할때는 Retrofit.Builder를 통하여 빌드하고 Get구문과 결과를 받을 Class를 작성하여 값을 받을 수 있게 한다. 또한 결과를 받는 값을 쓸 때는 response.body()를 통해 사용한다. 
도로선을 맵에 사용할때는 path 즉 경로의 수만큼의 ArrrayList를 add하여 사용한다.

-3-2 길찾기 

길찾기는 url Scheme로 구현했는데 Naver api의 url Scheme 문서를 참고하여 구현하면 된다.
https://guide.ncloud-docs.com/docs/maps-url-scheme

지도 앱 연동 URL Scheme

guide.ncloud-docs.com

위의 문서를 참고하여 만약 지도앱이 설치되어있다면 그 앱에 해당하는 url Scheme 기능이 실행되고 아니면 설치화면으로 이동하도록 구현하였다.
 

list = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (list == null || list.isEmpty()) {
    try {
        if(item.equals(st)){
            startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url_f+String.valueOf(region_position[stringToInt(item)][0])+"&dlng="+String.valueOf(region_position[stringToInt(item)][1])+"&dname="+encodeResult+url_b)));
        } else if (item.equals(ed)) {
            startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url_f+String.valueOf(region_position[stringToInt(item)][0])+"&dlng="+String.valueOf(region_position[stringToInt(item)][1])+"&dname="+encodeResult+url_b)));
        }else {
            startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url_f + String.valueOf(result_lat[stringToInt_s(item)]) + "&dlng=" + String.valueOf(result_long[stringToInt_s(item)]) + "&dname=" + encodeResult + url_b)));
        }
    }catch (Exception e){
        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.nhn.android.nmap")));
    }
} else {
    startActivity(intent);
}

'Project > 경남드리' 카테고리의 다른 글

[Android][JAVA]3장. 부가기능  (0) 2023.11.04
1장. 학기에서 공모전으로  (4) 2023.10.17
0장. 시작  (0) 2023.10.17

1. 앱 다시 만들기 

학기가 끝나고 방학 중 처음 개발했던 앱의 기능을 바탕으로 공모전에 나가기 위하여 기존의 팀원 중에 팀장을 비롯하여 개발을 주도적으로 했던 인원과 앱 개발을 해보고자하는 인원을 추가로 모아서 앱의 전반적인 디자인을 고치고  기존에 임의로 값을 적용했던 다 익스트라 알고리즘에서 공공데이터를 활용하여 가중치를 설정하고 이 데이터를 활용하여 앱의 기능 을 추가해보고자 했다. 

개발 초기에는 학기 중에 개발을 해왔던 인원이고 개발을 해보고자 하는 인원이 있어서 위와 같은 문제점은 없었고 개발이 잘 진행되리라 생각을 했다. 

2. 다시 소통의 문제 

개발을 하면서 소통의 부재와 같은 문제점이 다시 생기기 시작하였고 나는 데이터를 활용하기위해 rest api와 같은 방법과  어떻게 데이터를 활용하고 쓸 것인가를 계속 찾아보면서 배우는 것에만 신경을 써서 각자의 업무에서의 현황등 개발과정에서의 애로사항이라던지 도움을 줄 수 있는 부분에 대해 알고자 소통을 하려고 하였지만 내 생각만큼 소통이 이루어지지 않았다.

하지만 나는 이 앱을 꼭 완성하고 싶었고 이 앱의 아이디어와 기능을 구현해낼 수 있다면 충분히 상을 탈 수 있다고 생각 했기에 팀장을 비롯하여 팀원들과 계속 개인적으로든 단체연락에서든 소통을 시도하였고 이 과정에서 어떤 부분이 지금 안되는지 어떤 부분에서 어떻게 하고 있는지 의견을 주고 받으면서 이 앱의 기능을 하나씩 완성해나가려 했다. 

3. 그래도 무사히 제출 

다행히 제출기간안에 앱의 주요 기능들을 완성할 수 있었지만 초기에 계획했던 식당, 축제 표시기능과 esg 지수를 보여주어 선택할 수 있게하는 등 여러기능들을 구현하지 못해서 아쉬웠다. 하지만 처음 만들어보는 앱이고 소재가 괜찮다고 생각해서 공모전에 떨어지더라도 이 앱은 추가기능을 남는 시간에라도 천천히라도 구현을 해서 제대로 된 앱을 만들어보고 싶어졌다. 

 

이번에 앱 개발을 하고 공모전 준비를 해보면서 내가 아직 많이 부족하구나 라는 생각과 내가 모르는 부분이 있다면 검색이나 유투브등 다양한 경로를 통해서 찾다보면 찾을 수 있고 스스로 배워나갈 의지가 제일 중요하구나 라는 생각이 가장 많이 들었다. 앱을 만들든 게임을 만들든 가장 중요한 것은 일단 무엇이든 해보면서 내가 모르는 것이 있더라도 부딪혀보면서 몰랐던 것을 알아가면서 성장하면 되는 것이라고 생각해서 일단 무엇이든 할줄 아는 그런 행동력이다. 비록 소통의 문제라던지 문제를 정확히 파악하고 해결하는 것이 아닌 어영부영 하며 넘어가는 그러한 것으로 많이 부족한 앱을 만들어서 공모전에 내긴 했지만 이 앱을 개발하면서 이러한 기능을 어떻게 개발을 하고 어떤식으로 구현을 하면 되겠다라는 생각을 하고 실제로 코딩을 해보면서 정말 많이 배웠기 때문에 나는 얻은게 분명히 있다고 생각을 한다. 이러한 경험을 바탕으로 더 나은 앱, 게임을 만들어 볼 수  있는 사람이 돼가면 좋겠다. 

 

다음 장에서는 경남드리 앱의 주요 기능을 만들면서 배운 것을 정리해서 올려보려고 한다.

'Project > 경남드리' 카테고리의 다른 글

[Android][JAVA]3장. 부가기능  (0) 2023.11.04
[Android][JAVA]2장. 경남드리  (0) 2023.10.18
0장. 시작  (0) 2023.10.17

● 나의 최종목표는 재미있는 게임을 만드는 것이였지만 직접 개발하여 무엇인가를 만들어 보고 싶었기에 학기 중 프로젝트로 관광앱을 만들어보려고 하였으며 만든 앱을 통하여 경상남도 소프트웨어 공모전에 나가보고자 했다.

그때는 처음으로 협엽을 통한 앱 개발 프로젝트를 해보는 것이라 미숙한 점과 어려웠던 점이 많았는데 크게 두가지 정도 문제점을 살펴보자면 

1. 협업툴 사용의 미숙함 

github와 notion 자주 써보지 않아서 처음에 앱을 빌드하고 협업 기획을 하는 데 초반 2~4주차를 협업툴의 설정과 기획에 써야했다.

2. 소통의 부재 

카톡,디스코드 그리고 매주의 회의를 통해 소통을 통한 빠른 개발을 하려고 하였지만 생각만큼 소통이 원활하게 되지않았다. 

 이러한 문제점에서 협업툴같은 경우는 회의시간에 서로 가르쳐주며 맞춰나가면서 해결을 했는데 소통의 부재는 해결법을 찾기가 어려웠다. 학기 중에 앱 개발을 마쳐야하는 프로젝트였기에 팀장은 아니였지만 팀장과 함께 팀원들과 계속 소통하려고 노력하였으며 안되더라도 나의 개발할 부분을 최대한 빨리 끝내두고 안된 팀원의 부분을 돕거나 코드를 보고 필요하거나 더 개발할 부분을 개발하였다.

  진행이 늦어지면서 처음 생각했던 기능들에서 구현 못한 부분이 많았지만 핵심기능인 길찾기 기능과 다익스트라 알고리즘을 사용한 관광 경유지 추천기능을 완성할 수 있었다. 

 

'Project > 경남드리' 카테고리의 다른 글

[Android][JAVA]3장. 부가기능  (0) 2023.11.04
[Android][JAVA]2장. 경남드리  (0) 2023.10.18
1장. 학기에서 공모전으로  (4) 2023.10.17

■데이터 모델링

테이블간의 관계를 표현하고 설계하는 것

●추상화

현실세계에 있는 데이터를 컴퓨터세계에 데이터베이스로 만들때 필요한 것으로

객체의 속성 중 가장 중요한 것에만 중점을 두어 줄여서 표현하는것이다.

설계 과정

1. 요구사항 수집 및 분석

2. 개념적 모델링

3. 논리적 모델링

4. 물리적 모델링

5. 데이터베이스 구현

 

'Database > 이론' 카테고리의 다른 글

데이터베이스 2강  (0) 2023.04.29
데이터베이스 1강  (0) 2023.04.20

※관계 데이터 모델

■용어 설명

릴레이션 관계 스키마(내포) 인스턴스(외연) 도메인 차수 카디날리티
행과 열로 구성된 테이블 릴레이션과 데이터 , 릴레이션과 릴레이션의 관계 속성,특징  데이터,내용 속성이 가질 수 있는 값 속성의 개수 튜플의 수 

 

 

■릴레이션의 특징

●속성

1.속성은 하나씩만 있어야하며

2.서로 다른 이름,

3.한 속성에서는 같은 도메인값(자료형)

4.순서는 상관없다.

●투플

1.중복은 허용하지 않는다

2.순서는 상관없다

 

 

 

■키

데이터베이스에서 키는 특정 투플을 식별할 때 사용하는 속성 혹은 속성의 집합이다.

●슈퍼키

투플을 유일하게 식별할 수 있는 하나의 속성 혹은 속성의 집합

●후보키 

투플을 유일하게 식별할 수 있는 최소의 집합

●기본키

여러 후보키 중 하나를 선정하여 대표로 삼는 키

※기본키 선정 시 고려사항

1.릴레이션 내 투플을 식별할 수 있는 고유한 값을 가져야함

2.NULL 값은 허용하지 않음

●대리키

마땅한 기본키가 없을 때 일련번호 같은 가상의 속성을 만들어 기본키로 만드는것 

인조키라고 함

●대체키 

기본키로 선정되지않은 후보키

●외래키

관계에 사용, 다른 테이블의 기본키를 참조하는키

※외래키 선정시 고려사항

1.도메인 같아야하고 참조되는 값이 변하면 참조하는 값도 변해야한다.

2.NULL값이 들어가도 되며,중복이 가능하고, 자기자신을 참조할 수 있으며 기본키의 일부도 가능하다.

■데이터무결성

●도메인 무결성 제약조건

투플이 각 속성의 도메인에 지정된 값만을 가져야 한다는 조건

●개체 무결성 제약조건

기본키 제약조건

●참조 무결성 제약조건

자식 릴레이션의 외래키는 부모 릴레이션의 기본키와 도메인이 동일해야 하며, 자식 릴레이션의 값이 변경될 때 부모 릴레이션의 제약을 받는다는 것

->삭제나 수정시 부모 릴레이션과 자식 릴레이션 모두 영향이 있다

■관계대수

어떻게 질의를 수행하는가 -> 절차적인 언어

●관계대수식

연산자<조건> 릴레이

●관계대수 연산자

SELECT δ(시그마) δ<조건>(R) 조건 선택
PROJECT π(파이) π<리스트>(R) 속성 별 리스트 가져오기
JOIN ⋈(보타이) R⋈<조건>S 조건에 맞는 두 릴레이션 가져오기
DIVISION ÷(나누기) R÷S S에 해당하는 값을 가지고 있는 행가져오기

●각 연산 및 연산별 차수/ 카디널리티 변화

  ●합집합

중복제거 후 두 릴레이션을 합침

->연산 후 차수가 같지만 카디널리티는 같거나 작다.

  ●교집합

두 릴레이션이 공통으로 가지고 있는 것을 보여줌

->연산 후 차수는 같고 카디널리티는 두 릴레이션의 어느것 보다 크지않다

  ●차집합

두 릴레이션에서 겹치지않는 부분 보여줌

->연산 후 차수는 같다 

-> 카디널리티 R-S일경우는 R과 같거나 작고 S-R이면 S와 같거나 작다.

  ●카티션프로덕트

->전체x전체 연산

->연산 후 두 릴레이션의 차수를 더한게 차수이고 카디널리티는 두 릴레이션의 카디널리티의 곱이다.

■조인연산

두 릴레이션의 공통 속성을 기준으로 속성 값이 같은 투플을 수평으로 결합하는 연산

R⋈<조건>S

->동등조인 조건에서 =연산자를 사용한 조인 

   ●자연조인

동등조인에서 중복되는 값을 제외한 연산

R⋈N<조건>S

  ●외부조인선이 있는 쪽에 있는값 모두 출력(상대 릴레이션이랑 맞췄을 때 NULL값이여도 출력)

'Database > 이론' 카테고리의 다른 글

데이터베이스3강  (0) 2023.05.01
데이터베이스 1강  (0) 2023.04.20

+ Recent posts