Skip to content

Commit

Permalink
[클린코드 리액트 2기 김희열] 페이먼츠 미션 Step3 (#105)
Browse files Browse the repository at this point in the history
* chore: React+TypeScript+Storybook 프로젝트 세팅

* chore: Complete the craco setup

* chore: Complete eslint setup

* fix: Fix typescript error

* feat: Add card component

* feat: Add CardForm Component & Internal Components

* feat: Add PageTitle component

* feat: Add navigate

* feat: Change to CardForm Compound Component Pattern

* feat: Reflect card number changes to Card components.

* feat: Reflect  card expired date changes to Card components

* feat: Reflect card owner changes to Card components

* feat: Reflect card password & security code changes to Card components

* feat: Add useCardInfo custom hook

* feat: Add Expiration Date Edge Case

* feat: Add Owner Edge Case

* feat: Add Password Edge Case

* feat: Add Numbers Edge Case

* feat: Save card information to local storage and go to the next page

* fix: Fix craco config eslint error

* fix: Delete /** in index.css

* feat: Add useInputFocus

* modify: Delete all console.log

* modify: Modify printWidth 80 to 120

* modify: Apply eslint fix

* modify: Remove component type

* modify: Rename useInputFocus and improve flexibility

* modify: Modify NextButtonBox names and improve flexibility

* modify: Improved flexibility of PageTitle components

* feat: Add first storybook

* feat: Add BackButton storybook

* feat: Change from ButtonBox to NavigationTextBox and add storybook

* feat: Add PageTitle storybook

* feat: Add CardExpiredDate storybook

* modify: Fix typo

* feat: Add CardAdd & components storybook

* feat: Set up storybook deploy

* modify: Modify storybook build

* modify: Modify storybook introduction

* modify: Add storybook addon docs

* modify: Change PassWord & Security Code to Uncontrolled Components

* feat: Switch to Card Status Context API

* modify: Refactoring Context API related code

* modify: Change CardPassword and CardSecurityCode Components to Non-Control Components

* modify: Changes the way non-control components are managed in the Context API

* modify: Modify nickname cardReducer

* feat: Add CardListContext

* feat: Add card alias update function

* feat: Add Card delete function

* feat: Add useCardUpdate custom hook

* feat: Add CardUpdate storybook

* feat: Add useCardList custom hook

* feat: Add CardList storybook

* feat: Add useCardCompleted custom hook

* feat: Add CardCompleted storybook

* modify: Move to the reducer domain subfolder

* feat: Reuse card components

* modify: Create a CardDetailsForm layout and reuse it in CardUpdate, CardCompleted components

* modify: Modify useCardInfo not to receive as an event factor

* add: Add BasicInput Component

* add: Add CardForm Custom Hooks

* modify: Changed how Input Component Props are delivered

* add: Add InputTitle component

* feat: Add InputBox component

* feat: Add InputContainer component

* feat: Switch to all Input non-control components and manage from the useCardAdd custom hook

* modify: Solve problems that are re-rendered Input components when card state changes by separating CardStateContext and CardDispatchContext

* add: 유효성 검증 로직 추가

* add: 보안코드 툴팁 추가

* add: 카드사 선택 기능 추가

* add: CardTypeButton 컴포넌트 분리

* add: useCardNumbers에서 카드타입 모달 닫힌 후 세번째 인풋에 포커싱 유지하도록 props 함수 추가

* modify: 만료일 유효성 조건 추가

* add: 가상키보드 UI 추가

* add: 가상키보드 추가

* modify: README.md 업데이트

* fix: upstream과 step3 브랜치의 충돌 해결

* modify: 카드 목록, 카드 완성 화면에서 카드타입 적용되도록 수정

* fix: 스토리북 안나오던 UI 나오도록 수정

* add: 스토리북에서 Context API 사용할 수 있도록 추가

* modify: 스토리북 디렉토리 변경 및 Context Decorator 도입

* add: Input 컴포넌트 스토리북 추가

* add: PreviewCard 스토리북 추가

* add: BigCard 스토리북 추가

* add: SmallCard 스토리북 추가

* add: Toast 스토리북 추가

* add: Tooltip 스토리북 추가

* add: CardTypeButton 스토리북 추가

* add: CardTypeSelectionModal 스토리북 추가

* add: VirtualKeyboard 스토리북 추가

* add: CardCompleted 스토리북 추가

* add: CardList 스토리북 추가

* add: CardUpdate 스토리북 추가

* modify: 카드 UI 수정

* modify: 스토리북 빌드 파일 새로 생성

* modify: 스토리북 main.js의 addons 배열 수정
  • Loading branch information
herekim authored Feb 17, 2024
1 parent 6c3e046 commit 37b21d2
Show file tree
Hide file tree
Showing 169 changed files with 4,111 additions and 1,385 deletions.
1 change: 0 additions & 1 deletion .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ module.exports = {
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/preset-create-react-app',
'storybook-addon-react-router-v6',
],
framework: '@storybook/react',
core: {
Expand Down
22 changes: 22 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#2f7c47",
"activityBar.background": "#2f7c47",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#422c74",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#2f7c47",
"statusBar.background": "#215732",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#2f7c47",
"statusBarItem.remoteBackground": "#215732",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#215732",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#21573299",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#215732"
}
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,59 @@
✔️ 다른 라이브러리나 프레임워크 없이 오로지 `React`만으로 상태를 관리하고 컴포넌트를 설계합니다.
✔️ `재사용 가능한 Component`를 직접 작성하고 사용합니다.
✔️ `Controlled` & `Uncontrolled Components`에 입각하여 `Form`을 핸들링합니다.

## 고민한 흔적들 (꾸준히 업데이트 예정)

### 1. CDD에 따라 UI구성 및 컴포넌트 작성

### 2. 꼭 필요한 상태인가?

- e.g. 가상 키보드

### 3. 스토리북 상호작용 테스트

- 스토리북에 넣을 컴포넌트 기준
1. Props에 따라서 UI가 달라지는 컴포넌트
1. Card
- PreviewCard ✅
- BigCard ✅
- SmallCard ✅
2. Button
- CardTypeButton ✅
- NavigationButton ✅
3. Modal
- CardTypeSelectionModal ✅
- VitualKeyboard ✅
4. Toast ✅
5. Tooltip ✅
2. 유저와의 상호작용을 통해 UI가 달라지는 컴포넌트
1. Input ✅
2. CardForm ✅
- CardNumbers ✅
- CardExpiredDate ✅
- CardOwner ✅
- CardPassword ✅
- CardSecurityCode ✅
3. 컴포넌트의 조합으로 생성된 페이지
1. /cart-add ✅
2. /card-completed ✅
3. /card-list ✅
4. /card-update ✅
- Context API를 테스트 하려면?
- Router를 테스트 하려면?
- 인터렉션을 테스트 하려면?
- 스토리북 내에서 Jest, Cypress 등을 테스트 하려면?
- 참고
- https://ui.toast.com/posts/ko_20220111

### 4. 재사용 가능한 컴포넌트

### 5. 제어 & 비제어 컴포넌트

### 6. Context API를 활용한 전역 상태 관리와 계층 재구성

### 7. 테스트에 대한 고찰

### 8. 기타

- ref를 이용한 Context + 비제어 컴포넌트로 만든 폼의 가상 키보드 붙이기
453 changes: 371 additions & 82 deletions build-storybook.log

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@radix-ui/react-toast": "^1.1.3",
"@radix-ui/react-tooltip": "^1.0.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
Expand Down Expand Up @@ -85,6 +87,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.4",
"prop-types": "^15.8.1",
"storybook-react-context": "^0.6.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"webpack": "^5.75.0"
},
Expand Down
16 changes: 10 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom'

import { CardProvider, CardListProvider } from '@/providers'
import { Modal } from '@/components/modal'
import { CardProvider, CardListProvider, ModalProvider } from '@/providers'
import routes from '@/routes'

const router = createBrowserRouter(routes)

function App() {
return (
<div className="root">
<CardListProvider>
<CardProvider>
<RouterProvider router={router} />
</CardProvider>
</CardListProvider>
<ModalProvider>
<CardListProvider>
<CardProvider>
<RouterProvider router={router} />
<Modal />
</CardProvider>
</CardListProvider>
</ModalProvider>
</div>
)
}
Expand Down
21 changes: 0 additions & 21 deletions src/components/button/BackButton.stories.tsx

This file was deleted.

90 changes: 90 additions & 0 deletions src/components/button/CardTypeButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'

import CardTypeButton, { CardTypeButtonProps } from './CardTypeButton'

export default {
title: 'Components/Button/CardTypeButton',
component: CardTypeButton,
} as ComponentMeta<typeof CardTypeButton>

const Template: ComponentStory<typeof CardTypeButton> = ({ name, color, backgroundColor }: CardTypeButtonProps) => (
<div className="root">
<div className="app">
<CardTypeButton
name={name}
backgroundColor={backgroundColor}
color={color}
selectCardType={(name: string, backgroundColor: string, color: string) =>
console.log(name, backgroundColor, color)
}
/>
</div>
</div>
)

const CARD_TYPES = {
하얀카드: {
name: '하얀카드',
color: '#000000',
backgroundColor: '#F5F5F5',
},
파란카드: {
name: '파란카드',
color: '#ffffff',
backgroundColor: '#162bb1',
},
빨간카드: {
name: '빨간카드',
color: '#ffffff',
backgroundColor: '#932929',
},
초록카드: {
name: '초록카드',
color: '#000000',
backgroundColor: '#54cb25',
},
에메랄드카드: {
name: '에메랄드카드',
color: '#ffffff',
backgroundColor: '#20d0ad',
},
분홍카드: {
name: '분홍카드',
color: '#ffffff',
backgroundColor: '#d320c7',
},
보라카드: {
name: '보라카드',
color: '#ffffff',
backgroundColor: '#7c1ddb',
},
주황카드: {
name: '주황카드',
color: '#ffffff',
backgroundColor: '#e1860f',
},
}

export const 하얀카드 = Template.bind({})
하얀카드.args = CARD_TYPES['하얀카드']

export const 파란카드 = Template.bind({})
파란카드.args = CARD_TYPES['파란카드']

export const 빨간카드 = Template.bind({})
빨간카드.args = CARD_TYPES['빨간카드']

export const 초록카드 = Template.bind({})
초록카드.args = CARD_TYPES['초록카드']

export const 에메랄드카드 = Template.bind({})
에메랄드카드.args = CARD_TYPES['에메랄드카드']

export const 분홍카드 = Template.bind({})
분홍카드.args = CARD_TYPES['분홍카드']

export const 보라카드 = Template.bind({})
보라카드.args = CARD_TYPES['보라카드']

export const 주황카드 = Template.bind({})
주황카드.args = CARD_TYPES['주황카드']
23 changes: 23 additions & 0 deletions src/components/button/CardTypeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CardBackgoundColor, CardColor } from '@/domain'

export interface CardTypeButtonProps {
name: string
backgroundColor: CardBackgoundColor
color: CardColor
selectCardType: (name: string, bg: string, color: string) => void
}

const CardTypeButton = ({ name, backgroundColor, color, selectCardType }: CardTypeButtonProps) => {
return (
<button
key={name}
className="flex-column-center gap-2 font-sm"
onClick={() => selectCardType(name, backgroundColor, color)}
>
<div style={{ backgroundColor }} className="card-color" />
<p>{name}</p>
</button>
)
}

export default CardTypeButton
24 changes: 24 additions & 0 deletions src/components/button/NavigationButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'

import NavigationButton from './NavigationButton'

export default {
title: 'Components/Button/NavigationButton',
component: NavigationButton,
// Todo: 다음 버튼을 눌렀을 때도 상호작용 테스트를 할 수 있는건가?
args: {
additionalClassNames: 'mt-50',
text: '다음',
to: '/',
},
} as ComponentMeta<typeof NavigationButton>

const Template: ComponentStory<typeof NavigationButton> = (props) => (
<div className="root">
<div className="app">
<NavigationButton {...props} />
</div>
</div>
)

export const Default = Template.bind({})
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@ import { useNavigate } from 'react-router-dom'

type Destination = '/card-add' | '/card-completed' | '/'

interface NavigationTextButtonProps {
interface NavigationButtonProps {
additionalClassNames?: string
text: string
preNavigation?: () => void
onBeforeNavigate?: () => void
to: Destination
isNavigationEnabled?: () => boolean
}

const NavigationTextButton = ({
const NavigationButton = ({
additionalClassNames = '',
text,

preNavigation,
onBeforeNavigate,
to,
}: NavigationTextButtonProps) => {
isNavigationEnabled = () => true,
}: NavigationButtonProps) => {
const navigate = useNavigate()

const goToSpecifiedPage = async (to: Destination) => {
if (preNavigation) {
await Promise.resolve(preNavigation())
if (onBeforeNavigate) {
await Promise.resolve(onBeforeNavigate())
}
if (isNavigationEnabled()) {
navigate(to)
}
navigate(to)
}

return (
Expand All @@ -34,4 +37,4 @@ const NavigationTextButton = ({
)
}

export default NavigationTextButton
export default NavigationButton
24 changes: 0 additions & 24 deletions src/components/button/NavigationTextButton.stories.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion src/components/button/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as BackButton } from './BackButton'
export { default as NavigationTextButton } from './NavigationTextButton'
export { default as NavigationButton } from './NavigationButton'
export { default as CardTypeButton } from './CardTypeButton'
Loading

0 comments on commit 37b21d2

Please sign in to comment.