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

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

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

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

+ Recent posts