Coder Social home page Coder Social logo

pre-onboarding-7th-3-2-3's Introduction

원티드 프리온보딩 프론트엔드 3팀 - Assignment #5

투자 관리 서비스의 관리자 기능 서비스

프로젝트 기간 : 2022년 11월 12일 ~ 2022년 11월 18일

아래의 아이디와 비밀번호로 접속해주세요.

아이디:[email protected]
비밀번호:12341234

🚨 사용자 목록에서 계좌수를 json-server api를 이용해 불러오려 했으나 실패하여 결국 Math.random 으로 구현했으니 참고바랍니다. 나중에 이 부분은 꼭 구현해서 수정하도록 하겠습니다!

🚨 배포 사이트가 열리지 않습니다. heroku에 배포하였으나 일정기간만 서비스 운영을 하기 때문입니다. aws의 ec2로 배포를 진행하고자 합니다


👁‍🗨데모

로그인 계좌정보 - 네비게이션 사용자 상세페이지 - 수정
로그인 계좌정보 네비게이션 사용자 상세 페이지 열람 및 수정
사용자목록 - 추가 사용자목록 - 삭제
사용자 추가 사용자 삭제

📖 목차


⌨️ 실행 방법

$ git clone https://github.com/pre-onboarding-frontend-7-team-3/pre-onboarding-7th-3-2-3.git
$ npm install
$ npm run dev

📃 협업 과정

  1. 비동기적 소통을 위해 노션 워크스페이스에서 프로젝트를 페이지와 컴포넌트로 나누고 미팅 로그와 주요 코드를 공유하여 개발 효율을 높이고자 노력했습니다.

    노션 링크

  2. 본 프로젝트는 동료학습에 최적화된 과정을 찾아가며 진행했습니다. VSC Live Code extension을 활용하여 라이브 코드 리뷰를 진행하고 각자 구현한 코드에 대한 피드백을 진행하여 Best Practice를 추가해 나가는 과정을 거쳤습니다. 후의 리팩토링도 동일한 과정을 거쳐 진행하였습니다.

  3. 소통 플랫폼으로 게더타운과 디스코드를 활용해서 협업을 진행했습니다.


☑️ Best Practice 및 채택 근거

1. TypeScript

  • TypeScript는 정적 타입을 지원하므로 컴파일 단계에서 오류를 포착할 수 있는 장점이 있습니다. 코드의 가독성을 높이고 예측할 수 있게 하며 디버깅이 쉽다는 장점에 모두 공감해서 채택했습니다. 명시적인 정적 타입 지정은 팀 단위로 협업 시에 의도를 명확하게 코드로 기술할 수 있다는 점에서도 의견을 모았습니다.

2. TanStack React-Query

  • 서버 상태와 비동기 호출을 react query로 관리하였고 데이터의 형태와 적합성 그리고 앱의 동작 흐름을 고려하여 stale timecache time을 설정했습니다.

  • 계좌목록에서는 금액과 같이 변동성이 큰 데이터는 staleTime과 cacheTime을 짧게(2000ms) 설정해서 최신 데이터를 받아오도록 했습니다.

  • 사용자 목록에서는 민감한 정보를 다루지 않기 때문에 staleTime과 cacheTime 을 길게 설정하고 사용자 추가, 수정, 삭제가 될 때 invalidateQueries를 사용, 데이터를 비교해 최신의 데이터를 UI로 출력하게 했습니다.

  • 캐싱 기능을 제공하는 react query의 장점을 살리기 위해, 다음 페이지에 대한 data를 prefetch하여, 페이지 이동 시, prefetch된 데이터를 바로 보여줄 수 있게 하였습니다.

    3-2 캐싱 및 stale time

3. API 함수 관리(model, query, repository) 및 OOP

  • 계좌 정보와 유저 정보 관련 api 코드들을 분리하여 작성하여 추상화했습니다. api코드와 query코드를 추상화하여 관심사를 분리하고 재사용성을 높였습니다.

  • react-query는 캐시받아 저장하고 다루기 때문에 DB 형태와 비슷하다고 판단했습니다. 기본적으로 스키마를 정의하고, 불러오는 레파지토리를 만들고, DB 에 넣는 쿼리를 가지는 형태의 데이터 저장방식을 모방해 현재의 아키텍쳐를 적용 반영했습니다.

  • 레파지토리를 class 객체로 만들게 된 이유는 서로 비슷한 기능을 하는 api 들이기 때문에 parameter 설정이나 기타 axios 설정 값을 공유할 수 있을 거라는 예측에 기반했습니다. 각 api 호출 기능들을 멤버함수로써 다뤄 api안에 api를 호출될 수 있는 상황을 대비했습니다.

model.ts

query.ts

repository.ts

4. 전역상태관리(Jotai)

  • 계좌목록 페이지 내 검색을 포함한 필터링 데이터 상태 관리에 Jotai를 사용했습니다.

  • 내장 hook useAtom을 사용해 accountQueryParams state를 관리하며 페이지의 이동과 새로고침에도 유지되도록 구현하였습니다.

    3-2 필터링

5. 클라이언트 환경에서 예외처리(React Hook Form)

  • Log in 페이지에 있는 input과 사용자추가시 팝업으로 뜨는 modal 의 input에 react-hook-form을 적용하여 input에 입력되는 value의 validation을 체크했습니다.

  • validation이 맞지 않으면 form이 submit되지 못하도록 했습니다.

  • 그리고 validation이 맞아서 submit에 성공하여 서버로 정보가 전송되었다 하더라도 서버에서 에러가 발생했을 경우, 에러 modal이 팝업되도록 했습니다.

    신규유저 modal 예외처리

6. API 호출 횟수 최적화

  • 검색창에 검색어를 입력했을 때 onChange 이벤트가 발생할 때마다 서버에 GET 요청을 보내는 것은 비효율적인 프로세스라고 공통된 의견을 나누었습니다.

  • 따라서 첫 onChange 이벤트의 발생 시점으로부터 의도적인 지연시간을 두어 API 호출 횟수를 줄였습니다.

  • 검색창의 onChange 이벤트가 비동기적으로 input의 상태 값을 업데이트하되, 사용자가 입력한 검색 결과에 대한 비동기 요청은 디바운싱 함수에서 설정한 시간(600ms)이 지난 뒤에 최종적으로 업데이트된 상태 값을 쿼리 스트링으로 보내 호출되게 구현했습니다.

    3-2 디바운싱

    import { useEffect, useState } from 'react';
    const useDebounce = (
    callback: Function,
    value: string,
    delay: number = 600
    ) => {
    useEffect(() => {
    const timer = setTimeout(() => {
    callback();
    }, delay);
    return () => clearTimeout(timer);
    }, [value, delay]);
    };
    export default useDebounce;

7. 페이지 별 접근 권한 관리

  • 라우팅 페이지에서 Route상단에 RequireAuth컴포넌트를 두어 페이지별 인가를 구현하였습니다.

  • RequireAuth 컴포넌트 내부에서 props를 통해 전달받은 isAuthRequire와, 로그인 성공 후 cookies에 저장하는 access_token 값의 유무를 기준으로 4단계로 나누어 리다이렉팅 하였습니다.

  • server에 존재하는 authorize token의 만료 시간이 지나면 관련한 모든 api의 호출에서 401.

  • 따라서 기존에 로그인 후 localStorage 저장하던 access_token을 서버의 authorize token 만료시간(1시간)과 일치시켜 로그아웃 될 수 있도록 하였습니다.

import { Outlet, Navigate } from 'react-router-dom';
import Cookies from 'universal-cookie';
interface Props {
isAuthRequired: boolean;
redirectUrl: string;
}
const RequireAuth = ({ isAuthRequired, redirectUrl }: Props) => {
const cookies = new Cookies();
const accessToken = cookies.get('access_token');
if (isAuthRequired && !accessToken) {
alert('토큰이 만료되었습니다.');
return <Navigate to={redirectUrl} replace />;
}
if (!isAuthRequired && !!accessToken) {
alert('이미 로그인 되었습니다.');
return <Navigate to={redirectUrl} replace />;
}
return <Outlet />;
};
export default RequireAuth;

🔒 팀 코드 컨벤션

  • git commit message
커밋명 내용
feat 파일, 폴더, 새로운 기능 추가
fix 버그 수정
docs 제품 코드 수정 없음
style 코드 형식, 정렬, 주석 등의 변경
refactor 코드 리팩토링
test 테스트 코드 추가
chore 환경설정, 빌드 업무, 패키지 매니저 설정등..
hotfix 치명적이거나 급한 버그 수정
remove 사용하지 않는 변수, 파일 etc 삭제
working 이미 만들어진 기능, 함수 작업중
merge branch merge
  • branch
브랜치명 내용
develop 파일, 폴더, 새로운 기능 추가
fix 버그 수정
docs 제품 코드 수정 없음
refactor 코드 리팩토링
hotfix 치명적이거나 급한 버그 수정
feat 새로운 기능 추가

🔨 사용 기술

HTML5 CSS3 JavaScript React TypeScript styled-components recoil Notionbadge badge


📦 폴더 구조

📂  src
│  ├─ App.tsx
│  ├─ apis
│  │  ├─ httpClient.ts
│  │  ├─ index.ts
│  │  └─ investmentService.ts
│  ├─ assets
│  │  └─ December&Company.jpeg
│  ├─ components
│  │  ├─ InvestmentAccountList
│  │  │  ├─ Account-query
│  │  │  │  ├─ InvestmentAccount.model.ts
│  │  │  │  ├─ InvestmentAccount.query.ts
│  │  │  │  └─ InvestmentAccount.repository.ts
│  │  │  ├─ InvestmentAccountItem
│  │  │  │  └─ InvestmentAccountItem.tsx
│  │  │  ├─ InvestmentAccountList.style.ts
│  │  │  ├─ InvestmentAccountList.tsx
│  │  │  ├─ atoms
│  │  │  │  └─ index.ts
│  │  │  └─ index.ts
│  │  ├─ LoginForm
│  │  │  ├─ Login-query
│  │  │  │  ├─ Login.query.ts
│  │  │  │  └─ Login.repository.ts
│  │  │  ├─ LoginErrorModal
│  │  │  │  ├─ LoginErrorModal.style.ts
│  │  │  │  └─ LoginErrorModal.tsx
│  │  │  ├─ LoginForm.style.ts
│  │  │  ├─ LoginForm.tsx
│  │  │  ├─ LoginInput
│  │  │  │  ├─ LoginInput.style.tsx
│  │  │  │  └─ LoginInput.tsx
│  │  │  └─ index.ts
│  │  ├─ NewUserModal
│  │  │  ├─ FileInput
│  │  │  │  ├─ FileInput.style.ts
│  │  │  │  └─ FileInput.tsx
│  │  │  ├─ FunnelButton
│  │  │  │  ├─ FunnelButton.style.ts
│  │  │  │  └─ FunnelButton.tsx
│  │  │  ├─ NewUserModal.style.ts
│  │  │  ├─ NewUserModal.tsx
│  │  │  ├─ UserInput
│  │  │  │  ├─ UserInput.style.ts
│  │  │  │  └─ UserInput.tsx
│  │  │  └─ index.ts
│  │  ├─ UserDetail
│  │  │  ├─ UserDetail.tsx
│  │  │  ├─ UserDetailTableItem.tsx
│  │  │  ├─ UserInfoTable.tsx
│  │  │  ├─ index.ts
│  │  │  └─ types.ts
│  │  ├─ UserList
│  │  │  ├─ DeleteModal
│  │  │  │  ├─ DeleteModal.tsx
│  │  │  │  └─ index.ts
│  │  │  ├─ UserList.style.ts
│  │  │  ├─ UserList.tsx
│  │  │  ├─ UserTableItem
│  │  │  │  └─ UserTableItem.tsx
│  │  │  ├─ atoms
│  │  │  │  └─ index.ts
│  │  │  └─ index.ts
│  │  └─ common
│  │     ├─ Dropdown
│  │     │  ├─ Dropdown.style.ts
│  │     │  └─ Dropdown.tsx
│  │     ├─ Header
│  │     │  ├─ Header.style.ts
│  │     │  └─ Header.tsx
│  │     ├─ Icons
│  │     │  ├─ Lock.tsx
│  │     │  ├─ Logo.tsx
│  │     │  ├─ User.tsx
│  │     │  └─ index.ts
│  │     ├─ Layout
│  │     │  ├─ Layout.style.ts
│  │     │  └─ Layout.tsx
│  │     ├─ Loader
│  │     │  ├─ Loader.style.ts
│  │     │  └─ Loader.tsx
│  │     ├─ PageContainer
│  │     │  ├─ PageContainer.style.ts
│  │     │  └─ PageContainer.tsx
│  │     ├─ PagenationButton
│  │     │  └─ PagenationButton.tsx
│  │     ├─ SEO
│  │     │  └─ SEO.tsx
│  │     ├─ SearchInput
│  │     │  ├─ SearchInput.style.ts
│  │     │  └─ SearchInput.tsx
│  │     ├─ Sider
│  │     │  ├─ Sider.style.ts
│  │     │  └─ Sider.tsx
│  │     └─ Table
│  │        ├─ CustomTableBody.style.ts
│  │        ├─ CustomTableBody.tsx
│  │        └─ CustomTableHead.tsx
│  ├─ constants
│  │  ├─ NewUserInputData.ts
│  │  ├─ dropDownData.ts
│  │  ├─ funnelButtonData.ts
│  │  ├─ routes.ts
│  │  ├─ siderData.ts
│  │  └─ tableData.ts
│  ├─ hooks
│  │  ├─ useDebounce.ts
│  │  ├─ useSignForm.ts
│  │  └─ useUnmountIfClickedOutside.ts
│  ├─ libs
│  │  └─ api
│  │     ├─ auth.ts
│  │     ├─ client.ts
│  │     └─ user.ts
│  ├─ main.tsx
│  ├─ pages
│  │  ├─ InvestmentAccounts
│  │  │  ├─ InvestmentAccounts.tsx
│  │  │  └─ index.ts
│  │  ├─ NotFound
│  │  │  ├─ NotFound.style.ts
│  │  │  ├─ NotFound.tsx
│  │  │  └─ index.ts
│  │  ├─ UserDetail
│  │  │  ├─ UserDetail.tsx
│  │  │  └─ index.ts
│  │  ├─ UserList
│  │  │  ├─ UserList.tsx
│  │  │  └─ index.ts
│  │  └─ login
│  │     ├─ Login.tsx
│  │     └─ index.ts
│  ├─ shared
│  │  └─ User-query
│  │     ├─ User.model.ts
│  │     ├─ User.query.ts
│  │     └─ User.repository.ts
│  ├─ store
│  │  └─ sider.ts
│  ├─ styled.d.ts
│  ├─ styles
│  │  ├─ GlobalStyles.ts
│  │  └─ Theme.ts
│  ├─ utils
│  │  ├─ assetsColorDecider.ts
│  │  ├─ auth
│  │  │  ├─ RequireAuth.tsx
│  │  │  └─ httpResponseUtils.ts
│  │  ├─ convertDate.ts
│  │  ├─ formatBoolean.ts
│  │  ├─ processData.ts
│  │  └─ validator.ts
│  └─ vite-env.d.ts
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts

👨‍👩‍👧‍👦 팀원

고영훈
(팀장)
조은지
(팀원)
김창희
(팀원)
박정민
(팀원)
YeonghunKO Joeunji0119 PiperChang ono212
YeonghunKO Joeunji0119 PiperChang ono212
문지원
(팀원)
이상민
(공지)
이지원
(팀원)
조수진
(팀원)
moonkorea00 dltkdals224 365supprot suzz-in
moonkorea00 dltkdals224 365support suzz-in

pre-onboarding-7th-3-2-3's People

Contributors

yeonghunko avatar moonkorea00 avatar 365support avatar lc-c-ln avatar dltkdals224 avatar ono212 avatar

pre-onboarding-7th-3-2-3's Issues

userList에서 userData에 id 추가

 const userData = useMemo(
    () =>
      defaultUserData?.data?.map((data: any) => ({
        name: data.name,
        account_count: '계좌수',
        email: data.email,
        gender_origin: GENDER[data.gender_origin],
        birth_date: data.birth_date.split('').slice(0, 10),
        phone_number: data.phone_number,
        last_login: data.last_login.split('').slice(0, 10),
        allow_marketing_push: formatBoolean(
          data.userSetting[0].allow_invest_push
        ),
        is_active: formatBoolean(data.userSetting[0].is_active),
        created_at: data.created_at.split('').slice(0, 10),
        id: data.id,
      })),
    [defaultUserData]
  );

generateData.ts

const { faker } = require('@faker-js/faker')
const fs = require('fs')
const brokers = require('./brokers.json')
const accountStatus = require('./accountStatus.json')

faker.setLocale('ko')

const users = []
const userSetting = []
const accounts = []

const brokercode = Object.keys(brokers)
const accountStatusCode = Object.values(accountStatus)

for (let i = 1; i < 101; i++) {
  // generate fake users and settings
  const id = faker.datatype.number()
  const uuid = faker.datatype.uuid()
  const user = {
    id: i,
    uuid,
    photo: faker.internet.avatar(),
    name: faker.name.fullName(),
    email: faker.internet.email(),
    age: faker.datatype.number({ min: 20, max: 66 }),
    gender_origin: faker.datatype.number({ min: 1, max: 4 }),
    birth_date: faker.date.birthdate({ min: 20, max: 65, mode: 'age' }),
    phone_number: faker.phone.number('010-####-####'),
    address: `${faker.address.country()} ${faker.address.city()}`,
    detail_address: faker.address.streetAddress(true),
    last_login: faker.date.between('2022-01-01', '2022-08-01'),
    created_at: faker.date.between('2019-04-01', '2022-08-01'),
    updated_at: faker.date.between('2019-04-01', '2022-08-01'),
  }
  const setting = {
    id: i,
    userId: i,
    uuid,
    allow_marketing_push: faker.datatype.boolean(),
    allow_invest_push: faker.datatype.boolean(),
    is_active: faker.datatype.boolean(),
    is_staff: faker.datatype.boolean(),
    created_at: faker.date.between('2019-04-01', '2022-08-01'),
    updated_at: faker.date.between('2019-04-01', '2022-08-01'),
  }
  users.push(user)
  userSetting.push(setting)

  // generate fake accounts
  for (let j = 1; j < faker.datatype.number({ min: 2, max: 11 }); j++) {
    const accountBrokerCode = brokercode.sort(() => 0.5 - Math.random())[0]
    const status = accountStatusCode.sort(() => 0.5 - Math.random())[0]
    const account = {
      id: j,
      userId: i,
      uuid: faker.datatype.uuid(),
      broker_id: accountBrokerCode,
      status,
      number: faker.finance.account(12),
      name: faker.finance.accountName(),
      assets: faker.finance.amount(200000, 1000000000),
      payments: faker.finance.amount(200000, 1000000000),
      is_active: faker.datatype.boolean(),
      created_at: faker.date.between('2019-04-01', '2022-08-01'),
      updated_at: faker.date.between('2019-04-01', '2022-08-01'),
    }
    accounts.push(account)
  }
}

const data = { users, userSetting, accounts }
console.log(JSON.stringify(data))
fs.writeFileSync('db.json', JSON.stringify(data))
console.log('...generated db.json')

useMutation의 모듈화 예시

function useTodoPostMutationQuery() {
  const queryClient = useQueryClient();
  //   console.log();

  return useMutation(addTodo, {
    onMutate: async (newTodo: Optional<ITodo>) => {
      await queryClient.cancelQueries(queryKeys.todos);
      const previouseTodoData = queryClient.getQueryData(queryKeys.todos);
      queryClient.setQueryData<ITodo[]>(queryKeys.todos, oldQueryData => {
        return [
          ...(oldQueryData as ITodo[]),
          { id: (oldQueryData as ITodo[]).length + 1, ...newTodo },
        ];
      });
      return {
        previouseTodoData,
      };
    },

    onError: (_error, _todo, context) => {
      queryClient.setQueriesData(queryKeys.todos, context?.previouseTodoData);
    },

    onSettled: () => {
      queryClient.invalidateQueries(queryKeys.todos);
    },
  });
}
import { useTodoPostMutationQuery } from '../../hooks/useAddTodoOptimisticMutaion';

function TodoForm() {
  const [inputVal, setInputVal, reset] = useInputState('');
  const { mutate } = useTodoPostMutationQuery();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    mutate({ isCompleted: false, todo: inputVal });
    reset();
  };

  return (
    <Paper style={{ width: '90%', margin: '1rem' }}>
      <form onSubmit={handleSubmit} style={{ display: 'flex' }}>
        <TextField
          fullWidth
          placeholder="Enter your Todo!"
          margin="normal"
          style={{ padding: '0 10px' }}
          value={inputVal}
          onChange={setInputVal}
        />
      </form>
    </Paper>
  );
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.