728x90

VSCode 에서 코드 자동완성 기능을 사용하는 방법을 적어둔다.

WebStorm에서는 "코드 자동완성", "Auto Import" 추가 설정없이 기본 내장되어 있어 신경 쓸 필요가 없더라.

 

1. 코드 자동 완성

- VSCode extension 버튼을 클릭하고 "react code snippets"를 입력하고 설치한다.

 

코드 파일에서

- 함수형인 경우 rsc 를 입력하고 탭키를 누르면 자동완성 기본 폼이 만들어진다.

- 클래스형 component인 경우 rcc 를 입력하고 탭키를 누르면 된다.

 

2. Auto Import

인터넷 강의를 듣다보면 VSCode로 설명하는데 import를 직접 추가 해주는 걸 보게 된다.

 

이렇게 추가해주고 나서 VScode를 한번 종료하고 나서 실행이 제대로 되더라.

Use까지만 입력하면 자동으로 바로 아래에 팝업창이 나온다. 선택하면 자동 import가 된다.

 

'React > React TOOL&TIP' 카테고리의 다른 글

React 프로젝트 실행 및 npm 최신버전 Update  (1) 2022.05.23
블로그 이미지

Link2Me

,

React useReducer

React/React 2022. 10. 13. 23:47
728x90

 

  • state : 현재 상태
  • dispatch : action을 발생시키는 함수
  • reducer : state와 action를 받아 새로운 state를 반환하는 함수
  • initialState : 초기값

 

useReducer 함수에는 첫번째 인자로 reducer 함수를, 두번째 인자로 초기 상태값을 넣어준다.

import React, { useReducer } from 'react';
 
function reducer(state, action) {
    switch (action.type) {
        case 'PLUS':
            return state + 1;
        case 'MINUS':
            return state - 1;
        default:
            return state;
    }
}
 
function Counter() {
    const [number, dispatch] = useReducer(reducer, 0);
 
    const onIncrease = () => {
        dispatch({ type: 'PLUS' });
    };
 
    const onDecrease = () => {
        dispatch({ type: 'MINUS' });
    };
 
    return (
        <div>
            <h1>{number}</h1>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
        </div>
    );
}
 
export default Counter;

 

import logo from './logo.svg';
import './App.css';
import Counter from "./counter";
 
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <Counter />
      </header>
    </div>
  );
}
 
export default App;
 

 

 

 

'React > React' 카테고리의 다른 글

React useMemo & useCallback  (0) 2022.10.24
React useRef 사용 예제  (0) 2022.10.15
React-bootstrap Header.js 테스트  (0) 2022.08.21
React CORS error 해결 방법  (0) 2022.08.20
React Query String  (0) 2022.06.08
블로그 이미지

Link2Me

,

TypeScript any

React/typescript 2022. 10. 13. 10:41
728x90

TypeScript에는 다른 프로그래밍 언어에는 존재하지 않는 타입들이 존재한다.

 

자바스크립트에서는 아래와 같은 결과를 반환한다.

let value = 10;
console.log(typeof value); // number
console.log(value.length); // undefined

 

타입스크립트에서는 아래와 같이 에러가 발생함을 표시해준다.

밑줄 표시된 부분에 마우스를 가져가면....

TS2339: Property 'length' does not exist on type 'number'.

라는 결과를 보여준다. number 타입은 length 프로퍼티가 존재하지 않는다.

 

문자열인 경우에는 Javascript 와 TypeScript 모두 에러를 반환하지 않는다.

let str = "10";
console.log(typeof str); // string
console.log(str.length); // 2

 

변수 타입을 any 로 지정하면 에러가 사라지는 결 확인할 수 있다.

any is a type that disables type checking and effectively allows all types to be used.

즉 모든 타입을 허용한다는 뜻이다.

 

let u: any = true;
= "string"// Type 'string' is not assignable to type 'boolean'.
Math.round(u); // Argument of type 'boolean' is not assignable to parameter of type 'number'.

any 타입으로 하면 boolean, string, number 모두를 허용하므로 원치 않는 결과가 발생할 수 있다.

 

Javascript 로 변환하여 결과를 출력하면 어떻게 되는지 확인해보자.

// Javascript 에서 아래와 같이 값을 할당하는데 에러를 출력하지 않는다.
let u = true;
= "string"// Error: string 타입은 boolean 타입이 아니라 할당 불가
console.log(Math.round(u)); // NaN

콘솔 로그로 확인해보면 NaN 이란 결과를 출력한다.

 

any 타입지정은 원하지 않는 결과를 초래할 수 있으니 사용하지 않는게 좋다.

TypeScript 에서 경고 출력하는 메시지를 확인하면서 코드를 올바르게 수정하자.

'React > typescript' 카테고리의 다른 글

타입스크립트(TypeScript) 개요  (0) 2022.10.09
블로그 이미지

Link2Me

,
728x90

타입스크립트(TypeScript)는 자바스크립트(JavaScript)를 기반으로 정적 타입 문법을 추가한 프로그래밍 언어이다.

 

자바스크립트는 C나 Java와 같은 C-family 언어와는 구별되는 아래와 같은 특성이 있다.
- Prototype-based Object Oriented Language
- Scope와 this
- 동적 타입(dynamic typed) 언어 혹은 느슨한 타입(loosely typed) 언어
이와 같은 특성은 클래스 기반 객체지향 언어(Java, C++, C# 등)에 익숙한 개발자를 혼란스럽게 하며 코드가 복잡해질 수 있고 디버그와 테스트 공수가 증가하는 등의 문제를 일으킬 수 있어 특히 규모가 큰 프로젝트에서는 주의하여야 한다.

TypeScript 또한 자바스크립트 대체 언어의 하나로써 자바스크립트(ES5)의 Superset(상위확장)이다. C#의 창시자인 덴마크 출신 소프트웨어 엔지니어 Anders Hejlsberg(아네르스 하일스베르)가 개발을 주도한 TypeScript는 Microsoft에서 2012년 발표한 오픈소스로, 정적 타이핑을 지원하며 ES6(ECMAScript 2015)의 클래스, 모듈 등과 ES7의 Decorator 등을 지원한다.

 

// TypeScript 전역 설치
npm install -g typescript

 

 

TypeScript 파일(.ts)은 브라우저에서 동작하지 않으므로 TypeScript 컴파일러를 이용해 자바스크립트 파일로 변환해야 한다. 최종적으로 런타임에서는 이렇게 출력된 JavaScript 코드를 구동시키게 된다.

tsc script 와 같이 입력하면 script.js 파일이 같은 폴더에 생성된다.

 

자바스크립트 오류

- 실행을 하면 원치 않는 결과를 반환하기도 한다.

const a = 3// 정수
const b = '5'// 문자열
console.log(a + b); // 결과 : 35
console.log(a * b); // 결과 : 15 (원치 않는 결과)

 

타입스크립트에서는

숫자면 숫자, 문자열이면 문자열이라고 타입을 선언해주어서 계산이 작동되지 못하게 하거나, 컴파일 전에 오류 메시지를 띄우게 한다.

위와 같이 에러라고 표시를 해주고 있다.

강제로 tsc ex001.ts 를 터미널 창에서 실행하면.... ES3 버전의 Javascript 파일로 만들어진다.

tsc ex001.ts -t ES2015 를 터미널 창에서 실행하면 ES5 버전의 Javasript 파일로 만들어진다.

 

폴더에 있는 모든 ts 파일을 한꺼번에 js 파일로 만들려면 아래와 같이 하면 된다.

이제부터는 터미널 창에서 tsc 만 입력하면 자동으로 js 파일로 변경해준다.

만약 tsc ex001.ts 라고 입력하면 tsconfig.json이 무시되므로 주의하자.

 

TypeScript는 정적 타입을 지원하므로 컴파일 단계에서 오류를 포착할 수 있는 장점이 있다. 

명시적인 정적 타입 지정은 개발자의 의도를 명확하게 코드로 기술할 수 있다. 이는 코드의 가독성을 높이고 예측할 수 있게 하며 디버깅을 쉽게 한다.

 

 

 

참고하면 도움되는 글

https://yozm.wishket.com/magazine/detail/1376/

 

TypeScript는 어떻게 공부해야 하나요? | 요즘IT

지금 현재 개발하는 상황을 보면 TypeScript는 피할 수 없는 하나의 대세가 된 것 같습니다. TypeScript가 나온 이후로 점점 TypeScript로 만들어지고 있는 라이브러리나 코드의 비중은 높아지고 있고 아

yozm.wishket.com

 

 

 

 

 

 

'React > typescript' 카테고리의 다른 글

TypeScript any  (0) 2022.10.13
블로그 이미지

Link2Me

,
728x90

자바스크립트 정규표현식 생성

1. 생성자 방식

    RegExp 생성자 함수를 호출하여 사용할 수 있다.

// new RegExg(표현식)
const regexp1 = new RegExp("^abc");
 
// new RegExg(표현식, 플래그)
const regexp2 = new RegExp("^abc""gi");
const regexp1 = new RegExp(/^abc/i); // ES6
const regexp2 = new RegExp(/^abc/'i');
const regexp3 = new RegExp('^abc''i');

 

 

2. 리터럴 방식

// 정규표현식은 /로 감싸진 패턴을 리터럴로 사용한다.
// 정규 표현식 리터럴은 패턴(pettern)과 플래그(flag)로 구성된다.
const regexp1 = /^abc/;  // --> /패턴/
const regexp2 = /^abc/gi; // --> /패턴/플래그

 

 

자바스크립트 메소드

문법   설명
정규식.exec(문자열)                         일치하는 하나의 정보(Array) 반환
정규식.test(문자열)                           일치 여부(Boolean) 반환
문자열.match(정규식)                       일치하는 문자열의 배열(Array) 반환
문자열.search(정규식)                      일치하는 문자열의 인덱스(Number) 반환
문자열.replace(정규식,대체문자)     일치하는 문자열을 대체하고 대체된 문자열(String) 반환
문자열.split(정규식)                          일치하는 문자열을 분할하여 배열(Array)로 반환
생성자_정규식.toString()                  생성자 함수 방식의 정규식을 리터럴 방식의 문자열(String)로 반환

 

 

For example a(?=b) will match the "a" in "ab", but not the "a" in "ac".

Whereas a(?!b) will match the "a" in "ac", but not the "a" in "ab".

const regexr = /.+(?=:)/;  // positive 전방 탐색(lookahead)
const str = 'https://www.abc.com';
console.log(regexr.exec(str));  // 결과 : https
console.log(str.match(regexr)); // 결과 : https

 

const regexr = /.(?!.)/gi; // 부정형 전방탐색(negative lookahead)
const str = '홍길동';
console.log(regexr.exec(str));  // 결과 : 동
console.log(str.replace(regexr, '*')); // 결과 : 홍길*

 

정규표현식 기본적인 이해는 https://www.nextree.co.kr/p4327/ 를 참조하면 도움된다.

https://heropy.blog/2018/10/28/regexp/

'React > morden javascript' 카테고리의 다른 글

Javascript Array.map 사용 예제  (2) 2022.12.27
카카오 맵 장소 검색 구현 방법  (0) 2022.12.19
[Javascript] _cloneDeep()  (0) 2022.10.05
[ES6] Spread 연산자  (0) 2022.10.02
자바스크립트 Object  (0) 2022.06.11
블로그 이미지

Link2Me

,
728x90

현재 폴더에 package.json 추가된다.
npm init –y

 

개발용 의존성 패키지 설치 : 개발할 때만 필요하다.  --save-dev 대신에 -D
npm install parcel-bundler –D

 

원하는 버전을 명시하여 버전 설치
npm install lodash@4.17.20

최신버전의 정보를 보여준다.
npm info lodash

package.json 이 있는 곳에서 명령어를 입력해야 한다.
npm update lodash

package.json 파일이 있으면 node_module 을 설치할 수 있다.
package.josn 파일과 package-lock.json 파일을 삭제되지 않게 조심해야 한다.
npm install

 

{
  "name""ex01",
  "version""1.0.0",
  "scripts": {
    "dev""parcel index.html",
    "build""parcel build index.html"
  },
  "keywords": [],
  "author""",
  "license""ISC",
  "description""",
  "dependencies": {
    "lodash""^4.17.21"
  },
  "type""module"
}

Cannot use import statement outside a module 와 같은 에러 메시지가 발생하면

위와 같이 package.json에  "type": "module" 을 추가해주면 해결된다.

 

형변환(Type conversion)
=== 일치연산자
== 동등연산자 : 형변환이 발생한다.

 

객체는 참조타입(reference type)이기 때문에 할당 연산자 (=) 를 사용하면 메모리 주소값이 복사된다.

Spread operator 를 사용하여 의도적으로 얕은 복사(Shallow Copy)를 할 수 있다.

깊은 복사는 lodash 모듈을 설치해서 이용하면 편리하다.

loash 에서 제공하는 얕은 복사 함수는 _.clone(object) 이다.

loash 에서 제공하는 깊은 복사 함수는 _.cloneDeep(object) 이다.

import _ from 'lodash';
 
const user = {
    name'홍길동',
    age : 25,
    emails: ['link2me@test.com']
}
 
const copyUser = _.cloneDeep(user);
console.log(copyUser === user); // 메모리 주소가 같은가?
 
user.age = 30;
user.emails.push('jsk005@test.com');
console.log('user', user);
console.log('copyUser', copyUser);

결과

 

 

 

 

 

 

 

 

 

'React > morden javascript' 카테고리의 다른 글

카카오 맵 장소 검색 구현 방법  (0) 2022.12.19
[Javascript] 정규표현식  (0) 2022.10.08
[ES6] Spread 연산자  (0) 2022.10.02
자바스크립트 Object  (0) 2022.06.11
자바스크립트 호이스팅(Hoisting)  (0) 2022.06.10
블로그 이미지

Link2Me

,
728x90

ES6에서는  '...'와 같이 다소 특이한 형태의 문법이 추가되었다. 
점 3개가 연달아 붙어있는 이 표시는 Spread Opertor(스프레드 오퍼레이터, 전개 연산자)를 나타내는 것으로, 배열, 함수, 객체 등을 다루는 데 있어서 매우 편리하고 재미있는 새로운 기능을 제공한다.

 

const numbersOne = [123];
const numbersTwo = [456];
const numbersCombined = [...numbersOne, ...numbersTwo];

 

 

const numbers = [123456];
 
const [one, two, ...rest] = numbers;

컴마 개수 이후의 값이 rest 값이다.

 

const arr1 = [012];
const arr2 = [345];
 
// (1) 배열 복사
const arr3 = [...arr1]; // arr3은 [0, 1, 2]
 
// (2) 배열에 추가 요소로 넣기
const arr4 = [12, ...arr2, 910]; // arr4는 [1, 2, 3, 4, 5, 9, 10]
 
// (3) 두 배열 이어 붙이기
const arr5 = [...arr1, ...arr2]; // [0, 1, 2, 3, 4, 5];

 

'React > morden javascript' 카테고리의 다른 글

[Javascript] 정규표현식  (0) 2022.10.08
[Javascript] _cloneDeep()  (0) 2022.10.05
자바스크립트 Object  (0) 2022.06.11
자바스크립트 호이스팅(Hoisting)  (0) 2022.06.10
Javascript this and Class  (0) 2022.06.09
블로그 이미지

Link2Me

,
728x90

로그인 6개월 차단하는 기능을 구현해 달라는 요청으로 간단 구현한 사항 적어둔다.

 

<?php
error_reporting(0);
/*
ini_set("display_startup_errors", 1);
ini_set("display_errors", 1);
error_reporting(E_ALL);
// */
 
require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
require_once $g['path_root'].'sessionChk.php'// 세션 체크
require_once $g['path_root'].'ipFiltering.php';
require_once $g['path_class'].'dbconnect.php';
$a = new DBDataClass();
 
date_default_timezone_set('Asia/Seoul');
$today = date("Y-m-d"); // 오늘 날짜
 
// getDbUpdate($table, $set, $params, $where);
// $access // 이용권한 없음(0), 로그인 허용(1), 가입승인대기(2)
$rows = $a->getDbresult('members','admin=0','date, idx, regdate');
foreach($rows as $R):
    if($R['date'!== NULL) {
        // 최근접속일자
        $from_day = substr($R['date'],0,4)."-".substr($R['date'],4,2)."-".substr($R['date'],6,2);
        echo $from_day.'<br />';
        $from = new DateTime($from_day);
        $to = new DateTime($today);
        $day_diff = $from->diff($to)->days;
        if($day_diff > 180){ // 6개월동안 로그인한 기록이 없으면
            $SET ="access=0"// 접속을 차단시킨다.
            $params=array($R['idx']);
            $a->getDbUpdate('members',$SET,$params,'idx=?');
            echo 'idx : '.$R[1].'<br />';
        }
    } else {
        // 가입일자
        $from_day = substr($R['regdate'],0,4)."-".substr($R['regdate'],4,2)."-".substr($R['regdate'],6,2);
        echo $from_day.'<br />';
        $from = new DateTime($from_day);
        $to = new DateTime($today);
        $day_diff = $from->diff($to)->days;
        if($day_diff > 365){
            $SET ="access=2";
            $params=array($R['idx']);
            $a->getDbUpdate('members',$SET,$params,'idx=?');
            echo 'no access : '.$R[1].'<br />';
        }
    }
endforeach;
?>
 

 

 

<?php
class DBDataClass {
    protected $db// 변수를 선언한 클래스와 상속받은 클래스에서 참조할 수 있다.
 
    public function __construct() {
        $this->dbConnect();
        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.
    }
 
    private function dbConnect() {
        require_once 'dbinfo.php';
        try {
            // MySQL PDO 객체 생성
            $this->db = new PDO(_DSN, _DBUSER, _DBPASS);
            $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
        } catch(PDOException $ex) {
            die("오류 : " . $ex->getMessage());
        }
    }
 
    /*
     $sql = "INSERT INTO users (name, surname, sex) VALUES (?,?,?)";
     $stmt= $pdo->prepare($sql);
     $stmt->execute(array($name, $surname, $sex));
    */
    public function recur_quot($cnt) {
        $R = array();
        for ($i = 0$i < $cnt$i++) {
            array_push($R"?");
        }
        return implode(","$R); // 배열을 문자열로
    }
 
    // 신규 자료 추가(ok)
    function putDbInsert($table$key$params) {
        try {
            $this->db->beginTransaction();
            $sql = "insert into " . $table . " (" . $key . ") values(" . $this->recur_quot(count($params)) . ")";
            $stmt = $this->db->prepare($sql);
            $status = $stmt->execute($params); // $params 는 배열 값
            $this->db->commit();
            return 1;
        } catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : " . $pex->getMessage();
            return 0;
        }
    }
 
 
    function getDbUpdate($table$set$params$where) {
        $sql = "update " . $table . " set " . $set . ($where ? ' where ' . $where : '');
        try {
            $this->db->beginTransaction();
            $stmt = $this->db->prepare($sql);
            $status = $stmt->execute($params);
            $this->db->commit();
            return 1;
        } catch (PDOException $pex) {
            $this->db->rollBack();
            echo "에러 : " . $pex->getMessage();
            return 0;
        }
    }
 
    public function putDbArray($sql$params) {
        // $params : array 를 사용해야 한다.
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(); //foreach 문과 연동하여 결과처리
    }
 
    
    // 검색조건에 일치하는 데이터 가져오기
    public function getDbData($table$where$column$returntype = '') {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        if ($returntype == 1) {
            return $stmt->fetch(PDO::FETCH_ASSOC);
        } else {
            return $stmt->fetch();
        }
    }
 
    // DB Query result 함수
    function getDbresult($table,$where,$column) {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '');
        //echo $sql.'<br />';
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        return $stmt->fetchAll();
    }
 
    // table 결과 조회 용도
    public function getDbArray($table$where$column$orderby$rowsPage$curPage) {
        $sql = 'select ' . $column . ' from ' . $table . ($where ? ' where ' . $this->getSqlFilter($where) : '') . ($orderby ? ' order by ' . $orderby : '') . ($rowsPage ? ' limit ' . (($curPage - 1* $rowsPage) . ', ' . $rowsPage : '');
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        return $stmt->fetchAll(); //foreach 문과 연동하여 결과처리 하면 됨
    }
 
    //SQL필터링
    public function getSqlFilter($sql) {
        //$sql = preg_replace("/[\;]+/","", $sql); // 공백은 제거 불가
        return $sql;
    }
 
?>
 

 

 

 

 

블로그 이미지

Link2Me

,
728x90

Android Room 예제를 검색하면 가장 많이 검색되는 것이 Simple Note 를 다룬 것이다.

아래 동영상 이외에도 대부분의 게시글, 동영상을 보면 모두 Simple Note를 약간씩 다르게 다루고 있지만 본질은 모두 동일하더라.

https://www.youtube.com/watch?v=xPPMygGxiEo 

 

이 자료를 토대로 하여 코드를 약간 손을 보고 디자인 부분을 수정하고 검색 기능을 추가하여 자료를 정리했다.

- CoordinatorLayout 과 MaterialToolbar 사용

- Menu SearchView 기능 추가 및 Room Database 검색 기능 추가

- ListAdapter 와 RecyclerView.Adapter 두가지 구현

- View Binding 적용

 

package com.link2me.android.notesample.view.activity;
 
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
 
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
 
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.link2me.android.notesample.R;
import com.link2me.android.notesample.databinding.ActivityMainBinding;
import com.link2me.android.notesample.model.Note;
import com.link2me.android.notesample.view.adapter.NoteViewAdapter;
import com.link2me.android.notesample.viewmodel.MyFactory;
import com.link2me.android.notesample.viewmodel.NoteViewModel;
 
import java.util.List;
 
public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    private Context mContext;
    private ActivityMainBinding binding;
 
    public static final int ADD_NOTE_REQUEST = 1;
    public static final int EDIT_NOTE_REQUEST = 2;
 
    private NoteViewModel viewModel;
 
    private RecyclerView mRecyclerView;
    //private NoteAdapter mAdapter;
    private NoteViewAdapter mAdapter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        mContext = MainActivity.this;
 
        initView();
    }
 
    private void initView() {
        // 동기식 데이터 처리에서는 순서가 중요함.
        setToolbar();
        buildRecyclerView();
        initViewModel();
        setButtons();
    }
 
    private void setToolbar() {
        // activity에 툴바를 추가하려면 우선 기본 앱바(AppBar)부터 비활성화해야 한다.
        // 기존 actionBar는 theme 에서 NoActionBar로 설정하여 제거하고 툴바를 생성한다.
        setSupportActionBar(binding.mainToolbar);
        // Toolbar의 왼쪽에 버튼을 추가하고 버튼의 아이콘을 바꾼다.
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_dehaze_24);
 
        // 타이틀명 변경
        if (getSupportActionBar() != null) {
            getSupportActionBar().setTitle("TODO LIST");
            binding.mainToolbar.setTitleTextColor(Color.WHITE);
        }
 
    }
 
    private void initViewModel() {
        // viewModel init
        //viewModel = new ViewModelProvider(this, new MyFactory(getApplication())).get(NoteViewModel.class);
        viewModel = new ViewModelProvider(this).get(NoteViewModel.class);
 
        // viewModel observe
        viewModel.getAllNotes().observe(thisnew Observer<List<Note>>() {
            @Override
            public void onChanged(@Nullable List<Note> notes) {
                //mAdapter.submitList(notes);
                mAdapter.setNotes(notes);
                for(Note note:notes){
                    Log.e(TAG,note.getId() + ", " + note.getTitle() + ", " + note.getDescription() + ", " + note.getPriority());
                }
                Log.e(TAG, "note_db size : " + notes.size());
            }
        });
 
    }
 
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e(TAG,"onNewIntent");
    }
 
    private void buildRecyclerView() {
        mRecyclerView = binding.recyclerView;
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(mContext);
 
//        mAdapter = new NoteAdapter();
        mAdapter = new NoteViewAdapter();
 
        DividerItemDecoration decoration = new DividerItemDecoration(mContext, manager.getOrientation());
        mRecyclerView.addItemDecoration(decoration);
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(mAdapter);
    }
 
    private void setButtons() {
        // 안드로이드 디자인 지원 라이브러리에는 FloatingActionButton 클래스가 제공된다.
        FloatingActionButton buttonAddNote = binding.buttonAddNote;
        buttonAddNote.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, AddNoteActivity.class);
                startActivityForResult(intent, ADD_NOTE_REQUEST);
            }
        });
 
        new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,
                ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
            @Override
            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                return false;
            }
 
            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
                viewModel.delete(mAdapter.getNoteAt(viewHolder.getBindingAdapterPosition()));
                Toast.makeText(MainActivity.this"Note deleted", Toast.LENGTH_SHORT).show();
            }
        }).attachToRecyclerView(mRecyclerView);
 
        mAdapter.setOnItemClickListener(new NoteViewAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(Note note) {
                Intent intent = new Intent(MainActivity.this, AddNoteActivity.class);
                intent.putExtra(AddNoteActivity.EXTRA_ID, note.getId());
                intent.putExtra(AddNoteActivity.EXTRA_TITLE, note.getTitle());
                intent.putExtra(AddNoteActivity.EXTRA_DESCRIPTION, note.getDescription());
                intent.putExtra(AddNoteActivity.EXTRA_PRIORITY, note.getPriority());
                startActivityForResult(intent, EDIT_NOTE_REQUEST);
            }
        });
 
    }
 
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
 
        if (requestCode == ADD_NOTE_REQUEST && resultCode == RESULT_OK) {
            String title = data.getStringExtra(AddNoteActivity.EXTRA_TITLE);
            String description = data.getStringExtra(AddNoteActivity.EXTRA_DESCRIPTION);
            int priority = data.getIntExtra(AddNoteActivity.EXTRA_PRIORITY, 1);
 
            Note note = new Note(title, description, priority);
            viewModel.insert(note);
 
            Toast.makeText(this"메모가 저장되었습니다.", Toast.LENGTH_SHORT).show();
        } else if (requestCode == EDIT_NOTE_REQUEST && resultCode == RESULT_OK) {
            int id = data.getIntExtra(AddNoteActivity.EXTRA_ID, -1);
 
            if (id == -1) {
                Toast.makeText(this"Note can't be updated", Toast.LENGTH_SHORT).show();
                return;
            }
 
            String title = data.getStringExtra(AddNoteActivity.EXTRA_TITLE);
            String description = data.getStringExtra(AddNoteActivity.EXTRA_DESCRIPTION);
            int priority = data.getIntExtra(AddNoteActivity.EXTRA_PRIORITY, 1);
 
            Note note = new Note(title, description, priority);
            note.setId(id);
            viewModel.update(note);
 
            Toast.makeText(this"Note updated", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this"Note not saved", Toast.LENGTH_SHORT).show();
        }
    }
 
    /***
     * 메뉴는 프로젝트의 res/menu 폴더에 저장되는 XML 리소스 형식으로 정의할 수 있다.
     * 메뉴는 루트 노드에서 menu 태그와 일련의 item 태그로 구성된다.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // 부모 activity 나 fragment 와 메뉴를 연관시켜야 하므로 항상 super.onCreateOptionsMenu를 호출해야 한다.
        super.onCreateOptionsMenu(menu);
        MenuInflater menuInflater = getMenuInflater();
        menuInflater.inflate(R.menu.main_menu, menu);
 
        // 검색 메뉴 등록
        MenuItem searchItem = menu.findItem(R.id.action_search);
        SearchView searchView = (SearchView) searchItem.getActionView();
 
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                // 문자열 입력을 완료했을 때 문자열 반환
                if(query != null) {
                    searchDatabase(query);
                }
                return true;
            }
 
            @Override
            public boolean onQueryTextChange(String newText) {
                // 문자열이 변할 때마다 바로바로 문자열 반환
                if(newText != null) {
                    searchDatabase(newText);
                }
                return true;
            }
        });
 
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.delete_all_notes:
                viewModel.deleteAllNotes();
                Toast.makeText(this"All notes deleted", Toast.LENGTH_SHORT).show();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
 
    private void searchDatabase(String search){
        String searchQuery = "%" + search + "%";
 
        viewModel.searchDatabase(searchQuery).observe(thisnew Observer<List<Note>>() {
            @Override
            public void onChanged(List<Note> notes) {
                //mAdapter.submitList(notes);
                mAdapter.setNotes(notes);
            }
        });
    }
 
}

 

 

NoteViewModel.java

 
import android.app.Application;
 
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
 
import com.link2me.android.notesample.model.Note;
import com.link2me.android.notesample.repository.NoteRepository;
import com.link2me.android.notesample.view.activity.MainActivity;
 
import java.util.List;
 
public class NoteViewModel extends AndroidViewModel {
    private NoteRepository repository;
    private LiveData<List<Note>> allNotes;
 
    public NoteViewModel(@NonNull Application application) {
        super(application);
        repository = new NoteRepository(application);
        allNotes = repository.getAllNotes();
    }
 
    public void insert(Note note) {
        repository.insert(note);
    }
 
    public void update(Note note) {
        repository.update(note);
    }
 
    public void delete(Note note) {
        repository.delete(note);
    }
 
    public void deleteAllNotes() {
        repository.deleteAllNotes();
    }
 
    public LiveData<List<Note>> getAllNotes() {
        return allNotes;
    }
 
 
    public LiveData<List<Note>> searchDatabase(String searchQuery){
        return repository.searchDatabase(searchQuery);
    }
}

 

전체적인 폴더 구조는 아래와 같다.

 

전체 소스코드는 GitHub에 올려두었다.

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

 

GitHub - jsk005/JavaProjects: 활용 수준의 자바 기능 테스트

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

github.com

 

블로그 이미지

Link2Me

,
728x90

MVVM 구조의 핵심 사항만 소스코드로 예시한다.

전체 소스코드는 Github 에서 받아서 Project 에서 New Module을 생성하고 src 폴더를 덮어쓰기 해도 되고, 참고하면서 소스코드를 작성하는 것도 방법이다.

암호화함수를 모아둔 Class 는 오픈하지 않았으므로 전체적인 로직만 참고하면 도움될 것이다.

 

AddressRepository.java

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
 
import com.link2me.android.common.CipherUtils;
import com.link2me.android.rview_ex3.model.AddressResult;
import com.link2me.android.rview_ex3.network.RetrofitAdapter;
import com.link2me.android.rview_ex3.network.RetrofitService;
import com.link2me.android.rview_ex3.network.RetrofitURL;
 
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
 
public class AddressRepository {
    private RetrofitService service;
    private MutableLiveData<AddressResult> mutableLiveData;
 
    public AddressRepository() {
        mutableLiveData = new MutableLiveData<>();
        service = RetrofitAdapter.getClient(RetrofitURL.BaseUrl).create(RetrofitService.class);
    }
 
    public void addressDataRepo(String search) {
        String keyword = CipherUtils.encryptAES(CipherUtils.URLkey());
        service.GetAddrData(keyword, search).enqueue(new Callback<AddressResult>() {
            @Override
            public void onResponse(Call<AddressResult> call, Response<AddressResult> response) {
                mutableLiveData.postValue(response.body());
            }
 
            @Override
            public void onFailure(Call<AddressResult> call, Throwable t) {
                mutableLiveData.postValue(null);
            }
        });
    }
 
    public LiveData<AddressResult> getAddressResultLiveData(){
        return mutableLiveData;
    }
}
 

 

 

AddressViewModel.java

public class AddressViewModel extends ViewModel {
    private AddressRepository repository;
    private LiveData<AddressResult> _liveData;
 
    public AddressViewModel() {
    }
 
    public void getAllAddressData(String search){
        repository.addressDataRepo(search);
    }
 
    public LiveData<AddressResult> get_liveData(){
        if(_liveData == null){
            repository = new AddressRepository();
            _liveData = repository.getAddressResultLiveData();
        }
        return _liveData;
    }
}
 

 

 

AddressListAdapter.java

 
/***
 * https://www.youtube.com/watch?v=xPPMygGxiEo 동영상 시청하면 개념 이해하는데 엄청 도움됨
 */
 
// RecyclerView.Adapter 대신에 ListAdapter<T, VH> 를 상속한다.
// T는 리사이클러뷰가 화면에 표시할 데이터의 타입이다.
public class AddressListAdapter extends ListAdapter<Address_Item, AddressListAdapter.ViewHolder> {
    private final String TAG = this.getClass().getSimpleName();
    Context context;
 
    // 인터페이스 선언 -------------------------------------------------------------
    private OnItemClickListener mListener;
 
    public interface OnItemClickListener {
        void onItemClicked(Address_Item item, int position);
    }
 
    public void setOnItemClickListener(OnItemClickListener listener){
        mListener = listener;
    }
    // 인터페이스 ----------------------------------------------------------------
 
    public AddressListAdapter(Context context, @NonNull DiffUtil.ItemCallback<Address_Item> diffCallback) {
        super(diffCallback);
        this.context = context;
    }
 
    public void selectAll(int list_count){ // checkbox 전체 선택
        for(int i=0;i < list_count;i++){
            getItem(i).setCheckBoxState(true);
        }
        notifyDataSetChanged();
    }
    public void unselectall(int list_count){ // checkbox 전체 해제
        for(int i=0;i < list_count;i++){
            getItem(i).setCheckBoxState(false);
        }
        notifyDataSetChanged();
    }
 
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ItemAddressBinding binding = ItemAddressBinding.inflate(LayoutInflater.from(parent.getContext()),parent,false);
        return new ViewHolder(binding);
    }
 
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        // 실제로 데이터를 표시하는 부분
        //Address_Item currentItem = rvItemList.get(position);
        Address_Item currentItem = getItem(position);
        holder.bindItem(currentItem, position);
    }
 
    public class ViewHolder extends RecyclerView.ViewHolder{
        ItemAddressBinding itemBinding;
 
        public ViewHolder(@NonNull ItemAddressBinding binding) {
            super(binding.getRoot());
            itemBinding = binding;
        }
 
        void bindItem(Address_Item item, int position){
            String photoURL = RetrofitURL.BaseUrl + "photos/" + item.getPhoto();
            if(photoURL.contains("null")){
                Glide.with(context).asBitmap().load(R.drawable.photo_base).into(itemBinding.profileImage);
            } else {
                Glide.with(context).asBitmap().load(photoURL).into(itemBinding.profileImage);
            }
            itemBinding.childName.setText(item.getUserNM());
            itemBinding.childMobileNO.setText(PhoneNumberUtils.formatNumber(item.getMobileNO()));
            itemBinding.childOfficeNO.setText(PhoneNumberUtils.formatNumber(item.getTelNO()));
 
            itemBinding.profileImage.setOnClickListener(view1 -> {
                if(mListener != null){
                    mListener.onItemClicked(item,position); // 클릭의 결과로 값을 전달
                }
            });
 
            if (isCheckFlag == false) {
                itemBinding.callBtn.setVisibility(View.VISIBLE);
                itemBinding.callBtn.setOnClickListener(view -> builder.show());
                itemBinding.listcellCheckbox.setVisibility(View.GONE);
                itemBinding.childLayout.setOnClickListener(view ->
                        Toast.makeText(context, "상세보기를 눌렀습니다 ===" + position + " , idx :" + item.getIdx(), Toast.LENGTH_SHORT).show());
 
                itemBinding.childLayout.setOnLongClickListener(v -> {
                    isCheckFlag = true;
                    constraintLayout.setVisibility(View.VISIBLE);
                    notifyDataSetChanged();
                    return true;
                });
            } else {
                itemBinding.callBtn.setVisibility(View.GONE);
                //convertView.setClickable(false);
                itemBinding.listcellCheckbox.setVisibility(View.VISIBLE);
                itemBinding.listcellCheckbox.setTag(position); // This line is important.
 
                // 체크 박스 클릭하면 CheckBoxState 에 반영한다. setOnCheckedChangeListener 대신 사용
                itemBinding.listcellCheckbox.setOnClickListener(v -> {
                    if(getItem(position).getCheckBoxState() == true){
                        getItem(position).setCheckBoxState(false);
                        Log.d("checkbox","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
                    } else {
                        getItem(position).setCheckBoxState(true);
                        Log.d("checkbox","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
                    }
                });
 
                itemBinding.childLayout.setOnClickListener(v -> {
                    if(itemBinding.listcellCheckbox.isChecked() == false){
                        itemBinding.listcellCheckbox.setChecked(true);
                        getItem(position).setCheckBoxState(true);
                        //notifyDataSetChanged();
                        Log.d("checklist","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
                    } else {
                        itemBinding.listcellCheckbox.setChecked(false);
                        getItem(position).setCheckBoxState(false);
                        //notifyDataSetChanged();
                        Log.d("checklist","position : "+ position + " checkBoxState === "+ getItem(position).getCheckBoxState());
                    }
                });
            }
 
            // 재사용 문제 해결
            if(getItem(position).getCheckBoxState() == true){
                itemBinding.listcellCheckbox.setChecked(true);
                Log.d("ReUse","position : " + position + " checkBoxState === " + getItem(position).getCheckBoxState());
            } else {
                itemBinding.listcellCheckbox.setChecked(false);
                Log.d("ReUse","position : "+position + " checkBoxState ===" + getItem(position).getCheckBoxState());
            }
        }
    }
 
}
 
 

 

 

AddressActivity.java

public class AddressActivity extends AppCompatActivity implements AddressListAdapter.OnItemClickListener {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
    private ActivityAddressBinding binding;
 
    private AddressViewModel viewModel;
 
    private ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버 전체 데이터 리스트
    private ArrayList<Address_Item> searchItemList = new ArrayList<>(); // 검색한 데이터 리스트
    private RecyclerView mRecyclerView;
    private AddressListAdapter mAdapter;
 
    public static ConstraintLayout constraintLayout;
    public static boolean isCheckFlag = false;
    public CheckBox checkAll;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_address);
        binding = ActivityAddressBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        mContext = AddressActivity.this;
        initView();
    }
 
    private void initView() {
        // 동기식 데이터 처리에서는 순서가 중요함.
        viewModelProvider();
        buildRecyclerView();
        getAddressList(); // 서버 데이터 가져오기
        setButtons();
    }
 
    private void buildRecyclerView() {
        mRecyclerView = binding.rvAddress;
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(mContext);
 
        //mAdapter = new AddressViewAdapter(mContext,searchItemList); // 객체 생성
        mAdapter = new AddressListAdapter(mContext, Address_Item.itemCallback); // DiffUtil을 넣은 어댑터를 생성
        //mAdapter.submitList(searchItemList); // 서버에서 데이터를 가져온 이 후 코드에 추가하면 된다.
 
        DividerItemDecoration decoration = new DividerItemDecoration(mContext,manager.getOrientation());
        mRecyclerView.addItemDecoration(decoration);
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(mAdapter);
 
        mAdapter.setOnItemClickListener(this);
    }
 
    private void viewModelProvider() {
        // viewModel init
        viewModel = new ViewModelProvider(this).get(AddressViewModel.class);
 
        // viewModel observe
        viewModel.get_liveData().observe(thisnew Observer<AddressResult>() {
            @Override
            public void onChanged(AddressResult addressResult) {
                if(addressResult.getStatus().contains("success")){
                    addressItemList.clear(); // 서버에서 가져온 데이터 초기화 (전체 List)
                    searchItemList.clear(); // 서버에서 가져온 데이터 초기화 (검색 List)
 
                    for(Address_Item item : addressResult.getAddrinfo()){
                        addressItemList.add(item);
                        searchItemList.add(item);
                    }
 
                    Log.e(TAG, "viewModel observe");
 
                    mAdapter.submitList(searchItemList);
 
                } else {
                    Utils.showAlert(mContext,addressResult.getStatus(),addressResult.getMessage());
                }
            }
        });
 
    }
 
    private void getAddressList() {
        String search = "";
        viewModel.getAllAddressData(search);
    }
 
    private void setButtons(){
        isCheckFlag = false;
        constraintLayout = binding.listviewConstraint2;
 
        binding.search.setSubmitButtonEnabled(false);
        binding.search.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                // 문자열 입력을 완료했을 때 문자열 반환
                // 쿼리가 비어있을 때에는 호출되지 않는다.
                //viewModel.getAllAddressData(query); // 서버 재접속 후 데이터 가져오기
                return false;
            }
 
            @Override
            public boolean onQueryTextChange(String newText) {
                // 문자열이 변할 때마다 바로바로 문자열 반환
                filter(newText); // 전체 데이터 가져온 후 로컬에서 검색 처리
//                if(newText.trim().length() == 0) {
//                    viewModel.getAllAddressData("");
//                }
                return true;
            }
        });
 
        if (isCheckFlag == false) {
            constraintLayout.setVisibility(View.GONE);
        } else if (isCheckFlag == true) {
            constraintLayout.setVisibility(View.VISIBLE);
        }
 
        // all checkbox
        checkAll = binding.lvCheckboxAll;
        binding.lvCheckboxAll.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (checkAll.isChecked() == true) {
                mAdapter.selectAll(searchItemList.size());
            } else {
                mAdapter.unselectall(searchItemList.size());
            }
        });
 
        binding.btnCancel.setOnClickListener(v -> {
            isCheckFlag = false;
            constraintLayout.setVisibility(View.GONE);
            mAdapter.unselectall(searchItemList.size());
            checkAll.setChecked(false); // 전체 선택 체크박스 해제
        });
    }
 
    // Filter Class
    public void filter(String charText) {
        charText = charText.toLowerCase(Locale.getDefault());
        searchItemList.clear();
        if (charText.length() == 0) {
            searchItemList.addAll(addressItemList);
        } else {
            for (Address_Item wp : addressItemList) {
                if(Utils.isNumber(charText)){ // 숫자여부 체크
                    if(wp.getMobileNO().contains(charText) || wp.getTelNO().contains(charText)){
                        // 휴대폰번호 또는 사무실번호에 숫자가 포함되어 있으면
                        searchItemList.add(wp);
                    }
                } else {
                    String iniName = HangulUtils.getHangulInitialSound(wp.getUserNM(), charText);
                    if (iniName.indexOf(charText) >= 0) { // 초성검색어가 있으면 해당 데이터 리스트에 추가
                        searchItemList.add(wp);
                    } else if (wp.getUserNM().toLowerCase(Locale.getDefault()).contains(charText)) {
                        searchItemList.add(wp);
                    }
                }
            }
        }
        mAdapter.notifyDataSetChanged();
    }
 
    @Override
    public void onItemClicked(Address_Item item, int position) {
        Toast.makeText(mContext, "position : "+position + " clieck item name : "+item.getUserNM(), Toast.LENGTH_SHORT).show();
    }
}
 

 

Github Source Code : https://github.com/jsk005/JavaProjects/tree/master/recyclerview_mvvm

 

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

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

github.com

 

블로그 이미지

Link2Me

,
728x90

React 에서 RSA 암호화를 하여 패스워드를 Back-End(PHP)로 전송하고, PHP에서 복호화 처리를 해봤다.

npm install jsencrypt@3.2.1

로 모듈을 추가하고 아래와 같이 코드를 전송하면 된다.

jsencrypt 버전은 3.2.1 에서 정상동작함을 확인했다.

import {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import axios from "axios";
import { JSEncrypt } from "jsencrypt"// npm install jsencrypt
 
const Login = () => {
    // PHP 를 이용하여 Web에서 생성한 공개키 : 실제 길이는 훨씬 더 길다.
    const publicKey = `MIGfMA0GCSqGSIb3DQEBED/dKAJT7vdgzXooBzzTyUHwVJGyA1mKYDCIyU5/IQIDAQAB`;
    const encrypt = new JSEncrypt(); // Start our encryptor.
    encrypt.setPublicKey(publicKey); // Assign our encryptor to utilize the public key.
 
    const navigate = useNavigate();
    const initData = {
        userID: '',
        password: ''
    }
 
    const [formData, setFormData] = useState(initData);
    const [users, setUsers] = useState([]);
    // 첫번째 원소 : 현재 상태, 두번재 원소 : 상태를 바꾸어 주는 함수
    const [error, setError] = useState(null);
 
    const onChangeInput = (e) => {
        setFormData({
            ...formData,
            [e.target.name]: e.target.value
        });
        // e.target.name 은 해당 input의 name을 가리킨다.
        //console.log(formData);
    }
 
    const submitForm = (e) => {
        e.preventDefault();
        const params = {
            userID: formData.userID,
            password: encrypt.encrypt(formData.password)
        }
        console.log(params);
        axios.post('/reactSample/loginChk.php', params)
            .then((res) => {
                console.log(res);
                if (res.status === 200 && res.data.status === 'success') {
                    setUsers(res.data.userinfo);
                    navigate('/');
                } else {
                    console.log(res.data.message);
                    setError(true);
                }
            })
            .catch(error => {
                console.log(error.response)
            });
    }
 
    return (
        <div className="container h-100 mt-5">
            <div className="row d-flex justify-content-center align-items-center h-100">
                <div className="col-12 col-md-9 col-lg-7 col-xl-6">
                    <div className="card">
                        <div className="card-body p-5">
                            <h2 className="text-uppercase text-center mb-5">Sign In</h2>
                            <form onSubmit={submitForm}>
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="userID">userID</label>
                                    <input type="text" name="userID" onChange={onChangeInput} id="userID"
                                           className="form-control form-control-lg" value={formData.userID}
                                           required/>
                                </div>
 
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="password">Password</label>
                                    <input type="password" name="password" onChange={onChangeInput} id="password"
                                           className="form-control form-control-lg" value={formData.password}
                                           required/>
                                </div>
 
                                { error &&
                                <div className="alert alert-danger" role="alert">
                                    로그인 정보를 확인하세요!
                                </div>
                                }
 
                                <div className="d-flex justify-content-center">
                                    <button type="submit"
                                            className="btn btn-primary btn-block btn-lg gradient-custom-3 text-body">로그인
                                    </button>
                                </div>
 
                                <p className="text-center text-muted mt-5 mb-0">
                                    <Link to="/register" className="fw-bold text-body"><u>회원가입</u></Link>
                                </p>
 
                            </form>
 
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}
 
export default Login;
cs

 

 

PHP 에서 RSA 공개키 얻는 방법

 
<?php
header("Cache-Control: no-cache, must-revalidate");
header("Content-type: application/json; charset=UTF-8");
 
$pubkey = get_publickey();
echo $pubkey;
 
function get_publickey() {
    // 경로 : 절대경로로 설정 필요
    $rsakeyfile = '/home/rsa/key/rsa_pub.pem';
    
    $fp = fopen($rsakeyfile,"r");
    $key = "";
    while(!feof($fp)) {
        $key .= fgets($fp,4096);
    } 
    fclose($fp);
 
    $key = preg_replace('/\r\n|\r|\n/','',$key);
    $key = str_replace('-----BEGIN PUBLIC KEY-----','',$key);
    $key = str_replace('-----END PUBLIC KEY-----','',$key);
    return $key;
 
/*
CentOS 리눅스 에서 RSA 공개키, 개인키 생성 방법
mkdir -p /home/rsa/key/
cd /home/rsa/key/
 
# Private Key 생성
openssl genrsa -out rsa_pri.pem 1024
 
# Public Key 생성
openssl rsa -pubout -in rsa_pri.pem -out rsa_pub.pem
 
*/
?>

 

 

PHP loginChk.php

- RSA 복호화 처리 함수는 LoginClass.php 파일에 포함되어 있다.

<?php
// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST&& $_SERVER['REQUEST_METHOD'== "POST"){
    $_POST = json_decode(file_get_contents("php://input"),true); 
    @extract($_POST); // $_POST['loginID'] 라고 쓰지 않고, $loginID 라고 써도 인식되게 함
    if(isset($userID&& !empty($userID&& isset($password&& !empty($password)) {
        require_once 'phpclass/config.php';
        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");
 
        $password = $c->rsa_decrypt($password);
 
        $user = $c->getUser($userID$password);
        if($user['idx'== 1){ // 로그인 정보가 일치하면
            $_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;
        }
 
        $result = array(
            'status' => $status,
            'message' => $message,
            'userinfo' => $userinfo
        );
        echo json_encode($result);
 
    }
else { // 비정상적인 접속인 경우
    echo 0// loginChk.php 파일을 직접 실행할 경우에는 화면에 0을 찍어준다.
    exit;
}
?>
 

 

 

 

'React > React-PHP' 카테고리의 다른 글

React + PHP Login 처리  (0) 2022.08.27
React useEffect example (php REST API)  (0) 2022.08.22
React - PHP 간 연동 입문기  (0) 2022.08.20
블로그 이미지

Link2Me

,
728x90

React 에서 로그인 처리하는 걸 테스트하고 적어둔다.

import {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import axios from "axios";
 
const Login = () => {
    const navigate = useNavigate();
    const initData = {
        status: '',
        message: '',
        userinfo: {
            userNM: '',
            mobileNO: '',
            profileImg: ''
        }
    }
 
    const [formData, setFormData] = useState(initData);
    const [users, setUsers] = useState([]);
    // 첫번째 원소 : 현재 상태, 두번재 원소 : 상태를 바꾸어 주는 함수
    const [error, setError] = useState(null);
 
    const onChangeInput = (e) => {
        setFormData({
            ...formData,
            [e.target.name]: e.target.value
        });
        // e.target.name 은 해당 input의 name을 가리킨다.
        //console.log(formData);
    }
 
    const submitForm = (e) => {
        e.preventDefault();
        const params = {
            userID: formData.userID,
            password: formData.password
        }
        console.log(params);
        axios.post('/reactSample/loginChk.php', params)
            .then((res) => {
                console.log(res);
                if (res.status === 200 && res.data.status === 'success') {
                    setUsers(res.data.userinfo);
                    navigate('/');
                } else {
                    console.log(res.data.message);
                    setError(true);
                }
            })
            .catch(error => {
                console.log(error.response)
            });
    }
 
    return (
        <div className="container h-100 mt-5">
            <div className="row d-flex justify-content-center align-items-center h-100">
                <div className="col-12 col-md-9 col-lg-7 col-xl-6">
                    <div className="card">
                        <div className="card-body p-5">
                            <h2 className="text-uppercase text-center mb-5">Sign In</h2>
                            <form onSubmit={submitForm}>
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="userID">userID</label>
                                    <input type="text" name="userID" onChange={onChangeInput} id="userID"
                                           className="form-control form-control-lg" value={formData.userID}
                                           required/>
                                </div>
 
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="password">Password</label>
                                    <input type="password" name="password" onChange={onChangeInput} id="password"
                                           className="form-control form-control-lg" value={formData.password}
                                           required/>
                                </div>
 
                                { error &&
                                <div className="alert alert-danger" role="alert">
                                    로그인 정보를 확인하세요!
                                </div>
                                }
 
                                <div className="d-flex justify-content-center">
                                    <button type="submit"
                                            className="btn btn-primary btn-block btn-lg gradient-custom-3 text-body">로그인
                                    </button>
                                </div>
 
                                <p className="text-center text-muted mt-5 mb-0">
                                    <Link to="/register" className="fw-bold text-body"><u>회원가입</u></Link>
                                </p>
 
                            </form>
 
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}
 
export default Login;

간혹 보면 "아이디가 존재하지 않는다", "비밀번호가 틀렸다" 라는 문구를 출력하는 경우가 있는데 이런 경우 해커에게 빌미를 제공하는 것이다. 아이디가 있건 없건, 비밀번호가 틀렸든 간에 "로그인 정보가 일치하지 않는다"는 문구 하나로 처리해야 안전하다. (모의 해킹 시 지적사항)

 

Login.js
0.00MB

 

 

아직 전역 상태관리 처리까지는 못해봤다.

전역 상태관리 기준으로 테스트하게되면 업데이트할 예정이다.

 

PHP 로그인 처리 코드

- PHP 코드 결과로 만들어진 JSON 구조를 토대로 useState 초기값 구조를 작성한다.

<?php
// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST&& $_SERVER['REQUEST_METHOD'== "POST"){
    $_POST = json_decode(file_get_contents("php://input"),true); 
    @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");
 
        $user = $c->getUser($userID$password);
        if($user['idx'== 1){ // 로그인 정보가 일치하면
            $_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;
        }
 
        $result = array(
            'status' => $status,
            'message' => $message,
            'userinfo' => $userinfo
        );
        echo json_encode($result);
    }
else { // 비정상적인 접속인 경우
    echo 0// loginChk.php 파일을 직접 실행할 경우에는 화면에 0을 찍어준다.
    exit;
}
?>
 

 

 

 

'React > React-PHP' 카테고리의 다른 글

RSA Encryption Login in React.js and PHP  (0) 2022.08.27
React useEffect example (php REST API)  (0) 2022.08.22
React - PHP 간 연동 입문기  (0) 2022.08.20
블로그 이미지

Link2Me

,
728x90

코틀린 기반으로 코드를 구현하면 쉽게 샘플 예제를 찾을 수 있다. 하지만 Java 코드 기반 자료는 찾기가 쉽지 않다.

제일 빠르게 찾는 방법은 유투브 동영상에서 검색하는 것이다.

https://www.youtube.com/watch?v=xPPMygGxiEo 

이 동영상 강좌 시청하면서 링크된 Github 샘플 자료를 참고하면 이해하는데 훨씬 빠를 것이다.

 

 

ListAdapter는 RecyclerView에 쓸 수 있는 어댑터이다.
기존의 어댑터와는 다르게 DiffUtil을 사용하여 비동기식 처리를 할 수 있다.
기존의 기본 어댑터는 mAdapter.notifyDataSetChanged(); 메소드를 사용하여 리스트의 변경처리를 했었는데, 
ListAdapter에서는 submitList(...) 를 사용하여 리스트가 변경되었음을 어댑터에게 알려줄 수 있다.

 

DiffUtil 클래스는 oldItem, newItem의 두 데이터셋을 비교하여 값이 변경된 부분만을 RecyclerView에게 알려줄 수 있다.

비교 결과, 다르다고 판단된 아이템 항목만 교체를 진행한다. 이것이 중요한 포인트이다.

 

아래 샘플 예제는 MVVM 패턴을 고려하여 작성한 코드는 아니며, 내용 숙지 차원에서 수정 부분 중심으로 적어둔다.

 

Java 코드로 구현한 Class

- 일단 코드가 참 길다.

- DiffUtil.ItemCallback 메소드 구현을 이곳에 하는 예제도 있고 RecyclerViewApdater 에 구현한 예제도 있다.

import android.graphics.Movie;
 
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
 
import java.util.Objects;
 
public class Address_Item {
    private String idx;
    private String userNM;
    private String mobileNO;
    private String telNO;
    private String photo;
    private Boolean checkBoxState;
 
    public Address_Item(String idx, String userNM, String mobileNO, String telNO, String photo, Boolean checkBoxState) {
        this.idx = idx;
        this.userNM = userNM;
        this.mobileNO = mobileNO;
        this.telNO = telNO;
        this.photo = photo;
        this.checkBoxState = checkBoxState;
    }
 
    public String getIdx() {
        return idx;
    }
 
    public void setIdx(String idx) {
        this.idx = idx;
    }
 
    public String getUserNM() {
        return userNM;
    }
 
    public void setUserNM(String userNM) {
        this.userNM = userNM;
    }
 
    public String getMobileNO() {
        return mobileNO;
    }
 
    public void setMobileNO(String mobileNO) {
        this.mobileNO = mobileNO;
    }
 
    public String getTelNO() {
        return telNO;
    }
 
    public void setTelNO(String telNO) {
        this.telNO = telNO;
    }
 
    public String getPhoto() {
        return photo;
    }
 
    public void setPhoto(String photo) {
        this.photo = photo;
    }
 
    public Boolean getCheckBoxState() {
        return checkBoxState;
    }
 
    public void setCheckBoxState(Boolean checkBoxState) {
        this.checkBoxState = checkBoxState;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address_Item that = (Address_Item) o;
        return Objects.equals(idx, that.idx) && Objects.equals(userNM, that.userNM) && Objects.equals(mobileNO, that.mobileNO) && Objects.equals(telNO, that.telNO) && Objects.equals(photo, that.photo) && Objects.equals(checkBoxState, that.checkBoxState);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(idx, userNM, mobileNO, telNO, photo, checkBoxState);
    }
 
    public static DiffUtil.ItemCallback<Address_Item> itemCallback = new DiffUtil.ItemCallback<Address_Item>() {
        @Override
        public boolean areItemsTheSame(@NonNull Address_Item oldItem, @NonNull Address_Item newItem) {
            return oldItem.getIdx().equals(newItem.getIdx());
        }
 
        @Override
        public boolean areContentsTheSame(@NonNull Address_Item oldItem, @NonNull Address_Item newItem) {
            return oldItem.equals(newItem);
        }
    };
}
 

 

코드가 너무 긴 관계로 Java 에서 코틀린으로 코드를 변환시켰다.

그 다음에 var 변수 초기화 부분의 코드를 약간 수정했다.

이것은 Java 로 구현된 코드와 혼용 사용하는 Class 이다.

import android.os.Parcelable
import androidx.recyclerview.widget.DiffUtil
import kotlinx.parcelize.Parcelize
import java.util.*
 
@Parcelize
class Address_Item(
    // 결과를 받을 모델 (ArrayList 에 저장하므로 val 로 선언하면 안된다)
    // 서버 SQL의 칼럼명과 일치하게 작성해야 한다.
    var idx: String="",
    var userNM: String="",
    var mobileNO: String?=null,
    var telNO: String?=null,
    var photo: String?=null,
    var checkBoxState: Boolean
) : Parcelable {
    override fun equals(o: Any?): Boolean {
        if (this === o) return true
        if (o == null || javaClass != o.javaClass) return false
        val that = o as Address_Item
        return idx == that.idx && userNM == that.userNM && mobileNO == that.mobileNO && telNO == that.telNO && photo == that.photo && checkBoxState == that.checkBoxState
    }
 
    override fun hashCode(): Int {
        return Objects.hash(idx, userNM, mobileNO, telNO, photo, checkBoxState)
    }
 
    companion object {
        @JvmField
        var itemCallback: DiffUtil.ItemCallback<Address_Item> =
            object : DiffUtil.ItemCallback<Address_Item>() {
                override fun areItemsTheSame(oldItem: Address_Item, newItem: Address_Item): Boolean {
                    return oldItem.idx == newItem.idx
                }
 
                override fun areContentsTheSame(oldItem: Address_Item, newItem: Address_Item): Boolean {
                    return oldItem == newItem
                }
            }
    }
}

 

 

AddressActivity.java

- 기존 파일에서 객체를 생성하는 코드는 주석처리했다.

- Adpater 로 데이터를 전달하는 부분은 mAdpater.submitList(전달변수);

  이 코드는 서버에서 자료를 가져온 후 데이터를 넘기는 곳에 적어주면 된다.

   private void buildRecyclerView() {
        mRecyclerView = binding.rvAddress;
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(mContext);
 
        //mAdapter = new AddressViewAdapter(mContext,searchItemList); // 객체 생성
        mAdapter = new AddressListAdapter(mContext, Address_Item.itemCallback); // DiffUtil을 넣은 어댑터를 생성
        //mAdapter.submitList(searchItemList); // 서버에서 데이터를 가져온 이 후 코드에 추가하면 된다.
 
        DividerItemDecoration decoration = new DividerItemDecoration(mContext,manager.getOrientation());
        mRecyclerView.addItemDecoration(decoration);
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(mAdapter);
 
        mAdapter.setOnItemClickListener(this);
    }

 

 

AddressListAdapter.java

- 기존 RecyclerView.Adapter 대신에 ListAdapter<T, VH> 를 상속한다.

- RecyclerView 가 화면에 표시할 데이터 타입 Address_Item 을 추가한다.

- 이렇게 변경하고 나면 생성자 부분이 에러가 발생한다. 자동으로 추가하면 protected 생성되는데 public 으로 변경하고 DiffUtil.ItemCallback 코드는 이곳에서 추가 구현해도 되고, Address_Item Class 에 추가 구현한 것을 사용해도 된다.

- rvItemList.get 대신에 getItem 으로 변경한다.

- getItemCount() 메소드는 삭제한다.

import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
 
// RecyclerView.Adapter 대신에 ListAdapter<T, VH> 를 상속한다.
// T는 리사이클러뷰가 화면에 표시할 데이터의 타입이다.
public class AddressListAdapter extends ListAdapter<Address_Item, AddressListAdapter.ViewHolder> {
    private final String TAG = this.getClass().getSimpleName();
    Context context;
 
    public AddressListAdapter(Context context, @NonNull DiffUtil.ItemCallback<Address_Item> diffCallback) {
        super(diffCallback);
        this.context = context;
    }
 
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ItemAddressBinding binding = ItemAddressBinding.inflate(LayoutInflater.from(parent.getContext()),parent,false);
        return new ViewHolder(binding);
    }
 
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        // 실제로 데이터를 표시하는 부분
        //Address_Item currentItem = rvItemList.get(position);
        Address_Item currentItem = getItem(position);
        holder.bindItem(currentItem, position);
    }
 
    public class ViewHolder extends RecyclerView.ViewHolder{
        ItemAddressBinding itemBinding;
 
        public ViewHolder(@NonNull ItemAddressBinding binding) {
            super(binding.getRoot());
            itemBinding = binding;
        }
 
        void bindItem(Address_Item item, int position){
        }
    }
 
}
 

 

 

 

블로그 이미지

Link2Me

,
728x90

json 데이터 구조

{
  "result": [
    {
      "idx"1,
      "userNM""개발자",
      "mobileNO""01000010001",
      "telNO""0234560001",
      "photo""1.jpg"
    },
    {
      "idx"2,
      "userNM""이정은",
      "mobileNO""01001230001",
      "telNO""",
      "photo""2.jpg"
    },
    {
      "idx"3,
      "userNM""김홍길",
      "mobileNO""01001230002",
      "telNO""",
      "photo"""
    },
    {
      "idx"4,
      "userNM""최신형",
      "mobileNO""01001230003",
      "telNO""",
      "photo""4.jpg"
    },
  ]
}

 

React useEffect 함수

const initData = {
    result: {
        idx: 0,
        userNM: '',
        mobileNO: '',
        telNO: '',
        photo: ''
   }
}
const [arrayData, setArrayData] = useState(initData);
 
useEffect( () => {
    const params = {
        idx: 1
    }
    axios.post('http://localhost/reactSample/getContact.php', params)
        .then(res => {
            //console.log(res);
            if (res.status === 200) {
                // console.log(res.data.result);
                setArrayData(res.data.result);
            }
        })
        .catch(error => {
            console.log(error.response)
        });
}, []);
 

구글링을 하면 axios post 방식으로 params 를 넘기는 방식이 다양하게 나오는데 테스트를 해보니 안되더라.

위와 같이 하면 된다.

PHP 에서는 변수를 $data['idx'] 로 인식되더라.

유투브 강의 자료 따라서 할 때 변수를 잘못 인식할 수도 있으니 반드시 테스트가 필요하다.

 

 

npm install 및 import

//npm install --save axios
 
import {useEffect, useState} from "react";
import axios from "axios";

 

 

실전형 REST API 예제

- React 에서 전달받은 데이터를 처리하는 PHP 파일

<?php
//header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: access");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Max-Age: 3600");
//header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
 
$data = json_decode(file_get_contents("php://input"),true);
 
require_once 'phpclass/config.php';
require_once 'phpclass/dbconnect.php';
require_once 'phpclass/loginClass.php';
$c = new LoginClass();
 
//키워드 확인
if(!isset($data['keyword']) || empty($data['keyword'])){
    $result = array(
        'status' => "fail",
        'message' => "no keyword",
        'addrinfo' => null
    );
    echo json_encode($result);
    exit;
}
 
$keyword=$data['keyword'];
//키워드 일치 확인
if(strcmp($keyword,$mykey)<>0){
    $result = array(
        'status' => "keyfail",
        'message' => "서버와 단말의 KEY가 일치하지 않습니다.",
        'addrinfo' => null
    );
    echo json_encode($result);
    exit;
}
 
 
$column ="idx,userNM,mobileNO,telNO,photo";
$sql = "select idx,userNM,mobileNO,telNO,photo from Person";
if(!empty($search)) {
    $where = "userNM LIKE '%".$search."%' or mobileNO LIKE '%".$search."%'";
else {
    $where = "";
}
 
if(strlen($where> 0){
    $sql .= " where ".$where;
}
 
$R = array(); // 결과 담을 변수 생성
$result = $c->putDbArray($sql);
while($row = $result->fetch_assoc()) {
    $path = "./photos/".$row['photo'];
    if(!file_exists($path)) {
        $row['photo'= "";
    } else {
        $row['photo'];
    }
    array_push($R$row);
}
header("Cache-Control: no-cache, must-revalidate");
header("Content-type: application/json; charset=UTF-8");
 
$status = "success";
$message = "";
$addrinfo = $R// 전체 ArrayList 데이터
 
$result = array(
    'status' => $status,
    'message' => $message,
    'addrinfo' => $addrinfo
);
echo json_encode($result);//배열-문자열등을 json형식의 '문자열'로 변환
?>
 
cs

 

위와 같은 JSON 데이터를 처리하기 위한 React POST 전송 코드 구현 사항

import {useEffect, useState} from "react";
import axios from "axios";
import moment from 'moment';
import * as config from './config';
 
const ListData = () => {
    const today = moment().format('YYYYMMDD');
    const initData = {
        status:'',
        message:'',
        addrinfo: {
            idx: 0,
            userNM: '',
            mobileNO: '',
            telNO: '',
            photo: ''
       }
    }
    const [arrayData, setArrayData] = useState(initData);
 
    useEffect( () => {
        const params = {
            keyword: config.URLKEY + today,
        }
        axios.post('http://localhost/reactSample/getContact.php', params)
            .then(res => {
                //console.log(res);
                if (res.status === 200 && res.data.status === 'success') {
                    console.log(res.data.addrinfo);
                    setArrayData(res.data.addrinfo);
                } else {
                    console.log(res.data.message);
                }
            })
            .catch(error => {
                console.log(error.response)
            });
    }, []);
 
    // 화면 UI 구성은 생략
    return (
        <div className="container h-100 mt-5">
        </div>
    )
}
 
export default ListData;

 

모듈 설치 사항

npm install --save axios
npm install moment --save

 

 

주의사항

Android APP 과 PHP 간에 주고받은 PHP 파일 형식과 React 대응 PHP 파일 형식이 다르다.

Android/jQuery POST 변수 인식 방법은

if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){

}

로 하면 GET 방식으로 실행하면 내용을 확인할 수가 없다.

그런데 React 대응 PHP 코드는 GET 방식으로도 실행이 된다.

그러므로 keyword 또는 session 처리를 잘 구현해서 PHP 코드를 작성해야 할 것 같다.

 

ListData.js
0.00MB

'React > React-PHP' 카테고리의 다른 글

RSA Encryption Login in React.js and PHP  (0) 2022.08.27
React + PHP Login 처리  (0) 2022.08.27
React - PHP 간 연동 입문기  (0) 2022.08.20
블로그 이미지

Link2Me

,
728x90

https://react-bootstrap.github.io/getting-started/introduction/

 

React-Bootstrap

The most popular front-end framework, rebuilt for React.

react-bootstrap.github.io

에 설명된 것처럼

npm install react-bootstrap bootstrap

으로 모듈을 추가하고, App.js 또는 index.js 에

import 'bootstrap/dist/css/bootstrap.min.css';

를 추가한다.

 

Header.js 파일을 아래와 같이 하고 테스를 했더니 PC화면 사이즈에서는 속도 문제는 없다.

Navbar.Offcanvas 기능이 활성화되는 스마트폰 크기로 되었을 때 Menu 반응속도가 너무 느리다.

import Button from 'react-bootstrap/Button';
import Container from 'react-bootstrap/Container';
import Form from 'react-bootstrap/Form';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import NavDropdown from 'react-bootstrap/NavDropdown';
import Offcanvas from 'react-bootstrap/Offcanvas';
 
const Header = () => {
    return (
        <>
            <Navbar bg="primary" expand='sm' className="mb-3">
                <Container fluid>
                    <Navbar.Brand href="/">Navbar</Navbar.Brand>
                    <Navbar.Toggle aria-controls='navbar-expand-sm'/>
                    <Navbar.Offcanvas id='navbar-expand-sm' aria-labelledby='navbar-expand-sm' placement="end">
                        <Offcanvas.Header closeButton>
                            <Offcanvas.Title id='navbarLabel-expand-sm'>
                                홍길동
                            </Offcanvas.Title>
                        </Offcanvas.Header>
                        <Offcanvas.Body>
                            <Nav className="justify-content-end flex-grow-1 pe-3">
                                <Nav.Link href="/">Home</Nav.Link>
                                <Nav.Link href="/login">Login</Nav.Link>
                                <Nav.Link href="/register">Sign Up</Nav.Link>
                                <NavDropdown title="Dropdown" id='navbarDropdown-expand-sm'>
                                    <NavDropdown.Item href="/login">로그인</NavDropdown.Item>
                                    <NavDropdown.Item href="/register">회원가입</NavDropdown.Item>
                                    <NavDropdown.Divider/>
                                    <NavDropdown.Item href="#action5">
                                        Something else here
                                    </NavDropdown.Item>
                                </NavDropdown>
                            </Nav>
                        </Offcanvas.Body>
                    </Navbar.Offcanvas>
                </Container>
            </Navbar>
        </>
    )
}
 
export default Header;

 

Navbar.Offcanva 기능이 없는 단순 형태 구성 코드는 속도 문제 없었다.

import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
 
const Header = () => {
    return (
        <>
            <Navbar bg="primary" variant="dark">
                <Container fluid>
                    <Navbar.Brand href="/">Navbar</Navbar.Brand>
                    <Navbar.Toggle aria-controls="responsive-navbar-nav"/>
                    <Navbar.Collapse id="responsive-navbar-nav">
                        <Nav className="me-auto">
                            <Nav.Link href="/">Home</Nav.Link>
                            <Nav.Link href="/register">Register</Nav.Link>
                            <Nav.Link href="/login">Login</Nav.Link>
                        </Nav>
                    </Navbar.Collapse>
                </Container>
            </Navbar>
        </>
    )
}
 
export default Header;

Layout 구성하는 것에 대해서 좀 더 다양한 테스트를 해봐야 알 수 있을 듯 싶다.

 

'React > React' 카테고리의 다른 글

React useRef 사용 예제  (0) 2022.10.15
React useReducer  (0) 2022.10.13
React CORS error 해결 방법  (0) 2022.08.20
React Query String  (0) 2022.06.08
React Router v6  (0) 2022.06.07
블로그 이미지

Link2Me

,
728x90

React 가 대세라서 React 학습을 해보는 중이다.

인프런 사이트 강좌도 들어보고 fastcampus 강좌도 들어보고 있다. 사이트마다 장단점이 있는 거 같다.

전체적인 개념을 잡으면서 하는 것은 fastcampus 강좌가 좋은데, 최근에 변경된 것을 반영하지 않은 것이 있다보니 따라하기 실습을 해보면 잘 동작되지 않아서 진도 나기가기 쉽지 않아 중도에 멈춤 상태이다.

React 동영상 강좌를 가급적이면 2022년에 나온 걸 중심으로 학습하는게 정신 건강에 좋은 듯 싶다.

 

Back-End는 지금까지 배워왔던 PHP 로 해보는게 좋을 듯 싶어서 유투브 동영상 강좌를 검색하니 인도 개발자가 올린 동영상 강좌가 괜찮은 거 같아서 따라서 해보기로 하고 어제 동영상을 멈춰가면서 따라하기를 해보았다. 아쉽게도 소스코드를 Github 에 공개하지 않아서 일일이 타이핑을 하는 수고를 해야 한다.

https://www.youtube.com/watch?v=nmCeSPGcBnY 

 

프로젝트 생성 : npx create-react-app react-php

프로젝트로 이동 : cd react-php

 

모듈 추가

npm install --save bootstrap
npm install --save react-router-dom@6
npm install --save axios

 

동영상 강좌에서 bootsrap 모듈을 추가하라고 나오는 건 없다. 아직 bootstrap 모듈 추가한 것에 대한 기능을 제대로 사용할 줄 모른다. 나중에 사용법을 익힐 것을 감안하고 모듈 추가를 해두었다.

 

구글에서 boostrap cdn 을 입력하고 css 버튼을 눌러 하단의 HTML 코드를 복사하여 public 폴더 index.html 파일 헤더 부분에 붙여넣기 한다.

 

App.js

회원 가입과 로그인 기능을 보여주는 메인화면 구성을 하기 위한 App.js 파일 구성이다.

import "bootstrap/dist/css/bootstrap.min.css"
import './App.css';
import Header from "./Header";
import {BrowserRouter, Routes, Route} from "react-router-dom";
import Home from "./Home";
import Login from "./Login";
import Register from "./Register";
 
function App() {
    return (
        <BrowserRouter>
            <div className="container">
                <Header/>
                <Routes>
                    <Route path="/" element={<Home/>}/>
                    <Route path="/login" element={<Login/>}/>
                    <Route path="/register" element={<Register/>}/>
                </Routes>
            </div>
        </BrowserRouter>
    );
}
 
export default App;

 

Header.js

header.js 구성을 위해서 구글에서 bootstrap navbar 검색하고 Navbar text with an inline element 복사하여
Header.js 에 붙여넣기 한다. Webstorm에서 class 는 자동으로 className 으로 변경해 주더라.

동영상 보면 Visual Studio Code(vscode)에서 class 자동 변경은 안되는거 같더라.

import {BrowserRouter, Routes, Route, Link} from "react-router-dom";
const Header = () => {
    return (
        <nav className="navbar navbar-expand-lg bg-light">
            <div className="container-fluid">
                <a className="navbar-brand" href="#">Navbar w/ text</a>
                <button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
                        aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
                    <span className="navbar-toggler-icon"></span>
                </button>
                <div className="collapse navbar-collapse" id="navbarText">
                    <ul className="navbar-nav me-auto mb-2 mb-lg-0">
                        <li className="nav-item">
                            {/*<a className="nav-link active" aria-current="page" href="#">Home</a>*/}
                            <Link to="/" className="nav-link active">Home</Link>
                        </li>
                        <li className="nav-item">
                            <Link to="/register" className="nav-link">Register</Link>
                        </li>
                        <li className="nav-item">
                            <Link to="/login" className="nav-link">Login</Link>
                        </li>
                    </ul>
                    <span className="navbar-text">
                    </span>
                </div>
            </div>
        </nav>
    )
}
 
export default Header;

 

 

<a className="nav-link active" aria-current="page" href="#">Home</a>

부분을

<Link to="/" className="nav-link active">Home</Link>

로 변경하면 된다.

 

이 코드로 테스트를 해보니 PC화면에서는 버튼 기능이 잘 동작되는데 모바일 크기 사이즈로 화면을 축소하면 메뉴 선택하는 부분이 동작되지 않는 현상이 있다.

단순하게 bootstrap 5 코드를 React 로 변경만 했기 때문에 생기는 문제점으로 Navbar 가 제대로 동작되지 않는 듯 하다.

이 부분은 bootstrap 모듈 추가한 방식으로 다시 한번 테스트를 해볼 예정이다.

지금은 React - PHP 간 연동 처리하는 부분이 중점이라 그 부분에 치중하고 글을 작성해두는 것이다.

 

 

Home.js

const Home = () => {
    return (
        <div>
            Welcome to Home page
        </div>
    )
}
export default Home;

이 코드는 형태만 보여주는 수준이다.

Login.js 파일도 이와 같은 형태라서 보여주는 건 생략한다.

 

Register.js

구글에서 bootstrap form 으로 검색하면 https://mdbootstrap.com/docs/standard/forms/overview/ 가 검색된다.

여기서 적당한 것을 복사하여 붙여넣기를 하고 불필요한 부분은 수정한다.

제공된 기본 Form 에서 name, 상태변화값 onChangeInput, value 를 추가하고 id 값도 알기 쉽게 변경한다.

아래와 같이 코드를 작성하면 React 버전 회원가입 기본 폼이 완성된다.

PHP 로 submit 결과를 전송하기 위해서 axio.post 로 전달하고 결과를 돌려받도록 코드를 작성한다.

이 부분이 jQuery ajax 로 전송하는 것과 좀 다르다.

import {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import axios from "axios";
 
const Register = () => {
    const navigate = useNavigate();
    const [formData, setFormData] = useState({
        userNM: "",
        email: "",
        password: ""
    })
 
    const onChangeInput = (e) => {
        setFormData({
            ...formData,
            [e.target.name]: e.target.value
        });
        console.log(formData);
    }
 
    const submitForm = (e) => {
        e.preventDefault();
        const sendData = {
            userNM: formData.userNM,
            email: formData.email,
            password: formData.password
        }
        console.log(sendData);
        axios.post('http://localhost/php-react/insert.php', sendData)
            .then((res) => {
                console.log(res.data);
                navigate('/login');
            })
            .catch(error => {
                console.log(error.response)
            });
    }
 
    return (
        <div className="container h-100 mt-5">
            <div className="row d-flex justify-content-center align-items-center h-100">
                <div className="col-12 col-md-9 col-lg-7 col-xl-6">
                    <div className="card">
                        <div className="card-body p-5">
                            <h2 className="text-uppercase text-center mb-5">Create an account</h2>
                            <form onSubmit={submitForm}>
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="userNM">Your Name</label>
                                    <input type="text" name="userNM" onChange={onChangeInput} id="userNM"
                                           className="form-control form-control-lg" value={formData.userNM}
                                           required/>
                                </div>
 
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="email">Your Email</label>
                                    <input type="email" name="email" onChange={onChangeInput} id="email"
                                           className="form-control form-control-lg" value={formData.email}
                                           required/>
                                </div>
 
                                <div className="form-outline mb-4">
                                    <label className="form-label" htmlFor="password">Password</label>
                                    <input type="password" name="password" onChange={onChangeInput} id="password"
                                           className="form-control form-control-lg" value={formData.password}
                                           required/>
                                </div>
 
                                <div className="d-flex justify-content-center">
                                    <button type="submit"
                                            className="btn btn-success btn-block btn-lg gradient-custom-4 text-body">Register
                                    </button>
                                </div>
 
                                <p className="text-center text-muted mt-5 mb-0">
                                    <Link to="/login" className="fw-bold text-body"><u>로그인</u></Link>
                                </p>
 
                            </form>
 
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}
 
export default Register;

 

PHP DB 연결 코드 dbconnect.php

이 부분은 Legacy PHP 로 DB를 연결하는 방법이며, 내가 즐겨 사용하는 방식 중 하나다.

<?php
$db['server'= 'localhost';
$db['dbname'= 'studydb';
$db['user_name'= 'reactfox';
$db['password'= 'Wonderfull!';
$db['port'= "3306";
 
$db = isConnectDb($db);
 
function isConnectDb($db)
{
    $conn = mysqli_connect($db['server'],$db['user_name'],$db['password'],$db['dbname'],$db['port']);
    mysqli_set_charset($conn"utf8");  // DB설정이 잘못되어 euc-kr 로 되어 있으면 문제가 됨
    if (mysqli_connect_errno()) {
        echo "Failed to connect to MySQL: " . mysqli_connect_error();
        exit;
    } else {
        return $conn;
    }
}
 
/* 사용자 권한 등록
create database studydb default character set utf8;
 
use mysql;
create user reactfox@localhost identified by 'Wonderfull!';
grant all privileges on studydb.* to reactfox@localhost;
flush privileges;
quit
*/
?>
 
 

 

DB를 연결하기 위해 studydb 를 생성하고 DB 접속 권한을 부여하기 위한 걸 해준다.

이제 사용자 정보를 저장하기 위한 초간단 테이블을 구성한다. 이 테이블은 연동 과정을 보여주기 위한 것이지 실제 사용 수준의 테이블 스키마는 아니다.

CREATE TABLE `users` (
  `id` int(11NOT NULL,
  `name` varchar(50CHARACTER SET utf8mb3 NOT NULL,
  `email` varchar(50CHARACTER SET utf8mb3 NOT NULL,
  `password` varchar(200CHARACTER SET utf8mb3 NOT NULL
ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
ALTER TABLE `users`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `email` (`email`);
 
ALTER TABLE `users`
  MODIFY `id` int(11NOT NULL AUTO_INCREMENTAUTO_INCREMENT=1;
COMMIT;

email 칼럼은 UNIQUE KEY 를 지정하여 중복 저장되지 않도록 했다.

 

 

insert.php

아래 코드 상단에 처리하는 기능에 대해서는 다 모른다. 이 부분에서 엄청난 삽질을 하면서 개고생을 좀 했다.

Access-Control-Allow-Headers : 가 된 것이 문제가 되는 줄 몰랐다.

Access-Control-Allow-Headers: 와 같이 공백이 있으면 절대 안되더라. ㅠㅠㅠ

이것 때문에 CORS 에러 메시지 숱하게 보이고 구글링 하면서 별의 별 것을 다 찾아보게 되었다.

덕분에 이 코드의 중요성에 대해서 알게 된 것 정도이다.

윈도우에서 localhost 로 PHP 도 설치하고, React 도 설치해서 동작시키는 것에서는 CORS 에러를 만날 일이 적은 듯 하다. 리눅스 버전을 설치하고 나서 Same IP, Different Port 로 크롬브라우저가 CORS 에러 메시지를 보여주는 걸 해결하기 위해 수많은 삽질 끝에(?) 해결을 하긴 했다. 테스트 해보니 더 코드를 정교하게 할 부분도 있기는 있는 듯 하다.

https://link2me.tistory.com/2228 에 CORS 에러 해결 방법이 있으니 참조하면 된다.

<?php
//header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: access");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Max-Age: 3600");
//header("Access-Control-Allow-Headers: Content-Type, Authorization");
//header("Access-Control-Allow-Headers: Origin, Accept, X-Requested-With, Content-Type, Access-Control-Allow-Headers, Authorization");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
 
$data = json_decode(file_get_contents("php://input"));
 
if(isset($data&& !empty($data)){
 
    require_once 'dbconnect.php';
 
    $name = $data->userNM;
    $email = $data->email;
    $password = $data->password;
    $sql = "INSERT INTO users (name,email,password) VALUES ('$name','$email','$password')";
    if(mysqli_query($db,$sql)){
        echo json_encode(["status" => "success""message" => "User Inserted"'response' => 200]);
    } else {
        echo json_encode(["status" => "fail""message" => "Unable to register the user!"'response' => 400]);
    }
}
?>

 

UI 구성 처리에 대한 사항은 bootstrap 모듈 연동 방법으로 다시 한번 테스트 해볼 생각이다.

샘플로서의 가치가 있을 수준이 되면 my github 에 올려두고 링크를 걸어둘 것이다.

이상으로 인도 개발자 유투브 동영상 참조 및  다른 코드를 찾아 수정하면서 React-PHP 연동에 성공했다는 뿌듯함에 적어뒀다.

 

최근에 알게된 코딩앙마님의 React 강좌가 좋은 듯하여 학습 중이다.

https://www.youtube.com/watch?v=05uFo_-SGXU&list=PLZKTXPmaJk8J_fHAzPLH8CJ_HO_M33e7- 

 

'React > React-PHP' 카테고리의 다른 글

RSA Encryption Login in React.js and PHP  (0) 2022.08.27
React + PHP Login 처리  (0) 2022.08.27
React useEffect example (php REST API)  (0) 2022.08.22
블로그 이미지

Link2Me

,
728x90

React (Front-End) 와 PHP (Back-End) 간에 통신하는 기능으로 유투브 동영상을 시청하면서 타이밍해가면서 동작 여부를 확인하는데 안된다.

 

윈도우에서 React 를 설치하고, 윈도우에 AutoSet10 을 설치하고 http://localhost/php-react/insert.php 로 데이터 전송을 시도하면 같은 localhost 로 인식하여 MySQL DB에 데이터가 잘 저장된다.

 

윈도우에 CentOS 7.9 를 VirtualBox 를 이용하여 설치하고, Node.js 와 React 를 설치하고 PHP 도 같이 설치했다.

IPTIME 공유기 환경에서 http://192.168.1.20:3000 번으로 react 개발자 모드로 접속하고,

http://192.168.1.20/php-react/insert.php 로 접속을 했더니 에러가 발생한다.

 

80 포트와 3000번 포트가 서로 달라서 생기는 문제다.

일반적으로 브라우저는 보안 문제로 인해 동일 출처 정책(SOP, Same Origin Policy)을 따른다.

URL의 프로토콜, 호스트, 포트가 모두 같아야 동일한 출처로 볼 수 있는데, 예를 들어 abc.com 호스트에게 받은 페이지에서 cde.com 호스트로 데이터를 요청할 수 없다. 출처가 다른 호스트로 데이터를 요청하는 경우 CORS 정책을 위반하게 된다. 

 

Register.js 파일 내용

 

insert.php 파일 내용

 

문제 해결 방법

1. package.json  파일에 추가할 사항

 

2. Register.js 파일 수정사항

http://192.168.1.20 remove

3. Chrome Broswer 결과 확인

4. DB 테이블 확인

테이블에 저장이 잘 된 것을 확인할 수 있다.

 

 

 

 

 

 

'React > React' 카테고리의 다른 글

React useReducer  (0) 2022.10.13
React-bootstrap Header.js 테스트  (0) 2022.08.21
React Query String  (0) 2022.06.08
React Router v6  (0) 2022.06.07
React useState  (0) 2022.06.04
블로그 이미지

Link2Me

,
728x90

jQuery Ajax를 사용하여 서버와 비동기 통신을 하는 방법이다.

Front-End 단에서 사용하는 loginForm.html 이다. ( 테스트용으로 사용하던 loginForm.php 파일을 약간 수정)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="form.css"  />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function(){
    $('#login-submit').click(function(e){
        e.preventDefault();
        var userID = $("input[name='userID']");
        if( userID.val() =='') {
            alert("아이디를 입력하세요");
            userID.focus();
            return false;
        }
 
        var password = $("input[name='password']");
        if(password.val() =='') {
            alert("비밀번호를 입력하세요!");
            password.focus();
            return false;
        }
 
        $("form").submit();
    });
});
</script>
</head>
<body>
<div class="container">
<h2>로그인 Form 예제</h2>
<form method="post" action="/login">
    <table>
        <tr>
            <td style="font-size:14px">로그인 ID</td>
            <td><input type="text" name="userID" value=""></td>
        </tr>
        <tr>
            <td style="font-size:14px">패스워드</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan='2' align=left>
                <input type="button" id="login-submit" value="로그인">
            </td>
        </tr>
        <tr>
            <td colspan='2'><p>회원이 아니신가요? <a href="joinForm.html">회원가입 하기</a></p></td>
        </tr>
    </table>
</fom>
</div>
</body>
</html>
 

jQuery 를 CDN 경로로 설정하여 사용하는 것이 편하다.

node_modules/jquery 디렉토리에 설치하여 사용하고자 한다면 ....

npm install jquery --save 를 실행하여 jQuery 라이브러리를 설치한다.

app.js 파일에서 app.use('/node_modules', express.static(path.join(__dirname + '/node_modules'))) 를 추가한다.

loginForm.html 파일에서 <script src="node_modules/jquery/dist/jquery.min.js"></script> 로 수정한다.

 

 

app.js

- public 디렉토리를 static 으로 지정한다.

- express 4.16.0 이상부터 body-parser 를 별도 설치할 필요가 없다.

- form action='/login' 부분이 Back-End app.js 파일 내 app.post('/login', ... ) 에서 처리한다.

- res.render 에서 views 디렉토리 index.ejs 로 값을 넘겨 화면에 출력한다.

var express = require('express');
var path = require('path');
var app = express();
 
 
app.listen(3000function() {
    console.log("start, express server on port 3000");
});
 
app.use(express.static(path.join(__dirname, 'public')));
 
app.set('views', path.join(__dirname, 'views'));
app.set('view engine''ejs');
 
app.use(express.json()); //json 형태로 parsing
app.use(express.urlencoded({ extended: false }));
 
app.get('/'function(req,res) {
    res.sendFile(__dirname + "/publc/index.html");
});
 
app.post('/login'function(req, res) {
    // Form POST 전송의 결과 즉 action='/login'
    var post = req.body;
    console.log(post);
    res.render('index.ejs', {'title':post.userID});
});
 
module.exports = app;

여기서 전달할 데이터 userID 를 {'title' : res.body.userID} 

index.ejs 파일에서 변수로 받는 <%= title %> 에 전달한다.

 

좀 더 완벽하게 Ajax 처리하는 것은 다음에 올릴 예정이다.

블로그 이미지

Link2Me

,
728x90

Java 코드와 Kotlin 코드를 혼용하여 작성하는 환경 설정이 Android Studio 가 버전 업 되면서 변경되었다.

https://developer.android.com/kotlin/add-kotlin?hl=ko 이 URL을 참조하면 최신으로 변경된 것을 확인할 수 있다.

 

코틀린 최신 버전이 1.7.10 이지만 1.6.20 으로 설정했다.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = '1.6.20'
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
 
plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
}
 
 
task clean(type: Delete) {
    delete rootProject.buildDir
}

 

app build.gradle

plugins {
    id 'com.android.application'
    // 코틀린 혼용 사용을 위해 추가
    id 'kotlin-android'
    id 'kotlin-parcelize'
    id 'kotlin-kapt'
    // id 'kotlin-android-extensions' // deprecated 되었음.
}
 
android {
    compileSdk 32
 
    defaultConfig {
        applicationId "com.link2me.android.retrofit_mvvm_login"
        minSdk 23
        targetSdk 32
        versionCode 1
        versionName "1.1"
    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
 
    // View Binding : Android Studio 4.0 이상
    buildFeatures {
        viewBinding = true
    }
}
 
dependencies {
    // 아래 2줄은 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.core:core-ktx:1.8.0' // 2022년 6월 1일 버전
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
 
    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
 
//    implementation 'gun0912.ted:tedpermission:2.0.0'  // 위험권한 퍼미션 처리
    implementation 'io.github.ParkSangGwon:tedpermission-normal:3.3.0' // 위험권한 퍼미션 처리
    implementation 'androidx.cardview:cardview:1.0.0'  // 레이아웃으로 사용할 CardView
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
 
    implementation 'androidx.navigation:navigation-fragment:2.1.0'
    implementation 'androidx.navigation:navigation-ui:2.1.0'
 
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
 
    def lifecycle_version = "2.5.0"
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    // lifecycle-extensions의 API는 지원 중단되었다.
 
    // 이미지 출력용 Glide
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    //annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0' // Kotlin plugin doesn't pick up annotationProcessor dependencies
}

 

 

 

 

블로그 이미지

Link2Me

,
728x90

Retrofit 라이브러리를 사용하면 Session 처리 OK이고, 코드가 심플한 점도 장점이다.

MVVM 적용한 코드로 된 강좌들을 접하다보니 안할 수가 없어 기존 코드를 MVVM 코드로 정리를 해 볼 필요성이 있어 코드를 수정하여 구현해 보고 나서 동작에 대한 개념이 이해되었다.

 

디자인 패턴은 최고의 개발자들이 만들어낸 솔루션이다. 또한, 오랫동안 사용되어 오면서 안정성 검증이 완료된 솔루션으로 심지어 디자인 패턴은 문서화도 잘 되어있다.

 

MVVM 패턴
MVVM 패턴은 마틴 파울러의 Presentation 모델 패턴에서 파생된 디자인 패턴이다. 
MVVM 패턴의 목표는 비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하는 것이다. 
비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하게 되면, 테스트, 유지 보수, 재사용이 쉬워진다. 

- Model : 어플리케이션에서 사용되는 데이터와 그 데이터를 처리하는 부분.
- View : 사용자에서 보여지는 UI 부분.
- View Model : View를 표현하기 위해 만들어진 View를 위한 Model.

  View와는 Binding을 하여 연결후 View에게서 액션을 받고 또한 View를 업데이트한다.

 

1. View에 입력이 들어오면 ViewModel에게 명령을 한다.
2. ViewModel은 필요한 데이터를 Model에게 요청한다.
3. Model은 ViewModel에게 요청된 데이터를 응답한다.
4. ViewModel은 응답 받은 데이터를 가공해서 저장한다.
5. View는 ViewModel과의 Data Binding으로 인해 자동으로 갱신된다.

 

 

Retrofit 라이브러리를 활용한 MVVM 적용 Login 샘플 코드 예제이다.

 

public class LoginActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
    private ActivityLoginBinding binding;
 
    private BackPressHandler backPressHandler;
    private AuthViewModel viewModel;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_login);
        binding = ActivityLoginBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        backPressHandler = new BackPressHandler(this);
        mContext = LoginActivity.this;
 
        Utils.viewShowHide(binding.btnLogin, false);
        initView();
    }
 
    private void initView() {
        // viewModel init
        viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
 
        // viewModel observe
        viewModel.getLoginResultLiveData().observe(thisnew Observer<LoginResult>() {
            @Override
            public void onChanged(LoginResult loginResult) {
                if(loginResult.getStatus().contains("success")){
                    Utils.goNextActivity(mContext,MainActivity.class);
                    finish();
                } else {
                    Utils.showAlert(mContext, loginResult.getStatus(), loginResult.getMessage());
                }
            }
        });
 
        // addTextChangedListener
        binding.etPw.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
 
            }
 
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
 
            }
 
            @Override
            public void afterTextChanged(Editable editable) {
                String userID = binding.etId.getText().toString().trim();
                String password = binding.etPw.getText().toString().trim();
                if (!userID.isEmpty() && !password.isEmpty())
                    Utils.viewShowHide(binding.btnLogin, true);
                // userID 와 패스워드 모두 입력하면 로그인 버튼이 활성화된다.
            }
        });
 
        // button setOnClickListener
        binding.btnLogin.setOnClickListener(view -> {
            login();
        });
 
    }
 
    private void login() {
        String userID = binding.etId.getText().toString().trim();
        String password = binding.etPw.getText().toString().trim();
        String uID = Utils.getDeviceId(mContext); // 스마트폰 고유장치번호
        String mfoneNO = Utils.getPhoneNumber(mContext); // 스마트폰 전화번호
        viewModel.login(userID, password,uID,mfoneNO);
        /*
        로그인 전달 변수 수정 시
        AutoViewModel.java 와 AutoRepository.java 파일 모두 수정
        */
    }
 
 
    @Override
    public void onBackPressed() {
        //super.onBackPressed();
        backPressHandler.onBackPressed();
    }
}

 

 

public class AuthViewModel extends ViewModel {
    private AuthRepository repository;
    private LiveData<LoginResult> _liveData;
 
    public AuthViewModel() {
    }
 
    public void login(String userID, String password, String uID, String mfoneNO) {
        repository.loginRepo(userID, password, uID, mfoneNO);
    }
 
    public LiveData<LoginResult> getLoginResultLiveData() {
        if (_liveData == null) {
            repository = new AuthRepository();
            _liveData = repository.getLoginResultLiveData();
        }
        return _liveData;
    }
 
}
 

 

 

 

public class AuthRepository {
    private RetrofitService loginService;
    private MutableLiveData<LoginResult> loginResultLiveData;
 
    public AuthRepository() {
        loginResultLiveData = new MutableLiveData<>();
        loginService = RetrofitAdapter.getClient(RetrofitURL.BaseUrl).create(RetrofitService.class);
    }
 
    public void loginRepo(String userID, String password, String uID, String mfoneNO){
        loginService.Login(userID,password,uID,mfoneNO)
                .enqueue(new Callback<LoginResult>() {
                    @Override
                    public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
                        loginResultLiveData.postValue(response.body());
                    }
 
                    @Override
                    public void onFailure(Call<LoginResult> call, Throwable t) {
                        loginResultLiveData.postValue(null);
                    }
                });
    }
 
    public LiveData<LoginResult> getLoginResultLiveData() {
        return loginResultLiveData;
    }
}

 

MVVM 기반 코드는 위 3개의 파일이 핵심인 거 같다.

전체 소스 코드는 Github 에 올려 두었다.

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

 

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

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

github.com

 

 

아래 GitHub를 참고하면 유용한 자료들이 많다.

https://github.com/probelalkhan?tab=repositories 

 

probelalkhan - Overview

probelalkhan has 147 repositories available. Follow their code on GitHub.

github.com

 

블로그 이미지

Link2Me

,