728x90

Android Text 를 Speech 로 변환하는 예제다.


<?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=".TextToSpeechActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/layout_appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/layout_textvoice"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="30dp"
        android:layout_marginEnd="24dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_appbar">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/et_text2voice"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="텍스트를 입력하세요"
            android:padding="10dp"></com.google.android.material.textfield.TextInputEditText>

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

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_speech"
        style="@style/Widget.AppCompat.Button.Borderless.Colored"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="30dp"
        android:layout_marginEnd="24dp"
        android:padding="10dp"
        android:text="음성변환"
        android:textColor="@color/colorOrangeDark"
        android:textSize="16sp"
        android:textStyle="bold"
        app:backgroundTint="@color/colorSkyBlue"
        app:cornerRadius="15dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_textvoice" />

</androidx.constraintlayout.widget.ConstraintLayout>
 



import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import java.util.Locale;

public class TextToSpeechActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;

    private TextToSpeech textToSpeech;
    private EditText speakText;
    private Button speakBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_speech);
        mContext = TextToSpeechActivity.this;

        initView();
    }

    private void initView() {
        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.setTitle("TextToSpeech");
        setSupportActionBar(toolbar);

        speakText = findViewById(R.id.et_text2voice);
        speakBtn = findViewById(R.id.btn_speech);

        textToSpeech = new TextToSpeech(getApplicationContext(), status -> {
            if (status != TextToSpeech.ERROR) {
                textToSpeech.setLanguage(Locale.KOREAN);
            }
        });

        speakBtn.setOnClickListener(v -> texttoSpeak());
    }

    private void texttoSpeak() {
        String text = speakText.getText().toString();
        if ("".equals(text)) {
            text = "Please enter some text to speak.";
            Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();
            return;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
        } else {
            String utteranceId = this.hashCode() + "";
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId);
        }
    }

    @Override
    protected void onDestroy() {
        if (textToSpeech != null) {
            textToSpeech.stop();
            textToSpeech.shutdown();
        }
        super.onDestroy();
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        startActivity(new Intent(mContext,SplashActivity.class));
        finish();
    }
}


예제 전체 소스코드는 https://github.com/jsk005/JavaProjects/tree/master/speechtext 에 있다.

블로그 이미지

Link2Me

,
728x90

SSH 포트 변경을 했더니 잘 안된다.

아래와 같이 하면 100% 될 것이다.


# 방화벽 확인
firewall-cmd --list-all

# 방화벽 포트 추가
firewall-cmd --permanent --zone=public --add-port=1922/tcp
firewall-cmd --permanent --zone=public --add-service=ssh

# SELINUX는 ssh에 포트 22 만 허용하므로 아래 한줄을 실행한다.
yum -y install policycoreutils-python
semanage port -a -t ssh_port_t -p tcp 1922

# SSH 포트 변경
vi /etc//ssh/sshd_config
Port 1922

# 방화벽 포트 제거
firewall-cmd --permanent --remove-port=1922/tcp

# 방화벽 재실행
firewall-cmd --reload

# Restart SSHD service
systemctl restart sshd.service

# 포트 확인
ss -tnlp|grep ssh


# 에러 로그 확인
cat /var/log/secure

블로그 이미지

Link2Me

,
728x90

Retrofit2 라이브러리를 이용하여 파일을 업로드하는 예제 예시다.

파일 업로드 외에 POST 변수 idx를 추가해서 보낼 때,

RequestBody descBody = RequestBody.create(MediaType.parse("text/plain"), idx);

가 추가하면 된다.


Retrofit2 통신에 대한 기본 이해는 https://link2me.tistory.com/1806 에 이미지 도식도를 참조하시라.



앱 build.gradle

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    implementation 'com.google.android.material:material:1.2.1'

    implementation 'com.squareup.okhttp3:okhttp:4.4.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'
} 


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


public interface DataCommAPI {

    @Multipart
    @POST(RetrofitURL.URL_ImageUpload)
    Call<UploadResult> uploadFile(@Part("idx") RequestBody idx,
                                  @Part MultipartBody.Part uploaded_file);
}

public class UploadResult {
    private String result;

    public String getResult() {
        return result;
    }

}

public class RetrofitURL {
    public static final String IPADDRESS = "http://www.abc.com/androidSample/upload/";

    public static final String URL_ImageUpload = "upload.php";
}

public class APIRequest {
    static Retrofit retrofit = null;

    public static Retrofit getClient() {
        if (retrofit==null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(RetrofitURL.IPADDRESS)
                    .client(createOkHttpClient())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }

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

private void uploadImage(String sourceImageFile, String idx){
     DataCommAPI retrofitInterface = APIRequest.getClient().create(DataCommAPI.class);

     File file = new File(sourceImageFile);
     RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
     MultipartBody.Part body = MultipartBody.Part.createFormData("uploaded_file", file.getName(), requestFile);
     RequestBody descBody = RequestBody.create(MediaType.parse("text/plain"), idx);

     Call<UploadResult> call = retrofitInterface.uploadFile(descBody,body);
     call.enqueue(new Callback<UploadResult>() {
         @Override
         public void onResponse(Call<UploadResult> call, Response<UploadResult> response) {
             if (response.isSuccessful()) {
                 UploadResult responseBody = response.body();
                 if(responseBody.getResult().contains("success")){
                     Toast.makeText(getApplicationContext(), "파일 업로드 성공", Toast.LENGTH_LONG).show();
                 } else {
                     Utils.showAlert(mContext,"파일 업로드","파일 업로드 실패");
                 }

             } else {
                 ResponseBody errorBody = response.errorBody();
                 Gson gson = new Gson();
                 try {
                     Response errorResponse = gson.fromJson(errorBody.string(), Response.class);
                    
Toast.makeText(getApplicationContext(), errorResponse.message(),

                        Toast.LENGTH_LONG).show();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }

         @Override
         public void onFailure(Call<UploadResult> call, Throwable t) {
             Log.e(TAG, "onFailure: "+t.getLocalizedMessage());
         }
     });

 }




블로그 이미지

Link2Me

,
728x90

내 마음대로 지우고 나서 재설치를 하려니까 제대로 설치가 안된다.

그래서 조금이나마 개념 이해하려고 적어둔다.

 

 

 

설치하기 전 상태 확인을 위해서

cd /etc/yum.repos.d

 

 

 

#Step 1 – Prerequsitis
yum -y install epel-release

 

 

#Step 2 Repository 설치
yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm

 

 

#Step 3 yum Utilities Package 설치
yum -y install yum-utils

 

#Step 4 – Disable repo for PHP 5.4
yum-config-manager --disable remi-php54
yum-config-manager --enable remi-php73

 

#Step 5 – Install PHP and PHP-FPM
yum -y install --enablerepo=remi-php73 httpd mod_ssl php php-zip php-fpm php-common php-opcache php-curl php-devel php-gd
yum -y install --enablerepo=remi-php73 php-imap php-ldap php-mysql php-mysqlnd php-pdo php-odbc php-pear php-xml php-xmlrpc php-pecl-apc   
yum -y install --enablerepo=remi-php73 php-mbstring php-mcrypt php-soap php-tidy curl curl-devel php-libwww-perl ImageMagick libxml2 libmxl2-devel    
yum -y install --enablerepo=remi-php73 mod_fcgid php-cli httpd-devel php-intl php-imagick php-pspell wget php-openssl    
yum -y install --enablerepo=remi-php73 unzip git mc

 

# PHP 설치버전 확인
php -v

 

# 설치된 PHP Package 확인
rpm -qa | grep php

 

#Step 6 PHP-FPM 설정
# PHP-FPM 설정을 위하여 2개의 디렉토리 생성
mkdir /etc/php-fpm.d/sites-enabled
mkdir /etc/php-fpm.d/sites-available

#Step 7
PHP-FPM 설정 파일 아래와 같이 수정
vi /etc/php-fpm.conf
;include=/etc/php-fpm.d/*.conf
include=/etc/php-fpm.d/sites-enabled/*.conf

#Step 8
PHP-FPM 기본 웹 설정 파일 이동
mv /etc/php-fpm.d/www.conf /etc/php-fpm.d/sites-available

#Step 9
sites-enabled 폴더에 www.conf 링크 생성
ln -s /etc/php-fpm.d/sites-available/www.conf /etc/php-fpm.d/sites-enabled/www.conf

#Step 10
PHP-FPM 소켓 통신을 위한 폴더 생성
mkdir /var/run/php-fpm

#Step 11
www.conf 설정파일을 아래와 같이 수정
vi /etc/php-fpm.d/sites-available/www.conf
;listen = 127.0.0.1:9000
listen = /var/run/php-fpm/default.sock
listen.owner = apache
listen.group = apache
listen.mode = 0660
php_value[opcache.file_cache] = /var/lib/php/opcache  ; 주석만 제거

#Step 12 – Apache VirtualHost 구성
vi /etc/httpd/conf.d/default.conf
# 아래 붙여넣기 한다.
<VirtualHost _default_:80>
       DocumentRoot "/var/www/html"

       ErrorLog logs/default-error_log
       LogLevel warn
       TransferLog logs/default-access_log
       CustomLog logs/default-request_log "%t %h %x %x \"%r\" %b"

       SuexecUserGroup apache apache

       <Proxy "unix:/var/run/php-fpm/default.sock|fcgi://php-fpm">
               ProxySet disablereuse=off
        </Proxy>

       <FilesMatch \.php$>
               SetHandler proxy:fcgi://php-fpm
       </FilesMatch>
</VirtualHost>

 

 

#Step 13 Apache SSL 설정 파일(ssl.conf) 수정
vi /etc/httpd/conf.d/ssl.conf
# shift + g 눌러서 파일의 맨 아래로 이동한다.
# 설정파일의 맨 끝 </VirtualHost> 앞에 아래 내용 추가
       SuexecUserGroup apache apache

       <Proxy "unix:/var/run/php-fpm/default.sock|fcgi://php-fpm">
               ProxySet disablereuse=off
        </Proxy>

       <FilesMatch \.php$>
               SetHandler proxy:fcgi://php-fpm
       </FilesMatch>

 

 

#Step 14 보안설정
sudo chmod 640 /etc/httpd/conf/httpd.conf
sudo chown root:root /etc/httpd/conf/httpd.conf
sudo chmod 640 /etc/php.ini
sudo chown root:root /etc/php.ini

sudo chown -R apache:apache /var/www/html
sudo chmod -R 755 /var/www/html

 

#Step 15 httpd.conf 파일 수정
vi /etc/httpd/conf/httpd.conf
ServerName localhost
<IfModule dir_module>
    DirectoryIndex index.html index.php
</IfModule>

<Directory "/var/www/html">
    Options +FollowSymLinks -Indexes
    AllowOverride All
    Require all granted
    <LimitExcept GET POST>
      Order deny,allow
      Deny from all
    </LimitExcept>
</Directory>

#Step 16
Apache HTTPOXY 취약점 방지
echo "RequestHeader unset Proxy early" >> /etc/httpd/conf/httpd.conf

 

# Step 17 Configure PHP-FPM
vi /etc/php.ini
cgi.fix_pathinfo=0
short_open_tag = On
post_max_size = 60M  ; // 8M 으로 되어 있었음.
memory_limit = 128M
upload_max_filesize = 50M ; // 기본 2M 으로 되어 있었음
allow_url_fopen = On
;PHP Excel Libray 설치가 안되는 현상이 있어서 On으로 변경함.
date.timezone ="Asia/Seoul"

 

;PHP 버전정보 노출 방지
expose_php=Off


;PHP에서 세션은 일단 생성 된 뒤에, 가비지 콜렉터 관리로직에 의해 소멸된다.
session.gc_probability = 1
session.gc_divisor = 1
;이값이 100이면 1/100 즉 1%의 확률로 가비지콜렉션이 실행된다.
;1이면 정확하게 유효기간이 넘은 데이타가 삭제될 것이다.
session.gc_maxlifetime = 3600

 

 

만약 session 폴더를 다르게 지정했다면....

session.save_path = /tmp/phpsession

mkdir /tmp/phpsession
chown apache.apache /tmp/phpsession
chmod 750 /tmp/phpsession

 

#Step 18 Verify PHP-FPM version
/usr/sbin/php-fpm -version

 

 

#Step 19 시스템 부팅시 자동실행 설정
systemctl enable php-fpm
systemctl enable httpd

#Step 20
설치된 PHP-FPM 실행/종료
systemctl start php-fpm
systemctl start httpd

systemctl stop php-fpm
systemctl stop httpd

systemctl restart php-fpm
systemctl restart httpd

systemctl status php-fpm
systemctl status httpd

 

 

############################
##### CentOS7 방화벽 설정 #####
############################
# 방화벽 설치
yum -y install firewalld

# 기본 설정은 /usr/lib/firewalld/ 에 위치
# 방화벽 데몬 시작
systemctl start firewalld

# 서버 부팅 시 firewalld 데몬 자동 시작 설정
systemctl enable firewalld


# 방화벽 상태(실행여부) 확인
systemctl status firewalld
firewall-cmd --state

# 방화벽 확인
firewall-cmd --list-all

# 서비스 추가
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=mysql

# 서비스 제거
firewall-cmd --permanent --remove-service=http
firewall-cmd --permanent --remove-service=http

# 포트 등록
firewall-cmd --permanent --add-port=3306/tcp
firewall-cmd --permanent --add-port=22/tcp
firewall-cmd --permanent --add-port=2222/tcp

# 포트 삭제
firewall-cmd --permanent --remove-port=9000/tcp
firewall-cmd --permanent --remove-port=8080/tcp

# 방화벽 적용 및 확인
firewall-cmd --reload
firewall-cmd --list-all

 

#### PHP Excel Library 설치를 위한 Composer 설치 #####

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
composer -V
export COMPOSER_ALLOW_SUPERUSER=1
composer -V
echo "export COMPOSER_ALLOW_SUPERUSER=1" >> ~/.bashrc
cat ~/.bashrc | grep export

cd /var/www/html/
composer require phpoffice/phpspreadsheet

 

wget https://sourceforge.net/projects/tcpdf/files/tcpdf_6_3_2.zip

 

###### TCPDF 설치 ############
cd /usr/share/php
wget https://sourceforge.net/projects/tcpdf/files/tcpdf_6_3_2.zip
unzip tcpdf_6_3_2.zip

# 나눔폰트 설치 방법
cd /usr/share/fonts/
wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip

unzip NanumFont_TTF_ALL.zip -d NanumFont
cd /usr/share/php/tcpdf/tools/
ls /usr/share/fonts/NanumFont/*
ls /usr/share/fonts/NanumFont/* | xargs -I TTF php tcpdf_addfont.php -i TTF
ll /usr/share/php/tcpdf/fonts/ | grep nanum

블로그 이미지

Link2Me

,
728x90

Cutstom Dialog 기능을 이용하여 SMS authentication with One Time Password 처리를 위한 Layout 구성 예제이다.


custom_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:id="@+id/parent_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:id="@+id/parent_card_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent">

        <RelativeLayout
            android:id="@+id/inner_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/transparent">

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/dialog_title_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/default_title_text"
                android:textSize="20sp"
                android:textStyle="bold"
                android:textColor="@android:color/black"
                android:textAlignment="center"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="10dp"
                android:paddingLeft="10dp"
                android:paddingRight="10dp"
                android:paddingTop="5dp"
                android:paddingBottom="2dp"/>

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/dialog_explain_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/dialog_title_text"
                android:text="@string/default_explain_text"
                android:textSize="14sp"
                android:textColor="@android:color/black"
                android:textAlignment="center"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="10dp"
                android:paddingLeft="10dp"
                android:paddingRight="10dp"
                android:paddingTop="2dp"
                android:paddingBottom="5dp"/>

            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/et_auth_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/dialog_explain_text"
                android:layout_marginTop="10dp"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp" >

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_auth_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@color/colorWhite"
                    android:textSize="16sp"
                    android:hint="@string/prompt_auto"
                    android:inputType="numberDecimal"
                    android:imeOptions="actionDone"
                    android:selectAllOnFocus="true">

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

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

            <LinearLayout
                android:id="@+id/btn_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="20dp"
                android:layout_below="@id/et_auth_layout"
                android:gravity="top"
                android:orientation="horizontal">

                <com.google.android.material.button.MaterialButton
                    android:id="@+id/btn_disagree"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_marginRight="10dp"
                    android:layout_weight="1"
                    android:backgroundTint="@color/colorOrangeDark"
                    app:cornerRadius="10dp"
                    android:text="취소" />

                <com.google.android.material.button.MaterialButton
                    android:id="@+id/btn_agree"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="10dp"
                    android:layout_weight="1"
                    android:backgroundTint="@color/colorBlue"
                    app:cornerRadius="10dp"
                    android:text="전송" />

            </LinearLayout>
        </RelativeLayout>
    </androidx.cardview.widget.CardView>
</RelativeLayout>
 


예제의 코드는 https://github.com/jsk005/JavaProjects/tree/master/Interfaces/src/main/java/com/link2me/android/interfaces 에 올려져 있다.

Custom Dialog 를 Interface로 처리하는 코드 예제일 뿐이지 SMS OTP 인증 전체 코드는 아니다.


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

LinearLayout 예제  (0) 2020.10.13
LinearLayout 동적 생성  (0) 2020.10.01
EditText DatePicker  (0) 2020.08.18
Floating Action Button  (0) 2020.07.11
CardView Layout 예제  (0) 2020.04.12
블로그 이미지

Link2Me

,
728x90

SMS OTP 인증 구현을 위한 첫번째 단계 설계 개념이다.

ID/PW 인증 방식에 SMS OTP 인증을 추가하는 개념이다.


1. ID/PW 인증
   - 로그인한 정보에 오늘 날짜 SMS 인증 기록 유무를 가져온다.
   - 오늘날짜 인증기록이 없으면 SMS 인증으로 분기한다.
   - SMS 인증 기록이 있으면, PIN 번호 인증을 한다.
2. SMS OTP 인증 (1일 1회 인증 원칙)
   - SMS 인증 API를 통해 휴대폰으로 인증번호 발송
   - 인증번호 6자리를 입력하고 검증(verify) API 로 전송
   - 검증(verify) API 로부터 success return을 받으면
   - 서버에 사용자 인증 시각 정보를 기록한다.
   - 전일 인증 기록이 있을 경우 당일 인증 요청이 없는 것으로 간주하고 사용자 인증 정보를 update 한다.
   - error를 받으면 에러 상황에 맞는 것을 Popup으로 보여주고 로그인 종료처리한다.

위 로직을 기반으로 PHPClass 함수를 정의하고 코드 구현 예정이다.


Android : 서버 인증 결과를 JSON 으로 받아서 화면에서 인증 처리

  - Custom Dialog 방식 또는 New Activity 방식

PHP : Ajax를 이용한 인증 처리 로직 수정 보완

블로그 이미지

Link2Me

,
728x90

맨날 Debugging을 Log 찍어가면서 해왔는데, 우연히 검색해보다 페이스북에서 내놓은 Stetho 라이브러리를 이용하면 편하게 디버깅할 수 있다는 걸 알아서 사용해보고 간략하게 적어둔다.

 

https://github.com/facebook/stetho 에 가면 최신버전을 알 수 있다.

 

앱 build.gradle 추가

 

 

AndroidManifest.xml 추가

를 추가한다.

 

 

MyApplication Class 추가

public class MyApplication extends Application {
  public void onCreate() {
    super.onCreate();
    Stetho.initializeWithDefaults(this);
  }
} 

 

 

여기까지 하고 나서 앱을 컴파일 하면 연결된 폰의 정보를 크롬브라우저에서 확인할 수 있다.

 

크롬 주소창에 chrome://inspect 라고 입력한다.

아래와 같이 연결된 삼성폰 정보가 보이고 앱 명칭이 보인다.

 

 

2번을 누르면 새로운 창이 뜬다.

 

SharedPreference 정보를 살펴보니 저장된 정보가 보인다.

테스트 목적으로 만드는 템플릿 형식의 앱 개발이라 정보는 보여줘도 무방하기에 그대로 보이도록 했다.

PIN 인증번호 6자리 입력한 정보가 그대로 보인다.

 

여기서 직접 정보를 수정할 수도 있다.

 

PIN 번호도 임의변경하고 기존 PIN번호로 인증 시도했더니 안된다. 변경 PIN 번호 입력해야 가능하다.

이름과 서버에서 가져오는 이미지 저장 idx 값을 임의로 1에서 2로 변경해보고 이름도 홍길동으로 변경해봤다.

 

 

SQLite도 adb shell로 들어가서 확인하거나, db 파일 추출해서 pc에서 확인할 필요없이,
SQL을 직접 날려가며 확인할 수 있다고 하는데 SQLite DB 코드 만들면 테스트 해봐야겠다.

 

Network 연결방식은 Volley 라이브러리를 사용해서 그런지 정보가 안보인다.

Retrofit2 라이브러리를 사용한 코드로 테스트 해보면 제대로 보이려나?

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

ADB(Android Debug Bridge)  (0) 2021.01.07
OpenJDK 설치  (0) 2020.11.08
android SDK 설치 위치 변경  (0) 2020.07.06
[코틀린] Anko 라이브러리 추가 방법  (0) 2020.04.20
자바와 코틀린 함께 사용하기  (0) 2020.03.22
블로그 이미지

Link2Me

,
728x90

Android 에서 로그인을 하면 서버에서 PHP가 받아서 처리하는 로그인 예제이다.

결과를 json 으로 받아서 Android에서 저장해서 처리한다.


<?php
// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){
    @extract($_POST); // $_POST['loginID'] 라고 쓰지 않고, $loginID 라고 써도 인식되게 함
    if(isset($userID) && !empty($userID) && isset($password) && !empty($password)) {
        require_once 'phpclass/dbconnect.php';
        require_once 'phpclass/loginClass.php';
        $c = new LoginClass();

        header("Cache-Control: no-cache, must-revalidate");
        header("Content-type: application/json; charset=UTF-8");

        $rs = $c->LoginUserChk($userID,$password,$uID);
        if($rs > 0){
            $user = $c->getUser($userID, $password);
            if ($user != false) {
                $_SESSION['userID'] = $user['userID'];
                $_SESSION['userNM'] = $user['userNM'];
                $_SESSION['admin'] = $user['admin'];

                $row = array("userNM"=>$user['userNM'],"mobileNO"=>$user['mobileNO'],"profileImg"=>$user['idx']);

                $status = "success";
                $message = "";
                $userinfo = $row;
            } else {
                $status = "로그인 에러";
                $message = "다시 한번 시도하시기 바랍니다.";
                $userinfo = null;
            }

        } else if($rs === -1){
            $status = "단말 불일치";
            $message = '등록 단말 정보가 일치하지 않습니다. 관리자에게 문의하시기 바랍니다.';
            $userinfo = null;
        } else {
            $status = "로그인 에러";
            $message = '로그인 정보가 일치하지 않습니다';
            $userinfo = null;
        }
        $result = array(
            'status' => $status,
            'message' => $message,
            'userinfo' => $userinfo
        );
        echo json_encode($result);
    }
} else { // 비정상적인 접속인 경우
    echo 0; // loginChk.php 파일을 직접 실행할 경우에는 화면에 0을 찍어준다.
    exit;
}
?>


본 코드의 안드로이드 처리 부분 (Volley 라이브러리 활용)

https://github.com/jsk005/JavaProjects/blob/master/volleyloginsample/src/main/java/com/link2me/android/loginsample/LoginActivity.java 부분을 살펴보면 이해가 될 것이다.


Retrofit2 활용 예제는 GitHub에는 올리지 않았다.

@Parcelize
data class LoginResult (
    val status: String = "",
    val message: String = "",
    val userinfo: UserInfo? = null
): Parcelable

@Parcelize
data class UserInfo (
        val userNM: String = "",
        val mobileNO: String = "",
        val profileImg: String = ""
): Parcelable

void AutoLoginProgress() {
    userID = PrefsHelper.read("userid", "");
    userPW = PrefsHelper.read("userpw", "");

    if (userID != null && !userID.isEmpty() && userPW != null && !userPW.isEmpty()) {
        String uID = Utils.getDeviceId(mContext); // 스마트폰 고유장치번호
        String mfoneNO = Utils.getPhoneNumber(mContext); // 스마트폰 전화번호
        String AppVersion = String.valueOf(Utils.getVersionCode(mContext));

        mloginService.Login(userID,userPW,uID,mfoneNO,AppVersion)
                .enqueue(new Callback<LoginResult>() {
                    @Override
                    public void onResponse(Call<LoginResult> call, retrofit2.Response<LoginResult> response) {
                        LoginResult result = response.body();
                        if(result.getStatus().contains("success")){
                            PrefsHelper.write("userNM",result.getUserinfo().getUserNM());
                            PrefsHelper.write("mobileNO",result.getUserinfo().getMobileNO());
                            PrefsHelper.write("profileImg",result.getUserinfo().getProfileImg());
                        } else {
                            if(result.getStatus().contains("로그인 에러")){
                                startActivity(new Intent(SplashActivity.this, LoginActivity.class));
                                finish();
                            } else {
                                Utils.showAlert(mContext, result.getStatus(), result.getMessage());
                            }
                        }
                    }

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

                    }
                });
    } else {
        startActivity(new Intent(getApplication(), LoginActivity.class));
        finish();
    }
}


블로그 이미지

Link2Me

,
728x90

Intent 를 이용한 메일 발송 함수 이다.

구글 메일에서도 잘 보내지고, 기본 메일에서도 잘 보내진다.


테스트 환경 : 삼성 갤럭스 노트9


private void sendEmail_default(String emailTo, String subject, String message){
    Intent emailSelectorIntent = new Intent( Intent.ACTION_SENDTO );
    emailSelectorIntent.setData( Uri.parse( "mailto:" ) );

    final Intent emailIntent = new Intent( Intent.ACTION_SEND );
    emailIntent.putExtra( Intent.EXTRA_EMAIL, new String[]{ emailTo } );
    emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
    emailIntent.putExtra(Intent.EXTRA_TEXT, message);
    emailIntent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION );
    emailIntent.addFlags( Intent.FLAG_GRANT_WRITE_URI_PERMISSION );
    emailIntent.setSelector( emailSelectorIntent );

    startActivity( emailIntent );


}


블로그 이미지

Link2Me

,
728x90

원인을 정확하게 모르겠는데 MariaDB 테이블이 깨진 증상으로 인해서 평소 백업 스크립트를 잘 만들어둬야겠다는 생각이 들었다.


### DB 콜드 백업
# which mysql 을 하면 mariadb가 설치된 경로를 반환한다.
cd /var/lib
tar cvf mysqldb_20200902.tar ./mysql

# 콜드 백업 복구
rm -rf mysql/
tar xvf mysqldb_20200902.tar
systemctl restart mariadb

# Mysql DB 가 깨졌을때 복구 방법
mysql -uroot -p
check table 테이블명;
analyze table 테이블명;
repair table 테이블명;
check table 테이블명;


# 위 방법으로 해결이 안될 경우
각 테이블 하나에 3개의 파일이 생성되어 해당 테이블의 데이터가 실제로 저장
*.MYD : 실제 데이터가 저장되는 데이터파일
*.MYI : 테이블의 인덱스 정보를 저장하는 인덱스 파일
*.frm : 테이블의 구조가 저장되는 스키마파일

systemctl stop mariadb
또는
ps -ef | grep mysqld
kill -9 프로세스번호

cd /var/lib/mysql/
# 테이블 파일이 있는 디렉토리까지 이동하면 *.MYI 된 파일이 보인다.
# 이상유무 점검 방법
myisamchk -s 테이블명.MYI
myisamchk -v 테이블명.MYI
myisamchk -i 테이블명.MYI

# 테이블 상태 점검
myisamchk -ev 테이블명.MYI
# 안전모드로 복구
myisamchk -o 테이블명.MYI
# 테이블 파일 복구
myisamchk -r 테이블명.MYI



블로그 이미지

Link2Me

,
728x90

회원 로그인 후 회원 정보를 가져와서 ProfileView 함수에 처리하는 로직이다.


private void ProfileView() {
    ImageView img_profile = findViewById(R.id.img_profile);
    TextView userNM = findViewById(R.id.tv_userNM);
    TextView mobileNO = findViewById(R.id.tv_mobileNO);

    String photoURL = Value.PhotoADDRESS + PrefsHelper.read("profileImg","") + ".jpg";
    // 사진 이미지가 존재하지 않을 수도 있으므로 존재 여부를 체크하여 존재하면 ImageView 에 표시한다.
    PhotoURLExists task = new PhotoURLExists();
    try {
        if(task.execute(photoURL).get()==true){
            Glide.with(mContext).load(photoURL).override(170, 200).into(img_profile);
        }
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    userNM.setText(PrefsHelper.read("userNM",""));
    mobileNO.setText(PrefsHelper.read("mobileNO",""));
}

private class PhotoURLExists extends AsyncTask<String, Void, Boolean> {
    @Override
    protected Boolean doInBackground(String... params) {
        try {
            HttpURLConnection.setFollowRedirects(false);
            HttpURLConnection con =  (HttpURLConnection) new URL(params[0]).openConnection();
            con.setRequestMethod("HEAD");
            return (con.getResponseCode() == HttpURLConnection.HTTP_OK);
        }
        catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
} 


앱 build.gradle 추가

implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' 


Glide.with(this)
    .load("이미지 url...")
    .override(이미지 사이즈) // ex) override(170, 200)
    .into(imageView);


Glide 최신버전 확인 : https://github.com/bumptech/glide


참고하면 도움되는 자료

AsyncTask return 결과 처리하는 방법 : https://link2me.tistory.com/1516

[Java] SharedPreferences Singleton

출처: https://link2me.tistory.com/1827 [소소한 일상 및 업무TIP 다루기]
[Java] SharedPreferences Singleton

출처: https://link2me.tistory.com/1827 [소소한 일상 및 업무TIP 다루기]
[Java] SharedPreferences Singleton

출처: https://link2me.tistory.com/1827 [소소한 일상 및 업무TIP 다루기]

[Java] SharedPreferences Singletonhttps://link2me.tistory.com/1827


블로그 이미지

Link2Me

,