개발소스 이관 2

2020년 8월 14일 금요일

웹프런트 프로젝트

개요

  • React.js로 만들어졌습니다.
  • HTML/JS로만 이루어져있습니다. 데이터(비니니스로직)은 API프로젝트와 AJAX로 주고받습니다.

구조

  1. 프레임워크

React.js

  1. 주요서드파티
  • Ant Design : UI Framework
  • Material-UI : UI Framework
  • ag-Grid : 그리드 컴포넌트
  • Recharts : 차트 컴포넌트
  • Froala : 웹에디터 (상용)
  • FullCalendar : 작업달력
  • React Hook Form : Form작업(데이터 입력,수정)

폴더 구조

components

컴포넌트 (그리드, UI컨트롤등)

containers

고정 페이지 (404에러, 403에러, 로그인등)

pages

구현된 페이지

style

CSS 파일

utils

유틸 (ajax통신, 세션, 메뉴, 유틸함수등)

설정 파일

  • .env
// API프로그램 URL
REACT_APP_BASE_URL=http://fms.ezwork.kr
// 웹프론트 프로그램 도메인
REACT_APP_SITE_DOMAIN=fms.ezwork.kr
// 본사 SiteId
REACT_APP_ADMIN_SITE=100
// 로컬 스토리지 종류 (local, session)
// local : 브라우저에 저장됨, session : 브라우저 탭별로 저장됨 (브라우저를 닫으면 사라짐)
REACT_APP_STORAGE=local
// 빌드할때 소스맵을 생성하지 않음 (빌드시간 단축)
// 소스맵 : 빌드한 소스상에서 원본 소스를 추적할 때 사용(디버깅용)
GENERATE_SOURCEMAP=false

주요 컴포넌트

CustomFormModal

윈도우, 스타일을 재정의한 Modal 윈도우창

Grid

그리드, ag-grid를 이용해서 페이징, 정렬, 넘버링등을 구현되었습니다.

Editor

웹에디터, Froala Editor를 이용해서 업로드파일등을 추가되었습니다.

EntitySelect/MUIEntitySelect

셀렉트박스, DB데이터를 불러오는 셀렉트박스, AntD와 MUI 두가지로 구현되었습니다.

PopupSearch

팝업검색 선택창, 검색 + 그리드 + 모달을 이용해 각각 자원에 맞게 구현되었습니다.

주요 유틸

fetch.js

ajax통신 모듈, axios를 이용합니다

menu.js

메뉴로딩 모듈, api를 호출하여 메뉴정보를 정리합니다

polyfill.js

구버전IE를 위한 polyfill 모듈입니다

session.js

세션정보를 저장한 모듈입니다. 로그인정보, 사이트환경설정, localStorage/sessionStorage를 이용합니다.

weather.js

날씨정보 모듈, api를 호출하여 날씨정보를 가져오고 텍스트형태로 정리/표현합니다.

util.js

유틸 함수 모음, 형식변환 형식검증(validate)등 간단한 유틸함수 모음입니다.

URL 경로

페이지의 URL경로는 pages 가 URL 루트(/)경로 입니다. 기본적으로 index.js가 실행됩니다.. * 예시 /pages/Material/material/index.js 의 URL 경로는 /Material/material 입니다.

App.js

프로그램의 시작되는 부분입니다. 로그인체크, 본사/사옥모드, Route 설정등이 이곳에서 이루어집니다.

1) 실행 순서

1. 로그인체크
2. 메뉴로딩
3-1. 사이트 환경설정 로딩
3-2. Route에 맞는 페이지 컨포넌트 렌더링

2) Route 설정 (URL매핑)

react-router-dom (v5)를 이용하여 연결하고 있습니다. containers, pages 밑의 페이지들을 연결합니다.

<Switch>
  <Route path="/login" name="로그인" icon="login" component={Login} />
  <Route path="/logout" name="로그아웃" icon="logout" component={Logout} />
  <Route path="/siteconnect" name="현장연결" component={SiteConnect} />
  <Route path="/" name="" component={Home} exact />
  <RouteLayout
    menu={menu}
    selected={selected}
    path="/:layout(admin|site)/sitemap/search"
    name="메뉴검색"
    component={SitemapSearch}
  />
  <RouteLayout
    menu={menu}
    selected={null}
    path="/:layout(admin|site)"
    name="대시보드"
    component={Dashboard}
    exact
  />
  {routes.map((route) => {
    if (route.path && route.path != '/' && route.path != '/login') {
      return <RouteLayout menu={menu} selected={selected} {...route} exact />
    }
  })}
  <Route component={NotFound} />
</Switch>

containers 의 고정페이지들을 연결합니다. pages 하단의 페이지는 routes 변수에 들어있습니다. loop돌려서 연결합니다. 전체화면에 표현되는 페이지는 기본 Route에 연결되고 관리자 레이아웃이 포함된 Route는 RouteLayout을 사용합니다. routes 변수는 fetchMenu함수(메뉴정보를 API에서 가져오는 함수)에서 메뉴목록을 정리하여 등록합니다.

3) 본사/사옥모드 결정

URL로 본사/사옥 페이지를 결정합니다. /admin 으로 시작하면 본사모드, /site 로 시작하면 사옥모드입니다.

if (!session.getType()) {
  if (document.location.pathname.match(/^\/admin/)) {
    session.setType('admin')
  } else if (document.location.pathname.match(/^\/site/)) {
    session.setType('site')
  } else {
    const info = session.getUserInfo()
    if (info && info.IsAdmin) {
      session.setType('admin')
    } else {
      session.setType('site')
    }
  }
}

기본 페이지 구성

기본적인 페이지 화면구성은 리스트(검색), 폼입력, 폼수정입니다.

  1. 구성파일

index.js

리스트 화면입니다. Grid 컴포넌트로 리스트를 표현합니다.

filterForm.js

리스트내에 검색창입니다. 오른쪽 상단에 검색아이콘을 클릭하면 나타납니다.

detailModal.js

입력/수정폼, 상세조회폼을 가지고 있는 모달창입니다.

detailForm.js

상세조회폼입니다.

editForm.js

입력/수정폼입니다. React Hook Form을 이용해서 간결하게 구현되었습니다.

reducer.js

기본 Redux 리듀서입니다. 모달창(detailModal)을 호출할때 이용합니다.

  1. 흐름
  • index.js 리스트가 렌더링됩니다.
  • 리스트에서 등록클릭시
    1. detailModal 호출
    2. editForm(입력폼) 렌더링
  • Grid Row 클릭시
    1. detailModal 호출
    2. detailForm(상세조회폼) 렌더링
  • detailModal에서 수정클릭시 editForm(수정폼) 렌더링
  1. 모달창 호출

기본 리듀서에 정의된 detailModalShowAction 액션을 디스패치하여 호출합니다.

import { detailModalShowAction } from './reducer'

적용 예시

const detailModalShow = (payload) => {
  dispatch(detailModalShowAction(payload))
}

// Row클릭시 detail화면 보여주기
const onRowClicked = (event) => {
  const data = event.data
  detailModalShow({ visible: true, type: 'detail', id: [data.SiteId, data.MaterialId] })
}
const onChangeForm = (type, id) => {
  dispatch(detailModalShowAction({ visible: true, type: type, id: id }))
}

// 수정클릭시 edit화면 보여주기
const onEditClick = (e) => {
  onChangeForm('edit', id)
  e.preventDefault()
}
  1. 리스트 Grid 컴포넌트 사용

API프로젝트를 통해 list데이터(JSON)을 받아와서 렌더링됩니다. Grid컴포넌트 내부에서 fetch모듈을 사용하여(ajax) 주어진 apiUrl의 값을 가져옵니다.

apiUrl 정의

// listApi는 권한에 따라(auth), 선택한 사옥에 따라 querystring을 채워줍니다.
const apiUrl = session.listApi('/api/Material', auth, { siteIds: siteIds })

// auth에 업무필드가 설정되어있다면 businessFieldId가 추가됩니다.
// 사옥을 여러개 선택했다면 siteIds가 추가됩니다.
// 결과값 -> /api/Material?businessFieldId=102&siteIds[0]=101&siteIds[1]=105

api프로젝트에서 주는 list데이터 샘플 JSON포맷입니다. 이 데이터를 Grid 컴포넌트에서 읽어서 리스트형태로 보여줍니다.

{
  "page": 1,
  "total": 219,
  "limit": 5,
  "pages": 44,
  "list": [
    {
      "EquipmentId": 3909,
      "SiteId": 115,
      "SiteName": "부평",
      "EquipmentTypeId": 2,
      "EquipmentTypeName": "전동공구",
      "Name": "휴대용청소기",
      "Standard": "10.8V",
      "Unit": "EA",
      "StoredCount": 1,
      "AddDate": "2020-07-24T14:39:20.543",
      "SupplierName": "BLACK&DECKER",
      "SupplierPhoneNo": "",
      "WarehouseId": 170,
      "WarehouseName": "지하1방재실-4",
      "CurrentStockCount": 0,
      "TotalStockCount": 1,
      "RentCount": 1,
      "LossCount": 0,
      "UpdateDate": "2020-07-24T14:41:26.977"
    },
    {
      "EquipmentId": 2322,
      "SiteId": 115,
      "SiteName": "부평",
      "EquipmentTypeId": 7,
      "EquipmentTypeName": "수공구",
      "Name": "파이프렌치",
      "Standard": "12\"",
      "Unit": "EA",
      "StoredCount": 2,
      "AddDate": "2020-06-10T17:26:21.257",
      "SupplierName": "세신버팔로",
      "SupplierPhoneNo": "",
      "WarehouseId": 39,
      "WarehouseName": "지하7자재창고-1",
      "CurrentStockCount": 0,
      "TotalStockCount": 2,
      "RentCount": 2,
      "LossCount": 0,
      "UpdateDate": "2020-06-14T18:58:06.65"
    },
    ...
  ]
}

Grid 컬럼 정보 설정

const columnDefs = [
  {
    headerName: '번호',
    field: 'rownum',
    colId: 'Material.MaterialId',
    width: 80,
    sort: 'desc',
  },
  {
    headerName: '사옥',
    field: 'SiteName',
    colId: 'CmSite.Name',
    width: 150,
    hide: !session.isAdmin(),
  },
  {
    headerName: '업무분야',
    field: 'BusinessFieldName',
    colId: 'CmBusinessField.Name',
    width: 100,
  },
  {
    headerName: '자재(대)',
    field: 'FirstClassName',
    colId: 'FmsMaterialCodeClass.Name',
    width: 100,
  },
  {
    headerName: '자재(중)',
    field: 'SecondClassName',
    colId: 'FmsMaterialCodeClass1.Name',
    width: 100,
  },
  {
    headerName: '자재(소)',
    field: 'ThirdClassName',
    colId: 'FmsMaterialCodeClass2.Name',
    width: 100,
  },
  { headerName: '자재명', field: 'Name', width: 150 },
  {
    headerName: '규격',
    field: 'Standard',
    width: 100,
  },
  {
    headerName: '단위',
    field: 'Unit',
    width: 70,
  },
  {
    headerName: '단가',
    field: 'FinalPrice',
    colId: 'Material.FinalPrice',
    width: 100,
    format: 'price',
  },
  {
    headerName: '적정재고량',
    field: 'ReasonableStockCount',
    width: 100,
    format: 'price',
    hide: !siteConfig.UseMaterialReasonableStockCount,
  },
  {
    headerName: 'QR코드',
    field: 'QrCode',
    width: 80,
  },
  {
    headerName: '내용연수',
    field: 'DurableYears',
    width: 80,
  },
  {
    headerName: '제조사',
    field: 'Manufacturer',
    width: 80,
  },
  {
    headerName: '비고',
    field: 'Note',
    width: 80,
  },
  {
    headerName: '단종여부',
    field: 'IsDiscontinued',
    width: 80,
  },
]

기본적으로 ag-grid의 컬럼설정을 따릅니다. 그외에 Grid 컴포넌트에서 sort, format, align, hide 등이 추가되었습니다.

sort : asc/desc 초기정렬상태를 정의합니다. colId값을 기준으로 정렬합니다. (colId가 없으면 field)
format : price 가격형태의 값을 표현할때 정의합니다.
align: left/center/right 텍스트정렬상태를 정의합니다.
hide: true/false 컬럼을 숨깁니다.

그외에 특수값
field: 'rownum' Row번호를 표현합니다.

필터 정보 설정 (검색조건)

//filter 구조
{
  field : 필드값,
  op: [eq|ne|cn|sw|ew|gt|ge|lt|ge|in],
  value :}

// 조건 op 종류
eq: =
ne: !=
cn: LIKE %%
sw: LIKE%
ew: LIKE %gt: >
lt: <
ge: >=
le: <=
in: IN (1,2,3)

주어진 그리드 설정값으로 렌더링합니다.

<GridList
  ref={gridRef}
  apiUrl={apiUrl}
  options={{ columnDefs, filters, defaultSort: 'BusinessAndClass' }}
  pageSize={15}
  onRowClicked={onRowClicked}
/>