오픈뱅킹API 실습(React + Express + MySql) A부터 한...T정도까지?(Z까지는 아님)

const job = '프론트엔드';·2023년 9월 18일
2
post-thumbnail

오픈뱅킹 API 활용일대기

경고 : '쉽지 않은 여정이기 때문에 마음 단단히 먹어야 함'

준비1. 소스트리(Sourcetree)

step1: git 설치하기

brew install git

step2: 소스트리 설치하기

step3: 소스트리에서 git clone

3-1. url 에서 복제하기(깃 레파짓토리 주소)

3-2. 목적지 경로 및 이름(자동으로 설정됨)

3-3. 성공 !

여기에서 커밋, , 푸시, 머지 등등 하면 됨 !

준비2. 기본개념 파악하기

클라이언트 ↔ 서버

요청(request)

요청시 HTTP 통신 규약

통신 규약을 지키지 않으면 요청을 하는 클라이언트, 요청을 받는 서버 둘다 해당 메시지를 해석할 수 없음

요청 방법에 따른 - HTTP 메서드

  • get
  • post
  • put
  • delete

준비3. 사용스택

프론트엔드(클라이언트): React
백엔드(서버):express
데이터베이스: mySql
활용API: 오픈뱅킹 API
CSS: styled-components

준비4. 실행방법 및 해당포트

클라이언트: npm start / /3000
서버: node expressServer.js / /4000

준비5. 라이브러리

npm install styled-components
npm install react-router-dom
npm install axios
npm install query-string
npm install qrcode.react
npm install react-qrcode-reader
npm install react-slick --force
npm install react-modal --force
npm install slick-carousel --force
npm install jsonwebtoken
npm install --save mysql2
npm install dotenv --save
npm install express

준비6. API실습

통신을 위한 axios 설치

npm install axios

import React from "react";
import axios from "axios";

const AxiosComponent = () => {
  const handleClick = () => {
    axios.get("https://naver.com").then((res) => {
      console.log(res);
    });
  };

  return (
    <div>
      <button onClick={handleClick}>요청생성</button>
    </div>
  );
};

export default AxiosComponent;

  • 네트워크 오류 발생하면 코드를 잘 작성한거 (오류 당연함)

네트워크 오류 - CORS 정책 위배

  • 요청을 보내는 도메인이 달라서
  • localhost로 보냈고, 응답을 주는건 naver이기 때문(위에 코드에서 확인 가능)
  • 외부 리소스를 함부로 특정사이트로 가져오지 않게 하기 위함

실습1 ) news api 받아오기

import React from "react";
**import axios from "axios";**

const AxiosComponent = () => {
  const handleClick = () => {
    **axios
      .get(
        "https://newsapi.org/v2/everything?q=tesla&from=2023-08-13&sortBy=publishedAt&apiKey=개별apikey입력"
      )
      .then((res) => {
        console.log(res);
      });**
  };

  return (
    <div>
      <button onClick={handleClick}>요청생성</button>
    </div>
  );
};

export default AxiosComponent;


postman 으로 미리 확인하기

postman 설치

brew install --cask postman

postman 가입하기

요청 url 넣고 확인

쿼리 추가해보기(language 쿼리를 추가해 봄)

뉴스 검색기능

주요기능

  • input에 검색 키워드를 입력
  • button을 누르면 검색한 내용의 기사 제목이 리스트형식 결과로 출력됨

구조

최상위 컴포넌트 App
부모컴포넌트 NewsSearch
자식컴포넌트 HeaderComponent, Search, Result

1. NewsSearch 컴포넌트(부모 컴포넌트, 단 최상위 부모는 App컴포넌트)

 	  <HeaderComponent />  
	  <Search />
      <Result />
  • 이하 자식 컴포넌트(HeaderComponent, Search, Result)

2. Search 컴포넌트 내부

import React, { useState } from "react";
import axios from "axios";

const Search = ({ onSearch }) => {
  const [searchText, setSearchText] = useState("");

  const handleClick = () => {
    axios
      .get(
        `https://newsapi.org/v2/everything?q=${searchText}&from=2023-08-13&sortBy=publishedAt&apiKey=apikey입력&language=ko`
      )
      .then((res) => {
        onSearch(res.data.articles);
      });
  };

  const handleInputChange = (event) => {
    setSearchText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={searchText} onChange={handleInputChange} />
      <button onClick={handleClick}>검색</button>
    </div>
  );
};

export default Search;
  • input창에 입력된 valuesearchText로 저장 상태 관리를 위해 useState사용
  • onChange를 사용해 입력받는 값을 보내줌
  • 입력받은 값을 button의 onClick을 이용해서 데이터를 요청
  • 데이터를 받아서 onSearch에 담아서 넘겨줌

3. Result 컴포넌트 내부

import React from "react";

const Result = ({ articles }) => {
  return (
    <div>
      <ul>
        {articles.map((article, index) => (
          <li key={index}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Result;
  • 요청한 데이터에서 title 항목만 가져다가 title만 리스트로 검색결과를 보여줌

상태관리

status

  1. 원하는 검색어를 입력
  2. 검색어를 담은 버튼을 통해 해당하는 데이터를 요청(axios)
  3. 담아온 데이터를 바탕으로 결과를 리스트 형식으로 보여줌

요청한 데이터를 Search(자식)에서 onSearch로 NewsSearch(부모)로 다시 올려줌

const Search = ({ **onSearch** }) => { 

}
axios
      .get(
        `https://newsapi.org/v2/everything?q=${searchText}&from=2023-08-13&sortBy=publishedAt&apiKey=apikey&language=ko`
      )
      .then((res) => {
        **onSearch**(res.data.articles);
      })

담아온 데이터를 NewsSearch에서 상태를 업데이트 함

const [searchResults, setSearchResults] = useState([]); 

결과를 Result에 리스트 형식으로 넘겨줌

 // NewsSearch.jsx   

    <div>
      <HeaderComponent />
      <Search onSearch={handleSearch} />
      <Result **articles**={searchResults} />
    </div>

prop으로 부모로부터 넘겨받은 데이터의 결과를 보여줌

import React from "react";

const Result = ({ **articles** }) => {
  return (
    <div>
      <ul>
        {articles.map((article, index) => (
          <li key={index}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Result;

완성된 화면

cf) 다른방법 예제코드

상위 컴포넌트

import React, { useState } from "react";
import HeaderComponent from "../components/HeaderComponent";
import SearchInputComponents from "../components/newsSearch/SearchInputComponents";
import NewsListComponents from "../components/newsSearch/NewsListComponents";
import axios from "axios";

const NewsSearch = () => {
  const [searchValue, setSearchValue] = useState("");
  const [newsList, setNewsList] = useState([]);

  const handleChange = ({ target }) => {
    const { value } = target;
    console.log(value);
    setSearchValue(value);
  };
  const handleClick = () => {
    console.log("hello");
    // axios 요청 작성하기
    const apiKey = "";
    axios
      .get(
        `https://newsapi.org/v2/everything?q=${searchValue}&from=2023-08-13&sortBy=publishedAt&apiKey=${apiKey}`
      )
      .then((response) => {
        console.log(response);
        setNewsList(response.data.articles);
      });
  };

  return (
    <div>
      <HeaderComponent></HeaderComponent>
      <SearchInputComponents
        handleChange={handleChange}
        handleClick={handleClick}
      ></SearchInputComponents>
      <NewsListComponents newsList={newsList}></NewsListComponents>
    </div>
  );
};

export default NewsSearch;
  • 부모컴포넌트에서 데이터를 요청하고 받아서 자식 컴포넌트로 전달하는 구조

SearchInput컴포넌트

import React from "react";

const SearchInputComponents = ({ handleChange, handleClick }) => {
  return (
    <div>
      <div>
        <input onChange={handleChange}></input>
        <button onClick={handleClick}>전송</button>
      </div>
    </div>
  );
};

export default SearchInputComponents;
  • input에 담긴 내용을 전송

NewsList컴포넌트

import React from "react";

const NewsListComponents = ({ newsList }) => {
  return (
    <div>
      {newsList.map((news) => {
        return <>{news.title}</>;
      })}
    </div>
  );
};

export default NewsListComponents;

실습2) 오픈뱅킹 API

3-legged 방식

  • 함부로 다른 사람의 계좌 등을 동의 없이 확인하면 안되니깐 !

client ID, Key 발급 받아서 따로 저장해 두기

  • 서비스 상태를 이용중으로 바꿈
- 테스트API 호출 : https://testapi.openbanking.or.kr로 호출
- 오픈뱅킹에서 제공하는 인증을 사용
- 필수값이 Y인 항목만 이용

api 명세서 참고해서 작성(feat. Postman)

필수값(Y) 쿼리를 포스트맨에 추가해서 확인

  • 오류발생 왜냐?
  • testapi로 하기로 약속함

요청 url로 확인해보기 → 사용자 인증페이지로 이동

1. 인증

2. 계속

3. 성공

토큰발급 API

  • post 방식
  • body에 입력
  • x-www-form-urlencoded 선택
  • 필수 key 입력

  • 오류발생: ~113 오류 메세지가 발생하면 성공 (시간만료, 이미 사용한 코드는 이런 에러가 발생)
rsp_message": "인증요청거부-인증 파라미터 오류([3000113])"

  • 그래서 다시 인증을 해서 코드를 입력하면?

  • 정상적으로 받아옴
{
    "access_token": "access_token",
    "token_type": "Bearer",
    "refresh_token": "refresh_token",
    "expires_in": 7775999,
    "scope": "inquiry login transfer",
    "user_seq_no": "1101038125"
}

cf) 응답 메시지 명세

  • 다시 send를 해서 요청하면? 이미 사용한 code이기 때문에 다시 오류발생

사용자정보조회 API

  • accesstoken, 사용자 일련번호 필요


{
    "api_tran_id": "api_tran_id",
    "api_tran_dtm": "api_tran_dtm",
    "rsp_code": "A0000",
    "rsp_message": "",
    "user_seq_no": "user_seq_no",
    "user_ci": "0WSRNaalbJe40nXqYMeFMLas4YhCFMfIySdzGsSYRMz2v/qQQ6R1wvgEPirOt7FkUJiF1y9DUdsQvsjVRwKDIQ==",
    "user_name": "송가영",
    "res_cnt": "1",
    "res_list": [
        {
            "fintech_use_num": "120230226588951223594984",
            "account_alias": "오픈은행",
            "bank_code_std": "097",
            "bank_code_sub": "0000000",
            "bank_name": "오픈은행",
            "savings_bank_name": "",
            "account_num_masked": "123412341***",
            "account_seq": "",
            "account_holder_name": "송가영",
            "account_holder_type": "P",
            "account_type": "1",
            "inquiry_agree_yn": "Y",
            "inquiry_agree_dtime": "20230913141953",
            "transfer_agree_yn": "Y",
            "transfer_agree_dtime": "20230913141953",
            "payer_num": "20230913319841225825"
        }
    ],
    "inquiry_card_cnt": "0",
    "inquiry_card_list": [],
    "inquiry_pay_cnt": "0",
    "inquiry_pay_list": [],
    "inquiry_insurance_cnt": "0",
    "inquiry_insurance_list": [],
    "inquiry_loan_cnt": "0",
    "inquiry_loan_list": []
}

적용하기

사용자 인증 API

#### 사용자인증 API

  GET https://testapi.openbanking.or.kr/oauth/2.0/authorize
ParameterType
response_type고정값: code
client_idclient_id
redirect_urihttp://localhost:3000/authResult
scopelogin inquiry transfer
state12345678901234567890123456789012
auth_type0
import React from "react";
import HeaderComponent from "../components/HeaderComponent";

const MainPage = () => {
  const handleClick = () => {
    // 새 창을 열기
    const newWindow = window.open("", "_blank");

    const clientId = process.env.REACT_APP_BANK_ID;
    // 주소 설정
    const authorizeUrl = `https://testapi.openbanking.or.kr/oauth/2.0/authorize?response_type=code&client_id=${clientId}&redirect_uri=http://localhost:3000/authResult&scope=login%20inquiry%20transfer&state=12345678901234567890123456789012&auth_type=0`;

    // 새 창의 위치를 지정하지 않으면 기본적으로 중앙에 열립니다.
    // 만약 위치를 지정하려면 다음과 같이 사용할 수 있습니다.
    // const windowOptions = 'width=800,height=600,left=100,top=100';

    // 새 창을 열고 주소로 이동
    newWindow.location.href = authorizeUrl;
  };
  return (
    <div>
      <HeaderComponent title={"사용자 인증 센터 이동"}></HeaderComponent>
      <button onClick={handleClick}>사용자 인증</button>
    </div>
  );
};

export default MainPage;

사용자 토큰발급 API

#### 사용자 토큰발급 API

POST https://testapi.openbanking.or.kr/oauth/2.0/token

Content-Type: application/x-www-form-urlencoded; charset=UTF-8
ParameterType
codeauthorization_code
client_idclient_id
client_secretclient_secret
redirect_urihttp://localhost:3000/authResult
grant_type고정값: authorization_code

쿼리 문자열로 파싱하기 위한 라이브러리 설치

npm install query-string
import React, { useState } from "react";
import HeaderComponent from "../components/HeaderComponent";
import { useLocation } from "react-router-dom";
import queryString from "query-string";
import axios from "axios";

const AuthResult = () => {
  const queryParams = useLocation().search;
  const parsed = queryString.parse(queryParams);
  const code = parsed.code;
  const id = process.env.REACT_APP_BANK_ID;
  const pw = process.env.REACT_APP_BANK_PW;

  const [accessToken, setAccessToken] = useState("");
  const [userSeqNo, setUserSeqNo] = useState("");

  const handleClick = () => {
    let requestOption = {
      url: "/oauth/2.0/token",
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
      },
      data: {
        code: code,
        client_id: id,
        client_secret: pw,
        redirect_uri: "http://localhost:3000/authResult",
        grant_type: "authorization_code",
      },
    };
    axios(requestOption).then(({ data }) => {
      setAccessToken(data.access_token);
      setUserSeqNo(data.user_seq_no);
      if (data.rsp_code !== "O0001") {
        localStorage.setItem("accessToken", data.access_token);
        localStorage.setItem("userSeqNo", data.user_seq_no);
        alert("저장 완료");
      } else {
        alert("인증에 실패했습니다 다시 시도해 주세요");
      }
    });
  };

  return (
    <div>
      <HeaderComponent title={"토큰 발급 / 인증"} />
      <p>사용자 인증 코드 : {code}</p>
      <button onClick={handleClick}>토큰 발급하기</button>
      <p>accessToken : {accessToken}</p>
      <p>userSeqNo : {userSeqNo}</p>
    </div>
  );
};

export default AuthResult;
  • useLocation 훅을 사용해서 현재 URL의 쿼리 문자열을 가져와 queryParams변수에 저장
  • queryString 라이브러리를 사용해서 쿼리 문자열을 파싱(queryString.parse(queryParams))하고 그결과를 parsed 변수에 저장
  • useState를 사용하여 accessTokenuserSeqNo 상태 변수를 초기화
  • handleClick 함수는 HTTP POST 요청을 사용하여 토큰을 요청하고 결과를 처리

  • CORS 네트워크 오류발생

오류해결하기 package.json에 추가

계좌조회 API

거래고유번호 오류면 성공

  • bank_trans_id 항목은 ‘이용기관코드+U+이용기관부여번호’로 이루어져 있는 것임

  • 이용기관코드는 회원정보관리에서 확인할 수 있으며, 이용기관부여번호는 test중에는 임의의 9자리 난수로 하면 됨
import React, { useEffect, useState } from "react";
import MainAccountCard from "../components/list/MainAccountCard";
import axios from "axios";
import HeaderComponent from "../components/HeaderComponent";

const AccountList = () => {
  const [accountList, setAccountList] = useState([]);

  useEffect(() => {
    console.log("data");
    getAccountList();
  }, []);

  const getAccountList = () => {
    let requestOption = {
      url: "/v2.0/user/me",
      method: "GET",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        Authorization:
          "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMTAxMDM4MTI1Iiwic2NvcGUiOlsiaW5xdWlyeSIsImxvZ2luIiwidHJhbnNmZXIiXSwiaXNzIjoiaHR0cHM6Ly93d3cub3BlbmJhbmtpbmcub3Iua3IiLCJleHAiOjE3MDIzNjM1NjAsImp0aSI6ImMwYzc1YWM0LWU4MjctNDE3Yy1hZGI0LWY4ZWFjN2QwZjU2YyJ9.d7HKKhJiSJ0rDhViN7qUQdYt48ER_A73zrb6JnFimYU",
      },
      params: {
        user_seq_no: "1101038125",
      },
    };

    axios(requestOption).then((response) => {
      console.log(response);
      setAccountList(response.data.res_list);
    });
  };

  return (
    <div>
      <HeaderComponent title={"계좌조회"}></HeaderComponent>
      {accountList.map((account) => {
        return (
          <MainAccountCard
            key={account.fintech_use_num}
            bankName={account.bank_name}
            fintechUseNo={account.fintech_use_num}
          ></MainAccountCard>
        );
      })}
    </div>
  );
};

export default AccountList;

  • 성공 !

문제점 : 승인을 받지 않으면, 테스트 정보관리가 불가능함

  • 이하 프로젝트 진행은 (사용가능한)정보로 바꿔서 실행

필요한 정보(대체된 정보)

이용기관코드
AccessToken
userSeqNo

  • 요청했던 화면을 대체된 정보로 바꾼화면

잔액조회 API

import React, { useEffect, useState } from "react";
import axios from "axios";
import { useLocation } from "react-router-dom";
import queryString from "query-string";

const BalanceList = () => {
  const queryParams = useLocation().search;
  const parsed = queryString.parse(queryParams);
  const fintechNo = parsed.fintechUseNo;
  const [balanceList, setBalanceList] = useState([]);

  useEffect(() => {
    getBalanceList();
  }, []);

  const getBalanceList = () => {
    const accessToken = localStorage.getItem("accessToken");
    let requestOption = {
      url: "/v2.0/account/transaction_list/fin_num",
      method: "GET",
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      params: {
        bank_tran_id: genTrasId(),
        fintech_use_num: fintechNo,
        inquiry_type: "A",
        inquiry_base: "D",
        from_date: "20201212",
        to_date: "20230901",
        sort_order: "D",
        tran_dtime: "20230914103600",
      },
    };

    axios(requestOption).then((response) => {
      console.log(response);

      setBalanceList(response.data.res_list);
    });
  };

  function generateRandom9DigitNumber() {
    const min = 100000000; // Minimum value (smallest 9-digit number)
    const max = 999999999; // Maximum value (largest 9-digit number)

    const random9DigitNumber =
      Math.floor(Math.random() * (max - min + 1)) + min;
    return random9DigitNumber.toString();
  }

  const genTrasId = () => {
    return "M202300440U" + generateRandom9DigitNumber();
  };

  // Render the balanceList as a list
  return (
    <div>
      <ul>
        {balanceList.map((item, index) => (
          <li key={index}>
            순번: {index + 1}, 내용: {item.tran_type}, 거래금액: {item.tran_amt}
            , 잔액: {item.after_balance_amt}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default BalanceList;

QRcode 만들기

npm install qrcode.react
import React from "react";
import HeaderComponent from "../components/HeaderComponent";
import { QRCodeSVG } from "qrcode.react";
import styled from "styled-components";
import { useLocation } from "react-router-dom";
import queryString from "query-string";

const QrCode = () => {
  const QRBlock = styled.div`
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 1rem;
  `;

  const queryParams = useLocation().search;
  const parsed = queryString.parse(queryParams);
  const fintechNo = parsed.fintechUseNo;
  return (
    <div>
      <HeaderComponent title={"QR"}></HeaderComponent>
      <QRBlock>
        <QRCodeSVG size={256} value={fintechNo} />
        <p>{fintechNo}</p>
      </QRBlock>
    </div>
  );
};

export default QrCode;

QR reader만들기

npm i react-qrcode-reader
import React, { useState } from "react";
import HeaderComponent from "../components/HeaderComponent";
import QrCodeReader, { QRCode } from "react-qrcode-reader";

const QrReader = () => {
  const [val, setVal] = useState("");

  return (
    <>
      <HeaderComponent title={"QR Reader"} />
      <QrCodeReader delay={100} width={600} height={500} action={setVal} />
      <p>{val}</p>
    </>
  );
};

export default QrReader;

출금이체 API

참고)

오류발생: A0004

import axios from "axios";
import React, { useState } from "react";
import styled from "styled-components";

const ModalCardBlock = styled.div`
  display: flex;
  flex-direction: column;
  margin: 0.5rem;
  padding: 20px;
  border: 1px #112211 solid;
`;
const CardTitle = styled.div`
  font-size: 1rem;
  color: black;
`;
const FintechUseNo = styled.div`
  font-size: 0.7rem;
  margin-bottom: 30px;
`;

const WithDrawButton = styled.button`
  border: none;
  padding: 0.3rem;
  background: #2aa450;
  color: white;
  margin-top: 0.3rem;
`;

const ModalCard = ({ bankName, fintechUseNo, tofintechno }) => {
  //fintechUseNo : 내계좌
  //tofintechno : QR 코드로 읽어온 핀테크 계좌
  const [amount, setamount] = useState("");
  const [withdraw, setWithdraw] = useState([]);

  const genTransId = () => {
    let countnum = Math.floor(Math.random() * 1000000000) + 1;
    const clientNo = "M202300440";
    let transId = clientNo + "U" + countnum;
    return transId;
  };

  const handlePayButtonClick = () => {
    //출금이체 기능 작성
    //rsp_code가 A000일때 alert("출금완료")가 발생하고 나머지는 오류 메세지를 띄우게

    const accessToken = localStorage.getItem("accessToken");

    let requestOption = {
      url: "/v2.0/transfer/withdraw/fin_num",
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
        Authorization: `Bearer ${accessToken}`,
      },
      body: {
        bank_tran_id: genTransId,
        cntr_account_type: "N",
        cntr_account_num: "100000000001",
        dps_print_content: "쇼핑몰환불",
        fintech_use_num: fintechUseNo,
        wd_print_content: "오픈뱅킹출금",
        tran_amt: amount,
        tran_dtime: "20230812130000",
        req_client_name: "홍길동",
        req_client_fintech_use_num: fintechUseNo,
        req_client_num: "HONGGILDONG1234",
        transfer_purpose: "ST",
        recv_client_name: "유관우",
        recv_client_bank_code: "097",
        recv_client_account_num: "100000000001",
      },
    };

    axios(requestOption)
      .then((response) => {
        console.log(response);
        const { rsp_code } = response.data;

        if (rsp_code === "A000") {
          alert("이체성공");
        } else {
          alert("Error: 실패");
        }

        setWithdraw(response.data);
      })
      .catch((error) => {
        console.error("Error:", error);
        alert("Error: 실패");
      });
  };

  const handleChange = (e) => {
    const { value } = e.target;
    console.log(value);
    setamount(value);
  };

  return (
    <ModalCardBlock>
      <CardTitle>{bankName}</CardTitle>
      <FintechUseNo>{fintechUseNo}</FintechUseNo>
      <p>{tofintechno}로 돈을 보냅니다.</p>
      <input onChange={handleChange}></input>
      <WithDrawButton onClick={handlePayButtonClick}>결제하기</WithDrawButton>
    </ModalCardBlock>
  );
};

export default ModalCard;

오류해결하기

  • 오타 문제였음
bank_tran_id: genTransId()

  • 그래서, A0000 = 처리성공인데, 실패했다는 arlet창이 뜨는 것을 확인
  • 두번째 오타 발견, 처리성공 코드는 A0000
if (rsp_code === "A000")

import axios from "axios";
import React, { useState } from "react";
import styled from "styled-components";

const ModalCardBlock = styled.div`
  display: flex;
  flex-direction: column;
  margin: 0.5rem;
  padding: 20px;
  border: 1px #112211 solid;
`;
const CardTitle = styled.div`
  font-size: 1rem;
  color: black;
`;
const FintechUseNo = styled.div`
  font-size: 0.7rem;
  margin-bottom: 30px;
`;

const WithDrawButton = styled.button`
  border: none;
  padding: 0.3rem;
  background: #2aa450;
  color: white;
  margin-top: 0.3rem;
`;

const ModalCard = ({ bankName, fintechUseNo, tofintechno }) => {
  //fintechUseNo : 내계좌
  //tofintechno : QR 코드로 읽어온 핀테크 계좌
  const [amount, setamount] = useState("");
  const [withdraw, setWithdraw] = useState([]);

  const genTransId = () => {
    let countnum = Math.floor(Math.random() * 1000000000) + 1;
    const clientNo = "M202300440";
    let transId = clientNo + "U" + countnum;
    return transId;
  };

  const handlePayButtonClick = () => {
    //출금이체 기능 작성
    //rsp_code가 A000일때 alert("출금완료")가 발생하고 나머지는 오류 메세지를 띄우게

    const accessToken = localStorage.getItem("accessToken");

    let requestOption = {
      url: "/v2.0/transfer/withdraw/fin_num",
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
        Authorization: `Bearer ${accessToken}`,
      },
      data: {
        bank_tran_id: genTransId(),
        cntr_account_type: "N",
        cntr_account_num: "100000000001",
        dps_print_content: "쇼핑몰환불",
        fintech_use_num: fintechUseNo,
        wd_print_content: "오픈뱅킹출금",
        tran_amt: amount,
        tran_dtime: "20230812130000",
        req_client_name: "홍길동",
        req_client_fintech_use_num: fintechUseNo,
        req_client_num: "HONGGILDONG1234",
        transfer_purpose: "ST",
        recv_client_name: "유관우",
        recv_client_bank_code: "097",
        recv_client_account_num: "100000000001",
      },
    };

    axios(requestOption).then((response) => {
      console.log(response);
      const { rsp_code } = response.data;

      **if (rsp_code === "A0000") {
        alert("성공");
      }**

      setWithdraw(response.data);
    });
  };

  const handleChange = (e) => {
    const { value } = e.target;
    console.log(value);
    setamount(value);
  };

  return (
    <ModalCardBlock>
      <CardTitle>{bankName}</CardTitle>
      <FintechUseNo>{fintechUseNo}</FintechUseNo>
      <p>{tofintechno}로 돈을 보냅니다.</p>
      <input onChange={handleChange}></input>
      <WithDrawButton onClick={handlePayButtonClick}>결제하기</WithDrawButton>
    </ModalCardBlock>
  );
};

export default ModalCard;

입금이체 API

  • 다른 api 요청 명세서에서 Authorization 과 다름(scope 항목이 추가)
  • 이 경우 새로 2legged Api 요청을 통해 AccessToken을 받아야 함

나머지 body작성

노드JS(서버)

노드가 빠른이유?

  • 비동기 프로그래밍이기 때문(동시에 여러 작업을 실행할 수 있는 능력)
  • 논블럭킹: 작업이 완료되기를 기다리지 않고 다른 작업을 실행하고 나중에 결과를 가져옴

fs 라이브러리?

  • Node.js 내장 모듈인 fs(파일 시스템)

암호화

Crypto | Node.js v20.5.1 Documentation

단방향 sha256

const crypto = require("crypto");

const sha256Enc = (plainText, key) => {
  const secret = key;
  const hash = crypto
    .createHmac("sha256", secret)
    .update(plainText)
    .digest("hex");
  return hash;
};

암호화 복호화

JWT 암호화

JWT.IO

JWT 라이브러리

npm install jsonwebtoken

npm: jsonwebtoken

MySql 워크벤치 설치하기

brew install --cask mysqlworkbench
open -a MySQLWorkbench

DB연결 라이브러리

npm install --save mysql2

cf) .env 관리 라이브러리 적용

npm install dotenv --save

Git

const mysql = require("mysql2");
**const dotenv = require("dotenv");
dotenv.config();**

// create the connection to database
const connection = mysql.createConnection({
  **host: process.env.DB_HOST,
  user: process.env.DB_ACCOUNT,
  password: process.env.DB_PASSWORD,**
  database: "fintech",
});

// simple query
connection.query("쿼리자리", function (err, results, fields) {
  console.log(err);
  console.log(results); // results contains rows returned by server
  console.log(fields); // fields contains extra meta data about results, if available
});

DB 테이블 만들기(연결을 위한)

워크벤치

1. 스키마 만들기

2. 테이블 만들기

세부설정

3. Select Rows로 만든 테이블 확인하기

4. 테스트를 위한 데이터 넣어보기

연결 확인 (실행)

Express ?

라이브러리 설치

npm install express

const express = require("express");
const dotenv = require("dotenv");
const app = express();

dotenv.config();

app.get("/", function (req, res) {
  res.send("Hello World");
});

app.listen(process.env.PORT);

라우터 추가

const express = require("express");
const dotenv = require("dotenv");
const app = express();

dotenv.config();

app.get("/", function (req, res) {
  res.send("Hello World");
});

//라우터추가
app.post("/", (req, res) => {
  console.log(req);
  res.send("hello");
});

app.listen(process.env.PORT);

로그인 기능

const express = require("express");
const dotenv = require("dotenv");
const mysql = require("mysql2");
var jwt = require("jsonwebtoken");

const app = express();

dotenv.config();
const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_ACCOUNT,
  password: process.env.DB_PASSWORD,
  database: "fintech",
});

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.get("/", function (req, res) {
  res.send("Hello World");
});

app.post("/", (req, res) => {
  console.log(req.body);
  res.send("hello");
});

app.post("/login", (req, res) => {
  const { userAccount, password } = req.body;
  const sql =
    "SELECT user_id, user_account, user_password FROM fintech.user WHERE user_account = ?";
  connection.query(sql, [userAccount], (err, result) => {
    if (err) throw err;
    console.log(result);
    if (password === result[0].user_password) {
      let tokenKey = "f@i#n%tne#ckfhlafkd0102test!@#%";
      jwt.sign(
        {
          userId: result[0].user_id,
          userEmail: result[0].user_account,
        },
        tokenKey,
        {
          expiresIn: "10d",
          issuer: "fintech.admin",
          subject: "user.login.info",
        },
        function (err, token) {
          if (err) {
            console.error(err);
          }
          console.log("로그인 성공", token);
          res.json(token);
        }
      );
    }
  });
});

app.listen("4001");

암호화 적용

const express = require("express");
const dotenv = require("dotenv");
const mysql = require("mysql2");
var jwt = require("jsonwebtoken");
**const crypto = require("crypto");**

const app = express();

dotenv.config();
const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_ACCOUNT,
  password: process.env.DB_PASSWORD,
  database: "fintech",
});

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.get("/", function (req, res) {
  res.send("Hello World");
});

app.post("/", (req, res) => {
  console.log(req.body);
  res.send("hello");
});

app.post("/login", (req, res) => {
  const { userAccount, password } = req.body;
  const sql =
    "SELECT user_id, user_account, user_password FROM fintech.user WHERE user_account = ?";
  connection.query(sql, [userAccount], (err, result) => {
    if (err) throw err;
    console.log(result);
    **let encPassword = sha256Enc(password, "fintech");**
    if (encPassword === result[0].user_password) {
      let tokenKey = "f@i#n%tne#ckfhlafkd0102test!@#%";
      jwt.sign(
        {
          userId: result[0].user_id,
          userEmail: result[0].user_account,
        },
        tokenKey,
        {
          expiresIn: "10d",
          issuer: "fintech.admin",
          subject: "user.login.info",
        },
        function (err, token) {
          if (err) {
            console.error(err);
          }
          console.log("로그인 성공", token);
          res.json(token);
        }
      );
    } else {
      res.json("비밀번호 다릅니다.");
    }
  });
});

**const sha256Enc = (plainText, key) => {
  const secret = key;
  const hash = crypto
    .createHmac("sha256", secret)
    .update(plainText)
    .digest("base64");
  return hash;
};**

app.listen("4001");

포트 오류 해결하기

  • 도대체 내 4000번 포트를 누가 사용하고 있다는 거야?
  • 나. 내가 쓰고있더라구
  • 그래서 포트 종료하고 다시 실행했더니 됐음 !

outtoken(우리토큰)로 바꾸기

AccountList.jsx

const getAccountList = () => {
    **const ourtoken = localStorage.getItem("ourtoken");**
    let requestOption = {
      url: "/account",
      method: "GET",
      headers: {
        **ourtoken: ourtoken,**
      },
    };

    axios(requestOption).then((response) => {
      console.log(response);
      setAccountList(response.data.res_list);
    });
  };

AuthResult(클라이언트) - 서버

node JS(서버)

app.get("/authResult", (req, res) => {
  const authCode = req.query.code;
  let requestOption = {
    url: "https://testapi.openbanking.or.kr/oauth/2.0/token",
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    },
    data: {
      code: authCode,
      client_id: process.env.FINTECH_CLIENT_ID,
      client_secret: process.env.FINTECH_CLIENT_SECRET,
      redirect_uri: "http://localhost:4000/authResult",
      grant_type: "authorization_code",
    },
  };
  axios(requestOption).then(({ data }) => {
    if (data.rsp_code !== "O0001") {
      console.log(data);
    } else {
      console.log(data);
    }
  });
});
  • 엔드포인트: /authResult
  • 클라이언트에서 인증코드 authCode를 받아서 토큰을 발급
  • authCode 를 이용해서 엔드포인트로 POST 요청을 보냄

POST 요청

  • axois 사용
  • url : Open Banking API의 토큰 엔드포인트 URL을 지정
  • POST 방식
  • data에는 클라이언트에서 받은 authCode와 클라이언트ID, 시크릿넘버 등을 담아서 요청을 보냄

클라이언트

let requestOption = {
      url: "/oauth/2.0/token",
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
      },
      data: {
        code: code,
        client_id: id,
        client_secret: pw,
        redirect_uri: "http://localhost:3000/authResult",
        grant_type: "authorization_code",
      },
    };
    axios(requestOption).then(({ data }) => {
      setAccessToken(data.access_token);
      setUserSeqNo(data.user_seq_no);
      if (data.rsp_code !== "O0001") {
        localStorage.setItem("accessToken", data.access_token);
        localStorage.setItem("userSeqNo", data.user_seq_no);
        alert("저장 완료");
      } else {
        alert("인증에 실패했습니다 다시 시도해 주세요");
      }
    });

AccountList(클라이언트) - 서버

app.get("/account", auth, (req, res) => {
  let { userId } = req.decoded;
  console.log(req.decoded);
  const sql = "SELECT * FROM user WHERE user_id = ?";
  connection.query(sql, [userId], function (err, result) {
    console.log(result);
    const accesstoken = result[0].access_token;
    const userSeqNo = result[0].user_seq_no;
    console.log(accesstoken);
    const sendData = {
      user_seq_no: userSeqNo,
    };
    const option = {
      method: "GET",
      url: "https://testapi.openbanking.or.kr/v2.0/user/me",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        Authorization: `Bearer ${accesstoken}`,
      },
      params: sendData,
    };
    axios(option).then(({ data }) => {
      res.json(data);
    });
  });
});
  • endpoint: /account
  • 사용자의 인증여부: auth - 인증되지 않은 사용자일 경우 엔트포인트에 접근 불가
  • auth를 통과한(인증된 사용자일 경우) req.decoded(사용자 정보가 디코딩되어 해당 객체에 저장되어 있음)를 userId 에 넣음
  • sql db에서 user테이블에서 user_iduserId와 일치하는 사용자 정보를 조회
  • SQL 쿼리의 결과는 result에 저장
  • 사용자의 access_token (사용자 인증토근)과 user_seq_no (사용자 일련번호)를 추출
  • Open Banking API에 GET 요청을 보내는 설정을 option 객체에 구성
  • Open Banking API 엔드포인트 URL을 지정하며, /v2.0/user/me 엔드포인트에서 사용자 정보를 조회
  • headers에는 요청 헤더를 설정하며, 여기서는 Authorization 헤더에 사용자의 access_token을 포함시켜 인증
  • params 객체에는 요청 파라미터로 사용자의 user_seq_no를 전달

클라이언트

const ourtoken = localStorage.getItem("ourtoken");
    let requestOption = {
      url: "/account",
      method: "GET",
      headers: {
        ourtoken: ourtoken,
      },
    };

    axios(requestOption).then((response) => {
      console.log(response);
      setAccountList(response.data.res_list);
    });

참고: API명세서

오픈뱅킹 API

사용자인증 API

  GET https://testapi.openbanking.or.kr/oauth/2.0/authorize
ParameterTypeDescription
response_type고정값: codeRequired. OAuth 2.0 인증 요청 시 반환되는 형태
client_id<client_id>Required. 오픈뱅킹에서 발급한 이용기관 앱의 Client ID
redirect_urihttp://localhost:3000/authResultRequired. 사용자인증이 성공하면 이용기관으로 연결되는 URL
scopelogin inquiry transferRequired. Access Token 권한 범위
state12345678901234567890123456789012Required. CSRF 보안위협에 대응하기 위해 이용기관이 세팅하는 난수값
auth_type0Required. (0:최초인증, 1:재인증, 2:인증생략)

사용자 토큰발급 API

  POST https://testapi.openbanking.or.kr/oauth/2.0/token
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
ParameterTypeDescription
code<authorization_code>Required. 사용자인증 성공 후 획득한 Authorization Code
client_id<client_id>Required. 오픈뱅킹에서 발급한 이용기관 앱의 Client ID
client_secret<client_secret>Required. 오픈뱅킹에서 발급한 이용기관 앱의 Client Secret
redirect_urihttp://localhost:3000/authResultRequired. Access Token 을 전달받을 Callback URL
grant_type고정값: authorization_codeRequired. 3-legged 인증을 위한 권한부여 방식 지정

사용자정보조회 API

  GET https://testapi.openbanking.or.kr/v2.0/user/me
HeaderTypeDescription
AuthorizationBearer <access_token>Required. 오픈뱅킹으로부터 전송받은 Access Token 을 HTTP Header 에 추가
ParameterTypeDescription
user_seq_no고정값: codeRequired. 사용자일련번호, 토큰 발급받은 후 응답메세지에 포함

계좌조회 API

  GET https://testapi.openbanking.or.kr/v2.0/account/balance/fin_num
HeaderTypeDescription
AuthorizationBearer <access_token>Required. 오픈뱅킹으로부터 전송받은 Access Token 을 HTTP Header 에 추가
ParameterTypeDescription
bank_tran_id거래고유번호Required. 이용기관코드+U+이용기관부여번호
fintech_use_num고정값: codeRequired. 핀테크이용번호
tran_dtime20230914101010Required. 요청일시

거래내역조회 API

  GET https://testapi.openbanking.or.kr/v2.0/account/transaction_list/fin_num
HeaderTypeDescription
AuthorizationBearer <access_token>Required. 오픈뱅킹으로부터 전송받은 Access Token 을 HTTP Header 에 추가
ParameterTypeDescription
bank_tran_id거래고유번호Required. 이용기관코드+U+이용기관부여번호
fintech_use_num고정값: codeRequired. 핀테크이용번호
inquiry_typeARequired. 조회구분코드 - “A”:All, “I”:입금, “O”:출금
inquiry_baseDRequired. 조회기준코드 - “D”:일자, “T”:시간
from_date20201212Required. 조회시작일자
to_date20230901Required. 조회종료일자
sort_orderDRequired. 정렬순서 - “D”:Descending, “A”:Ascending
tran_dtime20230914101010Required. 요청일시

출금이체 API

  POST https://openapi.openbanking.or.kr/v2.0/transfer/withdraw/fin_num
Content-Type: application/json; charset=UTF-8
HeaderTypeDescription
AuthorizationBearer <access_token>Required. 오픈뱅킹으로부터 전송받은 Access Token 을 HTTP Header 에 추가
ParameterTypeDescription
bank_tran_id거래고유번호Required. 거래고유번호
cntr_account_typeNRequired. 약정 계좌/계정 구분 - “N”:계좌, “C”:계정
cntr_account_num100000000001Required. 약정 계좌/계정 번호
dps_print_content쇼핑몰환불Required. 입금계좌인자내역
fintech_use_numfintech_use_numRequired. 출금계좌핀테크이용번호
wd_print_content오픈뱅킹출금Required. 오픈뱅킹에서 발급한 이용기관 앱의 Client Secret
tran_amt1000Required. 거래금액
tran_dtime고정값: authorization_codeRequired. 요청일시
req_client_name홍길동Required. 요청고객성명
req_client_fintech_use_numreq_client_fintech_use_num요청고객핀테크이용번호
req_client_numHONGGILDONG1234Required. 요청고객회원번호
transfer_purposeSTRequired. 이체용도
recv_client_name유관우최종수취고객성명
recv_client_bank_code097최종수취고객계좌 개설기관.표준코드
recv_client_account_num100000000001최종수취고객계좌번호

입금이체 API

  POST https://testapi.openbanking.or.kr/v2.0/transfer/deposit/fin_num
Content-Type: application/json; charset=UTF-8
HeaderTypeDescription
AuthorizationBearer <access_token>Required. 오픈뱅킹으로부터 전송받은 Access Token 을 HTTP Header 에 추가
  • 단, scope: oob으로 토큰을 재발급
ParameterTypeDescription
cntr_account_typeNRequired. 약정 계좌/계정 구분 - “N”:계좌, “C”:계정
cntr_account_num200000000001Required. 약정 계좌/계정 번호
wd_pass_phraseNONERequired. 입금이체용 암호문구
wd_print_content환불금액Required. 출금계좌인자내역
name_check_optionoffRequired. 수취인성명 검증 여부 - “on”:검증함, “off”:미검증 (미지정 시 기본값: "on")
tran_dtime20230812130000Required. 요청일시
req_cnt1Required. 입금요청건수 - 입금요청건수는 1 건만 신청이 가능함.
req_list``Required. 입금요청목록
--tran_no1Required. 거래순번
--bank_tran_idbank_tran_idRequired. 거래고유번호
--fintech_use_numfintech_use_numRequired. 핀테크이용번호
--print_content오픈서비스캐시백Required. 입금계좌인자내역
--tran_amt1000Required. 거래금액
--req_client_name홍길동Required. 요청고객성명
--req_client_fintech_use_numreq_client_fintech_use_num요청고객핀테크이용번호주
--req_client_numHONGGILDONG1234Required. 요청고객회원번호
--transfer_purposeSTRequired. 이체용도

2legged (scope: oob으로 토큰을 재발급)

  POST https://testapi.openbanking.or.kr/oauth/2.0/token
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
ParameterTypeDescription
client_idclient_idRequired. 거래고유번호
client_secretclient_secretRequired. 약정 계좌/계정 구분 - “N”:계좌, “C”:계정
scope고정값 oobRequired. Access token 권한 범위
grant_type고정값 client_credentialsRequired. 2-legged 인증을 위한 권한부여 방식 지정
profile
`나는 ${job} 개발자`

5개의 댓글

comment-user-thumbnail
2023년 12월 12일

안녕하세요 좋은글 감사합니다.
오픈뱅킹 API 사용하여 실제 본인 계좌나 카드내역 조회는 안되는것인가요? test-bed 내에서만 활용 가능한지 궁금합니다.

1개의 답글
comment-user-thumbnail
2023년 12월 25일

사이트에서 개발관련문서 조회 권한이 없던데 따로 인증받으신건지 궁금합니다

1개의 답글
comment-user-thumbnail
2024년 6월 11일

안녕하세요. 혹시 테스트 데이터 승인을 따로 메일을 통해 받으신건가요? 사용자 인증을 해서 계좌 만드는 것까지는 되는데, 테스트 데이터가 없어서 그런지 계좌 조회부터는 막히네요..

답글 달기