Skip to content

Commit 6a3214b

Browse files
authored
[2단계 - 페이먼츠 모듈] 리바이(성은우) 미션 제출합니다. (#121)
* docs: 2단계 README 요구 명세 작성 * feat: 공유 ModalProps Type 정의 * styles: 취소 및 확인 버튼 컴포넌트 구현 * chore: ModalProps 타입 위치 변경 * chore: modal type 파일명 변경 * feat: AlertModal 컴포넌트 구현 * feat: ConfirmModal 컴포넌트 구현 * feat: PromptModal 컴포넌트 구현 * feat: size와 position에 따른 css 구현 * feat: 매개변수에 따른 Modal 로직 구현 * feat: Modal의 여러 공용 타입 정의 * docs: Custom Component 기능요구 명세 업데이트 * feat: CardBrandType 정의 * feat: matchCardBrand 유틸 함수 구현 - cardNumber 에 따른 CardBrand 매칭 * refactor: matchCardBrand 유틸함수 로직 수정 * refactor: 다양한 브랜드 추가로 로직 리팩토링 * docs: Custom Hooks 기능 요구 명세 업데이트 * test: AlertModal Storybook 구현 * test: ConfirmModal Storybook 구현 * test: PromptModal Storybook 구현 * test: CustomModal Storybook 구현 * chore: 불필요한 console.log 제거 * refactor: validateCardNuber 중복 에러 허용한 로직으로 리팩토링 * test: hook 과 유효성 검사 함수 성공 및 실패 테스트코드 구현 * style: focus일때 box shadow 적용 * feat: 모달 컴포넌트 웹 접근성(A11y) 개선을 위한 useFocusTrap 훅 구현 * refactor useRef 및 useEffect 활용하여 초기 focus 적용 * refactor: useFocusTrap 훅 Modal에 적용 * docs: 기능 요구 명세서 업데이트 * style: CloseButton 및 Modal css 개선 * refactor: useFocusTrap훅에서 변수명 더 명료하게 수정 * refactor: 정규식 상수화 * chore: Storybook CI 추가 * chore: npm version 업데이트 * fix: chromatic ci 설정 변경 * chore: 예시 임시 코드 추가 * chore: 불필요한 console 제거 * chore: 파일 네이밍 오타 수정 * feat: index에 Modal.styles 와 modal types를 export에 추가 * refactor: modal 로직을 조건식 대신 switch문을 통한 early return 방식으로 리팩토링 * test: modal storybook css 수정 * refactor: Modal에서 닫기 버튼 경로를 절대경로로 변경 * test: 각 Modal storybook에서 type를 args에서 분리 후 VariousModal storybook 추가로 구현 * refactor: ref는 불변객체이므로 useEffect 의존성 배열에서 제거 * refactor: matchCardBrand 함수에서 매직넘버 상수화 적용 * refactor: 색상 및 크기 상수화 및 공통 로직 분리하여 코드 개선 * refactor: id 및 aria-describedby 추가하여 스크린 리더 사용자를 위한 접근성 향상 * test: VariousModal Storybook에서 component 수정 * docs: NPM 영문 README 작성 * docs: NPM hooks README 업데이트 * docs: NPM custom components README 업데이트 * docs: payment custom hook README Title 수정
1 parent b6bee98 commit 6a3214b

31 files changed

+1467
-154
lines changed

.github/workflows/chromatic.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Workflow name
2+
name: 'React Payments Module Deployment'
3+
4+
# Event for the workflow
5+
on: push
6+
7+
# List of jobs
8+
jobs:
9+
chromatic:
10+
name: 'Run Chromatic'
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
17+
- run: yarn
18+
working-directory: components
19+
20+
- run: npx chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }}
21+
working-directory: components

components/README.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,85 @@
1-
# Button Module
1+
# React Modal Component Library
2+
3+
A customizable, accessible modal component library for React applications, supporting common modal types including `alert`, `confirm`, `prompt`, and `custom` modals. Built with accessibility in mind, keyboard focus is automatically trapped within open modals, and the Escape key closes the modal.
4+
5+
## ✨ Features
6+
7+
- ✅ Prebuilt modal types: `Alert`, `Confirm`, `Prompt`, `Custom`
8+
- ✅ Keyboard accessibility with `Escape` to close and focus trap
9+
- ✅ Customizable size and position (`small`, `medium`, `large`, etc.)
10+
- ✅ Easily styled with Emotion
11+
- ✅ Fully typed with TypeScript
12+
13+
## 🚀 Usage
14+
15+
### Basic Example
16+
17+
```tsx
18+
import Modal from '@your-org/payments-modal-component';
19+
20+
<Modal
21+
isOpen={isModalOpen}
22+
onClose={() => setIsModalOpen(false)}
23+
type='confirm'
24+
title='Delete Item'
25+
message='Are you sure you want to delete this item?'
26+
onConfirm={() => handleDelete()}
27+
/>;
28+
```
29+
30+
## Modal Types
31+
32+
### 1. Alert Modal
33+
34+
```tsx
35+
<Modal isOpen={true} onClose={handleClose} type='alert' message='This is an alert message!' />
36+
```
37+
38+
### 2. Confirm Modal
39+
40+
```tsx
41+
<Modal isOpen={true} onClose={handleClose} onConfirm={handleConfirm} type='confirm' message='Are you sure?' />
42+
```
43+
44+
### 3. Prompt Modal
45+
46+
```tsx
47+
<Modal isOpen={true} onClose={handleClose} onSubmit={(input) => console.log(input)} type='prompt' />
48+
```
49+
50+
### 4. Custom Modal
51+
52+
```tsx
53+
<Modal isOpen={true} onClose={handleClose} type='custom'>
54+
<div>Custom modal content goes here</div>
55+
</Modal>
56+
```
57+
58+
## Props
59+
60+
| Prop | Type | Description | Required | Default |
61+
| ----------- | ---------------------------------------------- | --------------------------------------- | ----------- | ---------- |
62+
| `isOpen` | `boolean` | Whether the modal is open | No | `false` |
63+
| `onClose` | `() => void` | Called when modal is closed | Yes ||
64+
| `type` | `'alert' \| 'confirm' \| 'prompt' \| 'custom'` | Type of modal to render | No | `'custom'` |
65+
| `title` | `string` | Title displayed at the top of the modal | No | `''` |
66+
| `message` | `string` | Message for `alert` or `confirm` modals | No | `''` |
67+
| `onConfirm` | `() => void` | Required if `type="confirm"` | Conditional ||
68+
| `onSubmit` | `(value: string) => void` | Required if `type="prompt"` | Conditional ||
69+
| `children` | `React.ReactNode` | Custom content, used in `type="custom"` | No | `null` |
70+
| `size` | `'small' \| 'medium' \| 'large'` | Modal size | No | `'medium'` |
71+
| `position` | `'top' \| 'center' \| 'bottom'` | Modal position on screen | No | `'center'` |
72+
73+
## Accessibility
74+
75+
- Focus trap inside modal using Tab / Shift+Tab
76+
- Closes with Escape key
77+
- Buttons and inputs are keyboard-focusable
78+
79+
## 📦 Installation
80+
81+
```bash
82+
npm install your-modal-library-name
83+
# or
84+
yarn add your-modal-library-name
85+
```

components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "payments-modal-component",
3-
"version": "0.0.4",
4-
"description": "우아한테크코스 payments modal component",
3+
"version": "0.0.9",
4+
"description": "payments modal component",
55
"type": "module",
66
"types": "dist/index.d.ts",
77
"main": "dist/index.js",

components/src/App.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1-
import styled from "@emotion/styled";
2-
import "./App.css";
3-
import { useState } from "react";
4-
import React from "react";
5-
// import Modal from "./lib/Modal";
6-
import { Modal } from "woowa-modal-payments";
1+
import styled from '@emotion/styled';
2+
import './App.css';
3+
import { useState } from 'react';
4+
import React from 'react';
5+
import Modal from './lib/modal/Modal';
76

87
function App() {
98
const [isOpen, setIsOpen] = useState(false);
10-
const handleCloseModal = () => {
9+
const onClose = () => {
1110
setIsOpen(false);
1211
};
1312
return (
1413
<div>
1514
<h1>My App</h1>
1615
<Modal
1716
isOpen={isOpen}
18-
handleCloseModal={handleCloseModal}
19-
title="타이틀입니다"
20-
position="center"
21-
>
22-
<h2>Modal Title</h2>
17+
onClose={onClose}
18+
title='타이틀입니다'
19+
message='메세지입니다'
20+
type='prompt'
21+
onConfirm={() => {}}
22+
size='large'
23+
// onSubmit={(input) => {
24+
// }}
25+
/>
26+
{/* <h2>Modal Title</h2>
2327
<p>This is a modal content.</p>
24-
</Modal>
28+
</Modal> */}
2529
<OpenModal onClick={() => setIsOpen(!isOpen)}>모달 켜지는 버튼</OpenModal>
2630
</div>
2731
);

components/src/Modal.stories.tsx

Lines changed: 0 additions & 40 deletions
This file was deleted.

components/src/lib/Modal.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.

components/src/lib/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export { default as Modal } from "./Modal";
1+
export { default as Modal } from './modal/Modal';
2+
export * from './modal/Modal.styles';
3+
export * from './shared/types/modal';
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { useState } from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import styled from '@emotion/styled';
4+
import { Modal } from '..';
5+
6+
const meta: Meta<typeof Modal> = {
7+
title: 'Components/CustomModal',
8+
component: Modal,
9+
argTypes: {
10+
title: { control: { type: 'text' } },
11+
message: { control: { type: 'text' } },
12+
position: {
13+
control: { type: 'radio' },
14+
options: ['center', 'bottom'],
15+
},
16+
size: {
17+
control: { type: 'radio' },
18+
options: ['small', 'medium', 'large'],
19+
},
20+
},
21+
};
22+
export default meta;
23+
24+
type Story = StoryObj<typeof Modal>;
25+
26+
export const Default: Story = {
27+
render: (args) => {
28+
const [isOpen, setIsOpen] = useState(false);
29+
const [checked1, setChecked1] = useState(false);
30+
const [checked2, setChecked2] = useState(false);
31+
const onClose = () => setIsOpen(false);
32+
33+
const handleClick = () => {
34+
alert('성공하였습니다.');
35+
};
36+
37+
return (
38+
<div>
39+
<OpenModal onClick={() => setIsOpen(true)}>Custom 모달 열기</OpenModal>
40+
41+
<Modal isOpen={isOpen} onClose={onClose} {...args} type='custom'>
42+
<div
43+
style={{
44+
width: '100%',
45+
display: 'flex',
46+
flexDirection: 'column',
47+
justifyContent: 'center',
48+
alignItems: 'center',
49+
}}
50+
>
51+
<div>
52+
<label>
53+
<input type='checkbox' checked={checked1} onChange={() => setChecked1(!checked1)} />
54+
[필수] 개인정보 수집이용 동의
55+
</label>
56+
<br />
57+
<label>
58+
<input type='checkbox' checked={checked2} onChange={() => setChecked2(!checked2)} />
59+
[필수] 고객정보 제 3자 제공동의
60+
</label>
61+
</div>
62+
<br />
63+
<SaveButton onClick={handleClick} disabled={!(checked1 && checked2)}>
64+
동의하고 저장하기
65+
</SaveButton>
66+
</div>
67+
</Modal>
68+
</div>
69+
);
70+
},
71+
args: {
72+
title: '약관에 동의해 주세요.',
73+
message: '',
74+
position: 'center',
75+
size: 'large',
76+
onConfirm: () => alert('확인 버튼 클릭'),
77+
onSubmit: (input: string) => alert(`입력한 아이디: ${input}`),
78+
},
79+
};
80+
81+
const OpenModal = styled.button`
82+
width: 120px;
83+
height: 50px;
84+
background-color: #007bff;
85+
color: white;
86+
border: none;
87+
border-radius: 4px;
88+
cursor: pointer;
89+
90+
transition: background-color 0.1s ease;
91+
:hover {
92+
background-color: #0056b3;
93+
}
94+
`;
95+
96+
const SaveButton = styled.button`
97+
width: 100%;
98+
margin-top: 10px;
99+
padding: 10px 20px;
100+
background-color: #333333;
101+
color: white;
102+
border: none;
103+
border-radius: 4px;
104+
cursor: pointer;
105+
106+
transition: background-color 0.1s ease;
107+
108+
&:disabled {
109+
background-color: #ccc;
110+
cursor: not-allowed;
111+
}
112+
113+
:hover {
114+
background-color: #555;
115+
}
116+
`;

0 commit comments

Comments
 (0)