728x90

Material Design Navigation Drawer


Material Design 에 대한 자료는 https://github.com/material-components/material-components-android 를 참조하면 도움된다.

안드로이드 네비게이션은 왼쪽에 슬라이드 형식으로 메뉴가 나왔다 들어갔다 하는 패널을 말한다.


앱 build.gradle 추가 사항

implementation 'com.google.android.material:material:1.1.0'


먼저 Material Design 사이트에서 제공하는 예제 파일을 열어봤더니 아래와 같은 예제가 나온다.

자식 View는 반드시 2개만 가진다는 점을 기억하자.

순서는 content 자식 View가 먼저 나오고 drawer View는 나중에 나온다.

그러면 drawer View가 나중에 나오기만 하면 되는 것인가? 아니다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <!-- As the main content view, the view below consumes the entire
         space available using match_parent in both dimensions. Note that
         this child does not specify android:layout_gravity attribute. -->
    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- android:layout_gravity="start" tells DrawerLayout to treat
         this as a sliding drawer on the starting side, which is
         left for left-to-right locales. The navigation view extends
         the full height of the container. A
         solid background is used for contrast with the content view.
         android:fitsSystemWindows="true" tells the system to have
         DrawerLayout span the full height of the screen, including the
         system status bar on Lollipop+ versions of the plaform. -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#333"
        android:fitsSystemWindows="true"
        app:menu="@menu/navigation_view_content"
        app:itemIconTint="@color/emerald_translucent"
        app:itemTextColor="@color/emerald_text"
        app:itemBackground="@color/sand_default"
        app:itemTextAppearance="@style/TextMediumStyle" />

</androidx.drawerlayout.widget.DrawerLayout>


위와 같이 하거나 또는

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <!-- 1. 콘텐츠 영역-->
    <include
        layout="@layout/content_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- 2. 왼쪽 사이드 메뉴-->
    <include layout="@layout/nav_header_main" />   

</androidx.drawerlayout.widget.DrawerLayout>


아래 nav_header_main.xml 파일을 보면 com.google.android.material.navigation.NavigationView 대신에 LinearLayout 으로 되어 있다. 꼭 com.google.android.material.navigation.NavigationView 를 사용하지 않아도 된다는 의미다.

android:layout_gravity="start" 를 추가해서 왼쪽 슬라이드 메뉴로 사용하겠다는 것만 포함되어 있다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navi_view"
    android:layout_width="280dp"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:background="@color/colorWhite"
    android:orientation="vertical"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ededed"
        android:orientation="vertical"
        android:padding="10dp">

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

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/img_info" />

            <ImageView
                android:id="@+id/btn_close"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="right|top"
                android:onClick="NaviItemSelected"
                android:src="@drawable/ic_close" />

        </FrameLayout>
    </LinearLayout>

</LinearLayout>
 


이제 자바 소스를 구현하는 걸 살펴보자.

public class MainActivity extends AppCompatActivity {
    Context context;
    DrawerLayout drawer_layout;
    ActionBarDrawerToggle toggle;
    //NavigationView navigationView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = MainActivity.this;

        drawer_layout = findViewById(R.id.drawer_layout);
        toggle = new ActionBarDrawerToggle(
                this, drawer_layout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawer_layout.addDrawerListener(toggle);
        toggle.syncState();

        // com.google.android.material.navigation.NavigationView 를 구현했을 때 처리 (구현 안해도 됨)
        //navigationView = findViewById(R.id.nav_view);
        //navigationView.setItemIconTintList(null);
        //navigationView.setNavigationItemSelectedListener(this);

        // content_main.xml 에서 지정한 버튼을 클릭하면 drawer 가 오픈되도록 함.
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               drawer_layout.openDrawer(GravityCompat.START);
            }
        });

    }

    // drawer 오픈된 상태에서 실행할 때 동작 이벤트 처리
    public void NaviItemSelected(View view) {
        switch (view.getId()) {
            case R.id.btn_close:
                drawer_layout.closeDrawer(Gravity.LEFT, true);
                break;
            case R.id.btn_logout:
                backPressHandler.appShutdown();
                break;
        }
    }
}


구현을 위한 핵심사항만 살펴본 것이므로 실제 구현 예제는 다른 블로그를 찾아보면 도움될 것이다.

예전 예제들은 android.support.v4.widget.DrawerLayout 로 되어 있는데 androidx.drawerlayout.widget.DrawerLayout 로 변경되었다는 것만 알자.


참고하면 좋은 자료

https://coding-factory.tistory.com/207 네비게이션 Drawer 사용법

https://medium.com/quick-code/android-navigation-drawer-e80f7fc2594f



블로그 이미지

Link2Me

,
728x90

탭을 누를 때마다 아래 화면이 마치 페이지가 넘어가듯이 변하는 것을 ViewPager 를 이용해 구현이 가능하다.

예전에 사용하던 방식에서 계속 새로운 기능으로 개선이 되면서 ViewPager를 이용한 TabLayout 을 연동하는 것이 가장 좋은 방법이다.

material Design에서 제공하는 tabLayout 을 이용하는 방법으로 된 예제다.

https://www.youtube.com/watch?v=YWjyAfEacT8 유투브 동영상 강의 보고 테스트한 걸 적어두었다. (2020.3.27)

그런데 제대로 된 코드가 아니라서 새롭게 수정 구현하고 적어둔다.(Upated 2020.4.26)


앱 gradle 에 라이브러리 추가하기

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0'
}


TabLayout 사용하기

검색창에서 tab 을 입력하면 tabLayout 이 나온다. 마우스로 끌어다가 1번과 같이 놓는다.

material Design 에서 제공하는 tabLayout 이 추가된다.

하는 방법은 https://www.youtube.com/watch?v=YWjyAfEacT8 을 참조하면 된다.

ConstraintLayout 작성하는 방법에 대한 설명이라고 보면 된다.


https://stackoverflow.com/questions/31640563/how-do-i-change-a-tab-background-color-when-using-tablayout

에서 선택된 아이템 배경색을 지정하는 걸 참조해서 구현한다.


android:layout_marginTop="40dp" 으로 해줘야만 공간이 제대로 나오는 버그(?)가 있어서

LinearLayout 으로 변경해서 해결했다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        app:tabTextColor="@color/colorDarkGray"
        app:tabSelectedTextColor="@color/colorWhite"
        app:layout_collapseMode="pin"
        app:tabGravity="fill"
        app:tabBackground="@drawable/tab_color_selector"
        app:layout_constraintBottom_toTopOf="@+id/vp_layout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Monday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Tuesday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Wednesday" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/vp_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tab_layout">

        <androidx.viewpager.widget.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

최근에는 ViewPager2 를 사용하라고 나오는 거 같은데 이건 나중에 테스트해볼 예정이다.


ViewPager 영역에 표시될 Fragment Layout 파일을 추가한다. 3개, 5개는 정하기 나름이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


Fragment1.java 파일

- Activity 에서 전달받은 데이터를 Fragment 에서 받아서 처리하는 코드를 추가했다.

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class Fragment1 extends Fragment {
    private View view;

    private String parameter;


    public static Fragment1 newInstance(String parameter){
        Fragment1 fragment1 = new Fragment1();
        Bundle args = new Bundle(); //Use bundle to pass data
        args.putString("parameter", parameter);
        instance.setArguments(args); //Finally set argument bundle to fragment
        return instance;
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            parameter = getArguments().getString("parameter");
            Log.e("FragParameter", parameter);
        }
    }


    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_1,container,false);
        return view;
    }


    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}

public class Fragment2 extends Fragment {
    private View view;

    public static Fragment2 newInstance(){
        Fragment2 fragment2 = new Fragment2();
        return fragment2;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_2,container,false);
        return view;
    }
}
 

public class Fragment3 extends Fragment {
    private View view;

    public static Fragment3 newInstance(){
        Fragment3 fragment3 = new Fragment3();
        return fragment3;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_3,container,false);
        return view;
    }
}
 



ViewPagerAdapter.java

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;

import java.util.ArrayList;
import java.util.List;

class ViewPagerAdapter extends FragmentPagerAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();
    private final List<String> mFragTitleList = new ArrayList<>();

    public ViewPagerAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFrag(Fragment fragment, String title) {
        mFragmentList.add(fragment);
        mFragTitleList.add(title);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragTitleList.get(position);
    }
}


MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;

import android.os.Bundle;

import com.google.android.material.tabs.TabLayout;

public class MainActivity extends AppCompatActivity {
    private ViewPager viewPager;
    private ViewPagerAdapter adapter;
    private TabLayout tabLayout;


    String code;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        code = getIntent().getExtras().getString("code");
        Log.e("MainActivityCode",code);


        Fragment frag1 = new Fragment1().newInstance(code);

        viewPager = findViewById(R.id.viewPager);
        tabLayout = findViewById(R.id.tab_layout);
        adapter = new ViewPagerAdapter(getSupportFragmentManager());
        adapter.addFrag(frag1, "Web");
        adapter.addFrag(new Fragment2(), "사랑");
        adapter.addFrag(new Fragment3(), "우정");
        viewPager.setAdapter(adapter);
        tabLayout.setupWithViewPager(viewPager);
    }
}


여기까지 하면 잘 동작할 것이다.


참고하면 좋을 자료

출처 : https://guides.codepath.com/android/viewpager-with-fragmentpageradapter

public class FirstFragment extends Fragment {
    // Store instance variables
    private String title;
    private int page;

    // newInstance constructor for creating fragment with arguments
    public static FirstFragment newInstance(int page, String title) {
        FirstFragment fragmentFirst = new FirstFragment();
        Bundle args = new Bundle();
        args.putInt("someInt", page);
        args.putString("someTitle", title);
        fragmentFirst.setArguments(args);
        return fragmentFirst;
    }

    // Store instance variables based on arguments passed
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        page = getArguments().getInt("someInt", 0);
        title = getArguments().getString("someTitle");
    }

    // Inflate the view for the fragment based on layout XML
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_first, container, false);
        TextView tvLabel = (TextView) view.findViewById(R.id.tvLabel);
        tvLabel.setText(page + " -- " + title);
        return view;
    }
}
 


블로그 이미지

Link2Me

,
728x90

Java 로 코딩된 프로젝트에 코틀린 코드 일부를 추가하는 걸 테스트 해보고 간단하게 적어둔다.


프로젝트 build.gradle

buildscript {
    ext.kotlin_version = '1.3.71'
    repositories {
        google()
        jcenter()
       
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}


앱 build.gradle

apply plugin: 'com.android.application'
// 코틀린 혼용 사용을 위해 추가
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.javakotlin"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.annotation:annotation:1.1.0'

    implementation 'com.google.android.material:material:1.0.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'

    implementation 'com.squareup.retrofit2:retrofit:2.7.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
}


Java 코드 파일과 kotlin 코드 파일을 혼용하여 사용해봤는데 잘 동작하는 걸 확인할 수 있었다.

코틀린을 완벽하게 이해한 상태이면 Java 코드 필요없겠지만 코틀린 문법 공부 하다 말아서 이렇게라도 해보는 것이 의의가 있다.


블로그 이미지

Link2Me

,
728x90

Meterial Design 로그인 예제 Layout 을 적어둔다.


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

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

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingBottom="@dimen/activity_vertical_margin">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="300dp"
                android:gravity="center"
                android:text="Login"
                android:textStyle="bold"
                android:textSize="45dp"
                android:fontFamily="cursive"
                android:textColor="@color/colorWhite"
                android:layout_marginTop="0dp"
                android:background="@drawable/logo"/>

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:layout_marginRight="20dp">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_id"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@color/colorWhite"
                    android:autofillHints="@string/prompt_id"
                    android:hint="아이디"
                    android:selectAllOnFocus="true" />

            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="20dp">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_pw"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:autofillHints="@string/prompt_pw"
                    android:hint="비밀번호"
                    android:background="@color/colorWhite"
                    android:imeOptions="actionDone"
                    android:inputType="textPassword"
                    android:selectAllOnFocus="true" />

            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.button.MaterialButton
                android:id="@+id/btn_login"
                style="@style/Widget.AppCompat.Button.Borderless.Colored"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="50dp"
                android:layout_marginRight="20dp"
                android:padding="10dp"
                android:text="로그인"
                android:textColor="@color/colorWhite"
                android:textSize="16sp"
                android:textStyle="bold"
                app:backgroundTint="@color/colorMint"
                app:cornerRadius="20dp" />

        </LinearLayout>
    </FrameLayout>

</ScrollView>


이미지 파일

logo.zip



'안드로이드 > Layout' 카테고리의 다른 글

Floating Action Button  (0) 2020.07.11
CardView Layout 예제  (0) 2020.04.12
ScrollView  (0) 2019.12.20
Dynamic Layouts  (0) 2019.01.06
Android Fragment 기본 예제  (0) 2018.09.11
블로그 이미지

Link2Me

,
728x90

그동안은 Retrofit을 사용할 일이 전혀 없었는데, 사용할 일이 있어서 관련 자료를 찾아서 테스트하고 적어둔다.

예제를 이해하는데 참 어려웠다. 그래서 유투브 동영상도 보고 이해하고 따라해보고 동작되는 걸 확인한 후에야 로직이 이해가 되었다.

구글에서 추천하는 Volley 라이브러리보다 속도가 빠르다고 검색결과가 나온다.

 

Retrofit 은 안전한 타입 방식의 HTTP 클라이언트로서 Android 와 Java 애플리케이션을 위한 라이브러리이다.

Json converter : JSON 타입의 응답결과를 객체로 매핑(변환)해주는 Converter

https://square.github.io/retrofit/ 를 참조한다. 그런데 설명만으로는 이해하기 어렵더라.

 

이해를 돕기 위해서 그림으로 설명한다.

Interface 생성

이 부분에는 GET/POST/PUT/DELETE 등 필요한 모든 함수를 선언하는 부분이다.

Call <> 사이에는 서버의 Json 형태에 따라 클래스 파일을 만들어서 넣어주면 된다.

/**
 * GET 방식, URL/getphoto.php/{code} 호출.
 * Data Type의 여러 개의 JSON을 통신을 통해 받음.
 * 주소값이 "http://www.abc.com/getphoto.php?code=1" 이 됨.
 * @param code 요청에 필요한 code
 * @return 다수의 Data 객체를 JSON 형태로 반환.
 */
@GET("/getphoto.php")
Call<Data> getData(@Query("code") String code);

 

 

앱 build.gradle 설정

    compileOptions { // 이거 안해주면 에러가 나오더라.
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

dependencies {
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'com.squareup.retrofit2:retrofit:2.7.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
}

 

AndroidManifest.xml

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

android:usesCleartextTraffic="true"

 

activity_login.xml

구글에서 Meterial Design 예제를 검색하면 나온다. 그걸 참조해서 하면 된다.

 

Resource 관련 파일을 첨부한다.

res.zip
다운로드

 

 

LoginActivity.java

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class LoginActivity extends AppCompatActivity implements View.OnClickListener {
    Context context;
    EditText etId;
    EditText etPw;
    String userID;
    String userPW;
    String uID; // 스마트 기기의 대체 고유값

    LoginAPI mloginService;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(LoginActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        context = LoginActivity.this;
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{
                            android.Manifest.permission.READ_PHONE_STATE,
                            android.Manifest.permission.READ_CALL_LOG, // 안드로이드 9.0 에서는 이것도 추가하라고 되어 있음.
                            android.Manifest.permission.CALL_PHONE,  // 전화걸기 및 관리
                            android.Manifest.permission.ACCESS_FINE_LOCATION
                    })
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        etId = findViewById(R.id.et_id);
        etPw = findViewById(R.id.et_pw);
        Button btn_login = findViewById(R.id.btn_login);

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Value.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build();


        mloginService = retrofit.create(LoginAPI.class);
    }

    @Override
    public void onClick(View v) {
        userID = etId.getText().toString().trim();
        userPW = etPw.getText().toString().trim();
        uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
        mloginService.Login(userID, userPW, uID)
                .enqueue(new Callback<LoginResult>() {
            @Override
            public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
                // 네트워크 통신 성공
                LoginResult result = response.body();
                if(result.getResult().contains("success")){
                    Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                    intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY);
                    startActivity(intent);
                    finish();
                } else {
                    showAlert(result.getResult(),result.getMessage());
                }
            }

            @Override
            public void onFailure(Call<LoginResult> call, Throwable t) {
                // 네트워크 통신 실패
            }
        });
    }



    private OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }

    public void showAlert(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        builder.setMessage(message)
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }
}

public interface LoginAPI { // 서버에 호출할 메소드를 선언하는 인터페이스
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(Value.URL_LOGIN)
    Call<LoginResult> Login(
            @Field("userId") String userId,
            @Field("userPw") String userPw,
            @Field("uuid") String uuid
            );
}

public class LoginResult { // 결과를 받을 모델
    private String result;
    private String message;

    public String getResult() {
        return result;
    }

    public String getMessage() {
        return message;
    }
}

public class Value extends AppCompatActivity {
    public static final String BASE_URL = "http://www.abc.com/mobile/";
    public static final String URL_LOGIN = "login.php";
}

 

예제는 결과를 받아서 RecyclerView 에 보여준다거나 후속 action 에 대한 건 기술하지 않았다.

이 예제는 그냥 복사해서 붙여넣기를 어떻게 하는지에 대한 설명이라고 보면 된다.

PHP 파일에 대한 사항은 기술하지 않는다. 이미 본 블로그 다른 예제를 보면 충분히 이해할 수 있으리라.

 

참고하면 도움되는 글

https://thdev.tech/androiddev/2016/11/13/Android-Retrofit-Intro/

 

https://link2me.tistory.com/1850 ArrayList 처리 예제

 

 

Update : 2020.8.3일

ㅇ Retrofit2 라이브러리를 여러번 호출하는 걸 만들어야 한다면 함수화를 하는 게 좋다.

 

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitAPI {
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        retrofit = new Retrofit.Builder()
                .baseUrl(Value.IPADDRESS)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build();

        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }
}

 

사용 예제

public interface RemoteService {// 서버에 호출할 메소드를 선언하는 인터페이스
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(RetrofitUrl.URL_Verson)
    Call<VersionResult> lastVersion(
            @Field("os") String os
    );

    @FormUrlEncoded
    @POST(RetrofitUrl.URL_GroupList)
    Call<GroupResult> getGroupList(
            @Field("keyword") String keyword
    );
}

class RetrofitUrl {
    companion object {
        const val URL_Verson = "lastVersion.php"
        const val URL_GroupList = "groupList.php"
    }
}

private void UpgradeChk() {
    mloginService = RetrofitAPI.getClient().create(RemoteService.class);
    mloginService.lastVersion("0").enqueue(new Callback<VersionResult>() {
        @Override
        public void onResponse(Call<VersionResult> call, Response<VersionResult> response) {
            VersionResult result = response.body();

            version = Value.VERSION; // 앱 버전
            version = version.replaceAll("[^0-9]", ""); // 버전에서 숫자만 추출
            Log.d("WEB", "Response: " + response);

            String Response = result.getVersion().replaceAll("[^0-9]", ""); // 버전에서 숫자만 추출
            System.out.println("Server Version : " + Response);

            if (Integer.parseInt(version) < Integer.parseInt(Response)) { // 서버 버전이 더 높으면
                UpgradeProcess();
                if(Build.VERSION.SDK_INT >= 26)  {
                    NonSecretApp_Setting(); // 출처를 알 수 없는 앱 설정 화면 띄우기
                } else {
                    startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
                }
            } else {
                AutoLoginProgress();
            }
        }

        @Override
        public void onFailure(Call<VersionResult> call, Throwable t) {

        }
    });
}

 

더 세부적인 것은 Github 에 올려진 소스코드를 참조하면 도움될 것이다.

https://github.com/jsk005/JavaProjects/tree/master/retrofitsample

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

블로그 이미지

Link2Me

,
728x90
안드로이드 10 에서는 TelephonyManager에서 개인을 특정할 수 있는 정보를 가져올수 없도록 변경되었다.

TelephonyManager().getDeviceId()
TelephonyManager().getImei()
TelephonyManager().getMeid()

그리고 하드웨어의 시리얼넘버도 사용이 불가능해졌다.
Builde.SERIAL


In android 10, couldn't get device id using permission "READ_PHONE_STATE".


getDeviceId() has been deprecated since API level 26.
You can use an instance id from firebase e.g FirebaseInstanceId.getInstance().getId();.
String deviceId = android.provider.Settings.Secure.getString(
                context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);


삼성갤럭시 S10(Android 10) 에서 테스트한 결과 코드를 적어둔다.

기존 안드로이드 폰은 IMEI 값이 15자리 숫자로만 되어 있었는데

아래 코드에서 제공하는 deviceID값은 IMEI 값이 아닌 다른 값을 제공하는 거 같다.

개발자모드에서 수집한 deviceID 와 release 모드로 앱을 만든 것과 값이 다르더라.

cc64a5b80853bc8d

36f7660af1dc22f2


public static String getDeviceId(Context context) {

    // 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
    String deviceId;

    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
    } else {
        final TelephonyManager mTelephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            //권한 없을 경우
            if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, Manifest.permission.READ_PHONE_STATE)) {
                // 사용자에게 해당 권한이 필요한 이유에 대해 설명
                Toast.makeText(context, "앱 실행을 위해서는 전화 관리 권한을 설정해야 합니다.", Toast.LENGTH_SHORT).show();
            }
            // 최초로 권한을 요청하는 경우
            ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_PHONE_STATE} , 2);
            return null;
        } else {
            Log.v("TAG","Permission is granted");
            if (mTelephony.getDeviceId() != null) {
                deviceId = mTelephony.getDeviceId();
            } else {
                deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            }
        }
    }
    return deviceId;
}

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case 2: {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(getApplicationContext(), "Permission granted", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(getApplicationContext(), "Permission denied", Toast.LENGTH_SHORT).show();
            }
            return;
        }
    }
}
 


만약 deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 가 제대로 동작하지 않는다면, 다른 대책을 세워야 할 거 같다.


if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        //deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        deviceId = getUniqueID(context);
}


public synchronized static String getUniqueID(Context context) {
    // 자바에서는 기본적으로 UUID를 생성하는 클래스를 지원한다.
    // UUID를 생성하면 간단하게 유니크한 ID를 얻을 수 있다.
    if (uniqueID == null) {
        SharedPreferences sharedPrefs = context.getSharedPreferences(PREF_UNIQUE_ID, Context.MODE_PRIVATE);
        uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null);
        if (uniqueID == null) {
            uniqueID = UUID.randomUUID().toString().replace("-", "");
            Log.e("UUID : ",uniqueID);
            SharedPreferences.Editor editor = sharedPrefs.edit();
            editor.putString(PREF_UNIQUE_ID, uniqueID);
            editor.commit();
        }
    }
    // 앱을 삭제하면 정보 삭제되므로 앱 삭제시에는 관리자에게 초기화 요청 필요
    return uniqueID;
}



Eclipse 테스트 일시 : 2020.3.19일

오래전에 Eclipse 개발툴로 개발한 걸 테스트 해봤다.

디바이스를 구분하는 고유 번호로  ANDROID_ID 로 대체하면 해결되더라.

릴리즈 버전과 디버깅 버전 APK 의 Android ID 가 다르지만 사용자에게는 문제는 없다.

//TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
String uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);


블로그 이미지

Link2Me

,
728x90

테이블 칼럼을 phpMyAdmin 을 이용하여 추가하다보니 명령어를 잘 모르겠다.

그래서 명령어를 추가해 둔다.


칼럼 추가

ALTER TABLE 테이블명
ADD 추가할 컬럼명  데이터 유형;


ALTER TABLE members ADD  isUpdate TINYINT( 2 ) NOT NULL ,

ADD isDelete TINYINT( 2 ) NOT NULL ,
ADD OStype TINYINT( 2 ) NOT NULL ,
ADD phoneBrand VARCHAR( 20 ) NULL ,
ADD phoneModel VARCHAR( 20 ) NULL ,
ADD level TINYINT( 2 ) NOT NULL DEFAULT '1',
ADD chosung VARCHAR( 10 ) NULL ,
ADD ismobileNO TINYINT( 2 ) NOT NULL DEFAULT '1'


칼럼 변경

ALTER TABLE members

CHANGE isDelete isDelete TINYINT( 2 ) NOT NULL DEFAULT '0'


칼럼 삭제

ALTER TABLE 테이블명
DROP 삭제할 컬럼명;

ALTER TABLE members
  DROP isUpdate,
  DROP isDelete;


특정 칼럼 다음에 필드 추가

ALTER TABLE members ADD isUpdate TINYINT( 2 ) NOT NULL DEFAULT '0' AFTER isGanbu ,
ADD isDelete TINYINT( 2 ) NOT NULL DEFAULT '0' AFTER isUpdate


칼럼 순서 변경

ALTER TABLE members modify column corpID VARCHAR( 12 ) AFTER idx;


'SQL' 카테고리의 다른 글

MySQL SUBTIME 예제 (30분전까지 데이터 불러오는 쿼리)  (0) 2021.03.24
MySQL DB 백업  (0) 2020.06.13
테이블명 변경  (0) 2019.03.11
MySQL 사용자 권한 부여  (0) 2018.11.26
MySQL 두 테이블 불일치 데이터 찾는 SQL  (0) 2018.08.18
블로그 이미지

Link2Me

,
728x90

오랫만에 PHP PDO 코드를 만들다보니 에러가 나와 어디를 수정해야 할지 헷갈린다.

PHP PDO로 구현한 코드에서 500 Error 메시지의 원인을 찾아내고 해결한 후 적어둔다.

 

Ajax 는 PHP B 파일에서 발생하는 에러를 파악하기 어려울 수 있다.

이럴경우에는 절분시험을 하듯이 B 파일에서만 별도로 데이터가 넘어왔다는 가정하에

아래 두줄 추가하고 alert(msg) 해서 메시지 내용을 살펴보고 코드의 문제점을 발견하고 찾아서 해결하면 된다.

<?php
error_reporting(E_ALL);
ini_set("display_errors", 1);

 

if (!isset($_POST['uid'])) {
    echo "-9";
    exit ;
}

 

에러를 무조건 출력시켜서 메시지를 분석해야 잘못된 부분을 찾아내기 쉽다.

 

 

 

 

 

 

 

코딩할 당시에는 알지만 오래되면 기억이 나지 않기 때문에 내가 나중에 보려고 정리하는 것이므로 댓글 태클은 정중히 사양.......

블로그 이미지

Link2Me

,
728x90

코드를 수정하다보니 SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data 이런 에러메시지가 나온다.


아무리 찾아도 문제가 없는데 말이다.

변수명을 변경했는데 캐싱된 데이터가 계속 잘못된 정보를 서버로 보내면서 벌어진 현상이다.


크롬 브라우저에서 Disable cache 를 하지 않아서 생긴 현상이다. 윽ㅠㅠㅠㅠㅠㅠ

블로그 이미지

Link2Me

,
728x90

PHP에서 QR Code를 생성하기 위해서는 php.ini 환경설정 값을 먼저 확인한다.



PHP QRCode 를 다운로드 받는 곳

https://sourceforge.net/projects/phpqrcode/files/releases/ 에서 받는다.

2010년에 구현된 파일로 Class 로 되어 있고 잘 동작된다.


파일을 폴더 그대로 압축을 푼다.


실행 예제 코드

<?php
// 압축을 해제한 phpqrcode 폴더의 qrlib.php 파일을 include한다.
include_once "./phpqrcode/qrlib.php";
$qrContents = "00아파트 122동 703호";
$filePath = sha1($qrContents).".png";

if(!file_exists($filePath)) {
    $ecc = 'H'; // L, M, Q, H, This parameter specifies the error correction capability of QR.
    $pixel_Size = 10; // This specifies the pixel size of QR.
    $frame_Size = 10; // level 1-10
    QRcode::png($qrContents, $filePath, $ecc, $pixel_Size, $frame_Size);
    echo "파일이 정상적으로 생성되었습니다.";
    echo "<hr/>";
} else {
    echo "파일이 이미 생성되어 있습니다.\n파일을 지우고 다시 실행해 보세요.";
    echo "<hr/>";
}

echo "저장된 파일명 : ".$filePath;
echo "<hr/>";
echo "<center><img src='".$filePath."'/></center>"
?>


실행결과



다음은 안드로이드 앱에서 QR Code를 구현하여 카메라로 위 이미지를 인식하면 $qrContents = "00아파트 122동 703호"; 인 00아파트 122동 703호 를 결과로 보여주는지 확인하면 된다.

보통은 https://abc.com/index.php 처럼 상품 설명이나 사이트 이동으로 연결하여 만든다고 보면 된다.



'안드로이드 > Android QRCode' 카테고리의 다른 글

Android QRCode 샘플  (0) 2020.08.25
PHP TCPDF 설치 및 샘플 코드  (0) 2020.05.30
QRCode 개요  (0) 2020.03.03
블로그 이미지

Link2Me

,
728x90

QR Code는 흑백 격자 무늬 패턴으로 정보를 나타내는 매트릭스 형식의 이차원 바코드이다.


QR코드는 1994년 일본 도요타 자동차 자회사인 덴소 웨이브가 도요타 자동차 전용 부품을 구별하기 위해 개발하면서 시작됐다. 기존 바코드 방식이 1차원적인 가로 선만으로는 담을 수 있는 정보의 양이 제한되기 때문에, 일정 면적에 정보를 담을 수 있는 2차원 코드를 개발한 것이다.

기존의 바코드가 20자 내외의 숫자 정보만 저장할 수 있지만, QR코드는 숫자 최대 7,089자, 문자 최대 4,296자를 저장할 수 있다.


QR코드는 정사각형 안 흑백 격자선 위로 다양한 무늬가 입혀진 것이다. QR코드는 크게 3가지 패턴으로 구성된다. 위치 찾기 패턴과 얼라인먼트 패턴, 셀 패턴 이다. 위치 찾기 패턴은 모든 QR코드의 세 모서리에 크게 자리 잡고 있는 사각형이다. 해당 기능은 QR코드를 인식기가 360도 어느 방향에서 감지하더라도 QR코드의 위치를 정확하게 파악, 빠른 정보 탐색이 가능하도록 만드는 일종의 나침반 기능을 한다. 이를 통해 데이터 인식 및 분석 속도가 빨라져 QR코드란 이름도 ‘빠른 응답’(Quick Response)에서 나왔다.


QR코드가 널리 쓰이면서 새로운 문제도 등장하고 있다. QR코드는 바코드에 비해 많은 정보를 담을 수 있어 QR코드에 악성코드나 유해 웹사이트 주소를 담아 유포하는 사례가 많아지고 있다.
검증된 기관이나 기업이 아닌 곳에서 제공하는 QR코드의 경우 접속할 때 신중해야 한다.


스마트폰의 사진촬영 기능으로 QR코드를 찍으면 QR코드에 담긴 정보에 따라 다양한 기능을 할 수 있다.

가장 많이 쓰는 기능은 특정 홈페이지로 보내는 것이다. 가령 제품 상자에 인쇄된 QR코드를 찍는 순간 제품을 자세히 소개하는 홈페이지를 보여주는 식이다. 인쇄매체를 가진 언론사는 기사 옆에 QR코드를 찍어 관련된 동영상을 쉽게 보여줄 수 있다.


Web상에서 QR Code를 생성해주는 사이트(https://ko.qr-code-generator.com/)가 있다.

QR Code를 생성하면 생성된 QR Code 이미지를 다운로드할 수 있고, Android QRCode Sample 코드로 입력한 텍스트가 출력되는지 Scan으로 확인해 볼 수 있다.


QR Code 판독원리  

1. 카메라에서 QR코드 영상 입력  

2. 카메라 영상을 흑백(bitmap)으로 변환.  

3. 흑백영상에서 QR코드 찾기  

4. 찾은 QR코드의 판독  

5. 판독데이터를 스마트폰 화면에 출력


다량의 QR Code를 한꺼번에 등록하는 기능을 하려면 별도로 PHP 라이브러리를 사용하는 것이 좋다.



'안드로이드 > Android QRCode' 카테고리의 다른 글

Android QRCode 샘플  (0) 2020.08.25
PHP TCPDF 설치 및 샘플 코드  (0) 2020.05.30
PHP QR code 생성하는 방법  (0) 2020.03.04
블로그 이미지

Link2Me

,
728x90

php.ini 파일을 수정하고 나서 service httpd restart 를 하니까 httpd 데몬이 살아나지 않는다.


Starting httpd: httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName


라는 에러 메시지가 나온다.


vi /usr/local/apache/conf/httpd.conf
ServerName localhost

yum 설치가 아닌 소스 설치라서 httpd 경로명이 다르다.

vi 에서 /ServerName 입력하면 해당 라인으로 바로 이동된다.


httpd.conf 에 분명히 ServerName localhost 라고 적어주고 재시작을 했음에도 에러 메시지가 출력된다.


hosts 파일을 한번도 수정해 준 적어 없어서 생기는 에러였다.


# vi /etc/hosts
127.0.0.1   localhost.localdomain localhost
로 수정하고 저장한 다음 재시작을 눌러주니 정상적으로 OK가 출력된다. 또는

127.0.0.1   localhost

로 수정해도 된다.



만약 httpd 데몬이 죽지 않는다는 메시지가 나온다면

# killall -9 httpd

# netstat -anp | grep 80

로 확인해보라.

필요시

# kill -9 pid번호

를 하면 해당 데몬이 죽을 것이다.


블로그 이미지

Link2Me

,
728x90

회원가입, 로그인, 게시판 글 등록시 스팸방지를 위해서 캡차코드를 적용할 수 있다.


https://www.phpcaptcha.org/ 에서 파일을 다운로드 하여 홈페이지에 업로드한다.


샘플코드를 간단하게 줄여서 테스트하고 적어둔다.


Form 입력코드

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
  <title>Securimage Example Form</title>
  <link rel="stylesheet" href="securimage.css" media="screen">
  <style type="text/css">
  <!--
  div.error { display: block; color: #f00; font-weight: bold; font-size: 1.2em; }
  span.error { display: block; color: #f00; font-style: italic; }
  .success { color: #00f; font-weight: bold; font-size: 1.2em; }
  form label { display: block; font-weight: bold; }
  fieldset { width: 90%; }
  legend { font-size: 24px; }
  .note { font-size: 18px;
  -->
  </style>
</head>
<body>

<fieldset>
<legend>Example Form</legend>
<form method="post" action="capchado.php" id="contact_form">
  <input type="hidden" name="do" value="contact">
  <p>
    <label for="ct_name">Name*:</label>
    <input type="text" id="ct_name" name="ct_name" size="35" value="">
  </p>

  <div>
    <?php
      require_once 'securimage.php';
      $options = array();
      $options['input_name'] = 'ct_captcha'; // change name of input element for form post
      $options['disable_flash_fallback'] = false; // allow flash fallback
      echo Securimage::getCaptchaHtml($options);
    ?>
  </div>

  <p>
    <br>
    <input type="submit" value="전송">
  </p>

</form>
</fieldset>

</body>
</html>


Form 에서 전송받은 데이터를 처리하는 코드

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

if ($_SERVER['REQUEST_METHOD'] == 'POST' && @$_POST['do'] == 'contact') {
    var_dump($_POST); // 디버깅 목적으로 찍어보라.

    $captcha = @$_POST['ct_captcha'];
    require_once dirname(__FILE__) . '/securimage.php';
    $securimage = new Securimage();

    if ($securimage->check($captcha) == false) {
        $captcha_error = '잘못된 보안코드가 입력되었습니다.';
        echo '<br/><br/>'.$captcha_error.'<br/>';
    } else {
      echo '<br/><br/>캡차 제대로 입력했어요<br/>';
    }

} // POST
?>


이 코드를 jQuery 로 처리하기 위해서는 소스보기로 해서 캡차의 input name 값이 뭔지 확인을 해보고 jQuery 입력체크를 하면 된다.

블로그 이미지

Link2Me

,