웰컴 페이지 | 회원가입 |
---|---|
로그인 | POS 커스터마이징 - 카테고리 등록 |
POS 커스터마이징 - 아이템 등록 | POS 메인 - 주문 내역 조회 |
POS 메인 - 주문 신규 등록 | POS 결제 - 현금 |
POS 결제 - 카드 | 대시보드 |
AI 컨설팅 | |
Salesync는 MSA 기반 Cloud POS 서비스로 어디서나 매장 관리를 할 수 있는 서비스입니다.
📁 frontend
├──── 📁 .github
│ ├──── 📁 ISSUE_TEMPLATE
│ │ ├──── 📄 ✅-feature-request.md
│ │ └──── 📄 🐞-bug-report.md
│ ├──── 📁 workflows
│ │ ├──── 📄 deploy.yml
│ │ └──── 📄 desc.txt
│ ├──── 📄 CODEOWNERS
│ └──── 📄 PULL_REQUEST_TEMPLATE
├──── 📁 beforeUIUX
├──── 📁 node_modules
├──── 📁 public
│ ├──── 📁 img
│ └──── 📄 index.html
├──── 📁 src
│ ├──── 📁 api
│ │ ├──── 📁 auth
│ │ │ ├──── 📁 info
│ │ │ ├──── 📁 login
│ │ │ └──── 📁 signup
│ │ ├──── 📁 dashboard
│ │ │ ├──── 📁 chart
│ │ │ ├──── 📁 consulting
│ │ │ │ └──── 📁 test
│ │ │ └──── 📁 costs
│ │ ├──── 📁 items
│ │ ├──── 📁 orders
│ │ ├──── 📁 pay
│ │ ├──── 📁 pos
│ │ │ ├──── 📁 category
│ │ │ └──── 📁 item
│ │ ├──── 📁 sale
│ │ ├──── 📄 BaseUrl.jsx
│ │ ├──── 📄 RefAPi.md
│ │ └──── 📄 Token.jsx
│ ├──── 📁 components
│ │ ├──── 📁 common
│ │ ├──── 📁 dashboard
│ │ ├──── 📁 func
│ │ ├──── 📁 order
│ │ ├──── 📁 payment
│ │ └──── 📁 pos
│ ├──── 📁 fonts
│ ├──── 📁 pages
│ ├──── 📁 popup
│ ├──── 📁 recoil
│ │ └──── 📁 atoms
│ ├──── 📁 routes
│ ├──── 📁 styles
│ ├──── 📄 App.css
│ ├──── 📄 App.jsx
│ ├──── 📄 index.css
│ └──── 📄 index.jsx
│──── 📄 .eslintrc.js
│──── 📄 .gitignore
│──── 📄 .prettierrc
│──── 📄 package-lock.json
└──── 📄 package.json
- .github: CI/CD 파이프라인, GitHub 워크플로우 및 협업 컨벤션
- beforeUIUX: UI/UX 개선 전 이미지
- node_modules: 프로젝트에 포함된 라이브러리들이 설치
- public: 컴파일이 필요없는 정적 파일들(프로젝트에 사용한 이미지 파일들)
- index.html: 가상 DOM을 위한 파일
- src:
- api: 백엔드와 통신하는 api 파일들(GET, POST, PUT, DELETE)
- auth: 사용자 인증과 관련된 api 파일들(info, login, signup)
- :
- BaseUrl.jsx: 자주 쓰이는 통신 URI를 환경변수로 선언
- RefApi.md: 각 API 요청 형식 컨벤션을 정리
- Token.jsx: 자주 쓰이는 token을 환경변수로 선언
- components: 각 페이지에 사용되는 컴포넌트들
- common: 모든 페이지에 공용으로 사용되는 컴포넌트들(header, footer, layout)이 위치
- :
- fonts: UI/UX용 폰트가 위치
- pages: 각 페이지의 JSX가 위치
- popup: 각 팝업의 JSX가 위치
- recoil: 전역으로 상태를 관리하기 위헤 사용한 recoil이 위치
- routes: 라우팅 컨벤션을 정의
- styles: 모든 페이지에 공용으로 사용되는 UI/UX들이 위치
- App.css: 화면 렌더링시에 보여지는 초기 css
- App.jsx: 화면 렌더링시에 보여지는 초기 컴포넌트
- index.css: App 컴포넌트를 document에서 id값이 root인 태그 안에 렌더링되는 css
- index.jsx: App 컴포넌트를 document에서 id값이 root인 태그 안에 렌더링
- api: 백엔드와 통신하는 api 파일들(GET, POST, PUT, DELETE)
- .eslintrc.js: eslint(airbnb형) 컨벤션을 정의
- .prettierrc: 가독성 좋은 코드를 위한 컨벤션을 정의
- package(-lock).json: 프로젝트 이름, 버전 및 라이브러리 목록이 표기
- 컴포넌트, 페이지, 팝업, API, 스타일: PascalCase
- Recoil: {기능}State.jsx
- Routing: Private(Public)Route.jsx
- CSS: styled-components(CSS-in-JS)
- 함수, 변수, 상수: camelCase
- 익명 함수: arrow function
- 이벤트 함수: on{이벤트}{기능}
- API: {기능}{요청방식}Api.jsx
- ESLint: .eslintrc 9E81 .js에 airbnb 형식 적용
- 컴포넌트 작성할 때 ⇒ 함수형 컴포넌트 사용
- 이벤트핸들러는 handler로 시작하도록 작성
- 변수명 작성할 때 이름 길어지더라도 무슨 뜻인지 의미를 나타내도록 작성
- handlerConfirm(x): 함수명만 보고 정확히 어떤 일을 하는지 알기 어려움
- handlerConfirmPasswordBlur: 함수명만 보고도 이 함수의 역할을 알 수 있음
POS는 거래 및 결제를 처리하는 비즈니스 도구로, 사용자들이 서비스를 사용하면서 안정적이고 신뢰감을 주기위해 블루 계열인 #00ADEF를 브랜드 색상으로 선택했습니다.
전체적인 애플리케이션 무드도 편안하고 안정된 환경을 제공하기 위해 눈에 부담을 주지 않는 블루 계열로 설정했습니다.
Salesync는 커스터마이징 가능한 웹 기반 POS를 제공합니다. sales는 판매, Sync는 Synchronization의 줄임말로 데이터 및 정보를 효율적으로 동기화하고 연동함을 의미합니다. 따라서 주문과 매출데이터를 강조하는 브랜드명으로 Salesync로 정했습니다.
두 단어의 첫 글자인 S를 강조하여 브랜드의 시각적인 식별성을 높여 쉽게 기억하도록 설정했습니다. 로고의 색상을 블루로 선택하여 전체적인 브랜드 컬러와의 일관성을 유지하고 안정성과 신뢰성을 전달했습니다.
Tech | Explanation |
---|---|
JSX | - 보기 쉽고 가독성이 좋은 JSX 형식의 코드를 사용했습니다. - 높은 활용도를 위해 JSX로 일부 컴포넌트 로직을 구현했습니다. |
React | - 재사용 가능한 컴포넌트를 작성하여 개발 효율성을 높였습니다. - React Router를 사용하여 업데이트 된 부분만 새로 렌더링했습니다. |
Recoil | - 코드 분할을 손상시키지 않고 앱 전역의 상태 관리하기 위해 Recoil을 선택했습니다. |
Axios | - API 호출을 위해 내장함수인 FETCH 대신 Axios를 선정했습니다. - 서버와의 데이터 통신을 비동기적으로 처리했습니다. |
styled components | - CSS를 별도의 파일로 두지 않고 하나의 모듈로 관리하기 위해 CSS-in-JS 방식을 채택했습니다. - 컴포넌트의 재사용을 위해 styled-components를 선정했습니다. |
AWS S3 | - 정적 파일로 구성된 React를 쉽게 호스팅할 수 있는 플랫폼인 Amazon S3 Bucket을 사용했습니다. - 가용성이 용이하며, 사용자에게 빠른 성능을 제공합니다. |
CloudFront | - Amazon S3와 CDN을 구성하여 성능을 최적화하고, ACM을 사용하여 SSL 인증서를 관리하기 위해 AWS CloudFront를 사용했습니다. |
GitHub Actions | - 손쉽게 워크플로우를 설정하고, 다양한 이벤트에 대한 트리거로 각 종 이슈의 원인을 탐색하기 위해 GitHub Actions로 파이프라인을 구성했습니다. |
BFF | - 응답 데이터를 클라이언트에서 요구되는 데이터로 파싱했습니다. - 데이터를 전송하는 과정에서 민감하거니 불필요한 데이터를 숨겼습니다. |
react-calendar | - 월별, 주별, 일별 등 다양한 표현 방식과 달력의 상태 관리를 용이하게 하기 위해 react-calendar를 선택했습니다. - 사용자에게 달력으로서 최적의 편리함을 제공합니다. |
react-modal | - 각 종 팝업을 위해 사용했으며, 이벤트 핸들링의 용이성과 중첩되어 열리는 것을 방지하는 모달 스택을 지원하는 react-modal을 사용했습니다. |
recharts | - 원형 그래프, 꺾은선 그래프 등 사용자가 직관적으로 이해하기 쉬운 대시보드 서비스를 위해 recharts를 사용했습니다. |
- 로그인
- 회원 가입 및 매장 정보 등륵
- 매장 정보 조회 및 수정
- 로그인 시 JWT 토큰 발급
- 카테고리 및 아이템 등록
- 카테고리 및 아이템 조회
- 카테고리 및 아이템 수정
- 카테고리 및 아이템 삭제
- 주문 등록 및 수정
- 미결제 테이블 조회
- 주문 전체 취소
- 일별 혹은 기간별 주문량 조회
- Item 서비스와 내부 통신
- 전체 매출 조회
- 일별 및 기간별 매출 조회
- Order 서비스와 내부 통신
- 아이템 원가 입력 및 수정
- 기간별 전체 매출 및 순이익 조회
- 컨설팅 조회
- Sale, Order, Item 서비스와 내부 통신
- Consulting 서비스의 Kafka Producer가 송신한 이벤트 수신하는 Kafka Consumer
- 하루 매출 정산 문자 전송
- 컨설팅
- Dashboard 서비스와 내부 통신
- 비동기 처리 후 결과값 이벤트 버스로 송신하는 Kafka Producer
- OpenAI API 사용을 위한 비동기 실행
Case1. ESLint
Failed to compile.
[eslint]
src/App.js
Line 3:5: Fragments should contain more than one child - otherwise, there’s no need for a Fragment at all react/jsx-no-useless-fragment
Line 3:5: 'React' must be in scope when using JSX react/react-in-jsx-scope
Line 3:5: JSX not allowed in files with extension '.js' react/jsx-filename-extension
Line 4:7: 'React' must be in scope when using JSX react/react-in-jsx-scope
src/index.js
Line 9:3: JSX not allowed in files with extension '.js' react/jsx-filename-extension
- 수정 후
function App() {
return <h1>GitHub Actions test v1</h1>;
}
export default App;
- 수정 후
:
rules: {
'react/react-in-jsx-scope': 'off',
'react/jsx-filename-extension': 'off'
},
:
- 재배포
- 버킷 웹 사이트 엔드포인트로 접근
Case2. API response
- 분리된 API 파일(StorePostApi.jsx)에서는 response 로그를 남길 경우 나오지만react파일(StorePage.jsx)에서 res 값을 찍을 경우 나오지 않음
:
StorePostApi(data)
.then(res =>{
if(res){
setUserCheck(true);
setTimeout(() => {
navigate('/signup/stores/pos');
}, 500);
} else{
throw new Error("토큰이 없습니다")
}
})
.catch(err=>{
console.error('API 호출 또는 토큰 발급 실패:',err)
})
:
- 수정 후
Case3. useEffect
:
useEffect(() => {
const fetchItems = async () => {
try {
const itemData = await ItemGetApi();
setItems(itemData); // 가져온 아이템 목록으로 상태 업데이트
} catch (err) {
console.error(err);
}
};
// selectedCategoryId가 변경될 때마다 새로운 아이템 목록을 가져옴
fetchItems();
}, [selectedCategoryId]);
:
- PosPage내에서 상태 변동이 있으니 그 props를 받아오는 PosItem에서 한 번 더 useEffect 해야댐
- 새로운 계정으로 회원가입하면 category, item이 빈 배열로 생성 -> null, undefined 등의 예외처리를 해야함
:
useEffect(() => {
if (selectedCategoryId !== null && items && items.categories && items.categories.length > 0) {
// 선택된 카테고리 또는 아이템 배열의 길이가 변경될 때 editModes를 false 값으로 초기화
setEditModes(Array(items.categories.length).fill(false));
}
}, [selectedCategoryId, items?.categories?.length]);
useEffect(() => {
const fetchItems = async () => {
try {
const itemData = await ItemGetApi();
setItems(itemData); // 가져온 아이템 목록으로 상태 업데이트
} catch (err) {
console.error(err);
}
};
// selectedCategoryId가 변경될 때마다 새로운 아이템 목록을 가져옴
fetchItems();
}, [selectedCategoryId]);
:
- useEffect가 한 파일 내에 여러 개 일때는 위에서부터 아래로 진행된다 (비동기적으로)
:
useEffect(() => {
const fetchItems = async () => {
try {
const itemData = await ItemGetApi();
setItems(itemData); // 가져온 아이템 목록으로 상태 업데이트
} catch (err) {
console.error(err);
}
};
// selectedCategoryId가 변경될 때마다 새로운 아이템 목록을 가져옴
fetchItems();
}, [selectedCategoryId]);
useEffect(() => {
if (selectedCategoryId !== null && items && items.categories && items.categories.length > 0) {
// 선택된 카테고리 또는 아이템 배열의 길이가 변경될 때 editModes를 false 값으로 초기화
setEditModes(Array(items.categories.length).fill(false));
}
}, [selectedCategoryId, items?.categories?.length]);
:
Case4. CloudFront
- 수정 전
const URL = "http://api.salesync.site";
export default URL;
- 수정 후
const URL = "https://api.salesync.site";
export default URL;
- 분명
https
로 변경했으나http
로 통신이 감
- 수정 후
const URL = "//api.salesync.site";
export default URL;
- CloudFront는 CDN으로 정적 파일이 남아있어서 이전에 사용한
http
주소로 캐시된 것이 업데이트 되지 않음.
- 수정 후
const URL = "https://api.salesync.site";
export default URL;
- 수정 후
name: Deploy to Amazon S3 bucket
on:
push:
branches: ["main"]
env:
AWS_REGION: ap-northeast-2
S3_BUCKET_NAME: salesync.site
CLOUDFRONT_NAME: E3P5E4B2X3UGOU
: - name: CloudFront delete cache
uses: chetan/invalidate-cloudfront-action@v2
env:
DISTRIBUTION: ${{ env.CLOUDFRONT_NAME }}
PATHS: "/*"
AWS_REGION: ${{ env.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Case5. Recoil
-
회원가입 이후 Recoil을 활용하여 회원 여부를 확인하고, PrivateRoute 및 PublicRoute에서 useRecoilState를 사용
-
→ 렌더링된 페이지 이후에 PrivateRoute와 PublicRoute가 다시 실행되어 의도치 않은 페이지로 이동하는 문제가 발생
- 수정 후
:
// PublicRoute.js
const userCheck = useRecoilValue(UserCheckState)
return userCheck ? element : <Navigate replace to="/"/>;
// 수정
const tokenCheck = localStorage.getItem('access_token')
return tokenCheck !==null ? element : <Navigate replace to="/"/>;
:
- PrivateRoute가 2번 실행됨
- 로그아웃 기능이 안됨
- 수정 후
:
const logout=()=>{
localStorage.removeItem('access_token');
localStorage.removeItem('csrf_token');
tokenCheckfunc()
}
const tokenCheckfunc=()=>{
const tokenCheck = localStorage.getItem('access_token')
if(tokenCheck !== null){
setUserCheck(true)
} else{
setUserCheck(false)
}
}
// 수정
const logout=()=>{
localStorage.removeItem('access_token');
localStorage.removeItem('csrf_token');
setUserCheck(false)
navigate('/')
}
:
- userCheck를 true로 변경 후 useEffect 가 실행되기때문에 privateRoute가 실행되어 home으로 라우팅 됨
- 조건을 추가하여 카테고리 비어있을 경우 pospage 머뭄
- 수정 후
:
useEffect(()=>{
if(categories.length == 0){
setCategoryState(true)
} else{
setCategoryState(false)
}
},[categories])
// Header.js
// kiosk 상태
const [isKiosk , setIsKiosk] = useRecoilState(KioskState)
// 카테고리 데이터 상태 체크 [0인경우 true]
const [categoryState, setCategoryState] = useRecoilState(CategoryState)
useEffect(()=>{
const checkCategory = async () => {
try{
const category = await CategoryGetApi();
if(category.categories.length !== 0){
setCategoryState(false)
}else{
setCategoryState(true)
}
} catch(err){
console.log(err)
}
}
checkCategory()
},[])
useEffect(() => {
// isKiosk 값이 변경될 때마다 실행되는 useEffect를 이용하여 라우팅을 수행합니다.
if (isKiosk) {
navigate('/kiosk');
} else if(!isKiosk && !categoryState){
navigate('/home')
}
}, [isKiosk]);
// CategoryState.js
import { atom } from "recoil";
export const CategoryState = atom({
key: "CategoryState",
default: false
});
:
Case6. Rendering
- 수정 후
:
</LineContainer>
</DashboardDiv>
<ConsultingButton type='button' onClick={handlerConsulting}>컨설팅</ConsultingButton>
</ComponentDiv>
:
- useState로 상태관리하는 date가 여기저기 걸려있음 - Calendar, PieChart, LineChart, Consulting API(3개)
- date를 props로 다른 페이지(or 컴포넌트)에 고정된 상수로 전달 → 변수 date의 의존성을 제거하여 렌더링 문제 해결
- 수정 후
<ConsultingContainer>
<ConsultingButton type="button" onClick={openConsult}>
컨설팅
</ConsultingButton>
</ConsultingContainer>