Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
## 📝 Requirements

### 필수 요구사항
- [ ] 원시적인 형태의 `Primitive UI` 형태의 컴포넌트 작성
- [ ] `Stepper` 기반의 애플리케이션 설계
- [x] 원시적인 형태의 `Primitive UI` 형태의 컴포넌트 작성
- [x] `Stepper` 기반의 애플리케이션 설계
- [ ] `Storybook` 상호 작용 테스트
- [ ] `Controlled` & `Uncontrolled Components`를 명확하게 구분하거나 선택하여 구현

### 카드 추가
- [ ] <(뒤로가기) 버튼 클릭 시, 카드 목록 페이지로 이동한다.
- [x] <(뒤로가기) 버튼 클릭 시, 카드 목록 페이지로 이동한다.
- [x] 카드 번호를 입력 받을 수 있다.
- [x] 카드 번호는 숫자만 입력가능하다.
- [x] 카드 번호 4자리마다 -가 삽입된다.
Expand All @@ -43,4 +43,4 @@
- [x] 카드 소유자 이름을 입력 받을 수 있다.
- [x] 이름은 30자리까지 입력할 수 있다.
- [ ] 이름 입력 폼 위에, 현재 입력 자릿수와 최대 입력 자릿수를 실시간으로 보여준다.
- [ ] 카드 추가 완료시 카드 등록 완료 페이지로 이동한다.
- [x] 카드 추가 완료시 카드 등록 완료 페이지로 이동한다.
11 changes: 9 additions & 2 deletions src/components/AddCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React, {useContext} from "react"
import {AddCardProps} from "../interface/AddCardProps.ts";
import CardContext from "../context/CardContext.tsx";
import {CardType} from "../type/CardType.ts";
import {useStepper} from "../context/StepperContext.tsx";

const AddCard: React.FC<AddCardProps> = ({cardName, cardNumber, name, expireDate}) => {
const {addCard} = useContext(CardContext)
const {setCurrentStep} = useStepper()

const handleClick = () => {
const newCard = {cardName, cardNumber, name, expireDate};
addCard(newCard);
const {month, year} = expireDate
const cardExpireDate = `${month} / ${year}`
const newCard: CardType = {cardName, cardNumber, name, cardExpireDate}
addCard(newCard)
setCurrentStep("AddCardComplete")
}

return (
Expand Down
24 changes: 24 additions & 0 deletions src/components/AddCardComplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, {useContext} from "react"
import CardContext from "../context/CardContext.tsx";
import {CardType} from "../type/CardType.ts";
import {useStepper} from "../context/StepperContext.tsx";
import {AddCardCompleteProps} from "../interface/AddCardCompleteProps.ts";

const AddCardComplete: React.FC<AddCardCompleteProps> = ({cardName, cardNumber, name, cardExpireDate, cardAlias}) => {
const {modifyCard} = useContext(CardContext)
const {setCurrentStep} = useStepper()

const handleClick = () => {
const card: CardType = {cardName, cardNumber, name, cardExpireDate, cardAlias}
modifyCard(card)
setCurrentStep("CardList")
}

return (
<div className="button-box">
<span className="button-text" onClick={handleClick}>다음</span>
</div>
)
}

export default AddCardComplete
104 changes: 8 additions & 96 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,108 +4,20 @@ import '../styles/utils.css'
import '../styles/button.css'
import '../styles/card.css'
import '../styles/modal.css'
import NumberInput from "./NumberInput.tsx"
import Input from "./Input.tsx"
import Card from "./Card.tsx"
import {Dispatch, SetStateAction, useState} from "react"
import Modal from "./Modal.tsx"
import AddCardForm from "../form/AddCardForm.tsx";
import {useStepper} from "../context/StepperContext.tsx";
import AddCardCompleteForm from "../form/AddCardCompleteForm.tsx";
import {CardProvider} from "../context/CardContext.tsx";
import AddCard from "./AddCard.tsx";
import CardListForm from "../form/CardList.tsx";

function App() {
const [cardName, setCardName] = useState('')
const [name, setName] = useState('')
const [expireDate, setExpireDate] = useState({month: '', year: ''})
const [cardNumber, setCardNumber] = useState({first: '', second: '', third: '', fourth: ''})
const [isModalOpen, setIsModalOpen] = useState(false)

const handleCardClick = () => {
setIsModalOpen(true)
}

const handleCardNameChange = (value: string) => {
setCardName(value)
setIsModalOpen(false)
}
const handleInputChange = (value: string, field: string, setState: Dispatch<SetStateAction<any>>) => {
setState(prevState => ({
...prevState,
[field]: value
}))
}

const handleExpiredDateChange = (value: string, field: string) => {
handleInputChange(value, field, setExpireDate)
}

const handleCardNumberChange = (value: string, field: string) => {
handleInputChange(value, field, setCardNumber)
}
const {step} = useStepper()

return (
<CardProvider>
<>
<div className="app">
<h2 className={"page-title"}>&lt; 카드 추가</h2>
<Card cardName={cardName} cardNumber={cardNumber} name={name} cardExpireDate={expireDate}
onClick={handleCardClick}></Card>
<div className="input-container">
<span className="input-title">카드 번호</span>
<div className="input-box">
<NumberInput className={"input-basic"} type={"text"} maxLength={4}
inputChange={(value) => handleCardNumberChange(value, 'first')}></NumberInput>
<span>-</span>
<NumberInput className={"input-basic"} type={"text"} maxLength={4}
inputChange={(value) => handleCardNumberChange(value, 'second')}></NumberInput>
<span>-</span>
<NumberInput className={"input-basic"} type={"password"} maxLength={4}
inputChange={(value) => handleCardNumberChange(value, 'third')}></NumberInput>
<span>-</span>
<NumberInput className={"input-basic"} type={"password"} maxLength={4}
inputChange={(value) => handleCardNumberChange(value, 'fourth')}></NumberInput>
</div>
</div>
<div className="input-container">
<span className="input-title">만료일</span>
<div className="input-box w-50">
<NumberInput className={"input-basic"}
type={"text"}
placeHolder={"MM"}
inputRule={(value) => /^(0?\d|1[0-2])?$/.test(value) && value !== "00"}
inputChange={(value) => handleExpiredDateChange(value, 'month')}></NumberInput>
<span>/</span>
<NumberInput className={"input-basic"}
type={"text"}
placeHolder={"YY"}
maxLength={2}
inputChange={(value) => handleExpiredDateChange(value, 'year')}></NumberInput>
</div>
</div>
<div className="input-container">
<span className="input-title">카드 소유자 이름(선택)</span>
<Input className={"input-basic"}
type={"text"}
placeHolder={"카드에 표시된 이름과 동일하게 입력하세요."}
maxLength={30}
inputChange={setName}></Input>
</div>
<div className="input-container">
<span className="input-title">보안 코드(CVC/CVV)</span>
<NumberInput className={"input-basic w-25"} type={"password"} maxLength={3}></NumberInput>
</div>
<div className="input-container">
<span className="input-title">카드 비밀번호</span>
<NumberInput className={"input-basic w-15"} type={"password"} maxLength={1}></NumberInput>
<NumberInput className={"input-basic w-15"} type={"password"} maxLength={1}></NumberInput>
<NumberInput className={"input-basic w-15"} type={"password"} maxLength={1} disabled={true}
defaultState={"*"}></NumberInput>
<NumberInput className={"input-basic w-15"} type={"password"} maxLength={1} disabled={true}
defaultState={"*"}></NumberInput>
</div>
<AddCard cardName={cardName} name={name} cardNumber={cardNumber} expireDate={expireDate}></AddCard>
</div>
<Modal isOpen={isModalOpen} selectCard={(value) => handleCardNameChange(value)}></Modal>
</>
{step === '/' && <AddCardForm/>}
{step === 'AddCardComplete' && <AddCardCompleteForm/>}
{step === 'CardList' && <CardListForm/>}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: 카드 입력 폼은 경로처럼 '/'로, 다른 단계는 직접 명시하셨네요. 랜딩 페이지의 느낌으로 '/'를 사용하신 거라 생각되는데, 다른 step과 마찬가지로 적어주는 것이 명시적이고, 또 이후 카드 등록 플로우 요구사항 변경 시 유연하게 대응할 수 있을 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경 하였습니다.

</CardProvider>
)
}
Expand Down
3 changes: 1 addition & 2 deletions src/components/Display.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react'
interface DisplayProps {
className: string
className?: string
value?: string
defaultValue?: string
}

const Display: React.FC<DisplayProps> = ({className, value, defaultValue}) => {

return (
<span className={className}>{value !== undefined && value !== "" ? value : defaultValue}</span>
)
Expand Down
30 changes: 30 additions & 0 deletions src/components/DivButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import {useStepper} from "../context/StepperContext.tsx";

export interface DivInputProps {
className: string
step?: string
value?: string
}

const DivButton: React.FC<DivInputProps> = ({className, step, value}) => {
const {setCurrentStep} = useStepper()
const handleClick = () => {
if (step) {
setCurrentStep(step)
}
}

return (
<>
<div
className={className}
onClick={handleClick}
>
{value}
</div>
</>
)
}

export default DivButton
36 changes: 36 additions & 0 deletions src/components/HeaderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import {useStepper} from "../context/StepperContext.tsx";
import IntrinsicElements = React.JSX.IntrinsicElements;

export interface HeaderButtonProps {
tag: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a: polymorphic하게 컴포넌트 구현해주셨네요 💯 정수님이 구현하신 것처럼 아토믹 컴포넌트에서 tag를 polymorphic하게 전달받으면 스타일은 재사용하면서 다른 html 요소의 기능을 사용할 수 있어 확장성이 개선됩니다.

className: string
step?: string
value?: string
}


const HeaderButton: React.FC<HeaderButtonProps> = ({tag, className, step, value}) => {
const {setCurrentStep} = useStepper()
const handleClick = () => {
if (step) {
setCurrentStep(step)
}
}

const Tag = tag as keyof IntrinsicElements;

return (
<>
<Tag
className={className}
onClick={handleClick}
>
{value}
</Tag>
</>
)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: step을 prop으로 전달받고, 버튼 컴포넌트 내부에 해당 step으로 stepper 상태를 변경하는 로직을 선언하셨네요. 이렇게 구현하면 요구사항에 따라 유연하게 대처하지 못할 가능성이 많습니다. 컴포넌트를 유연하게 만들고 재사용하기 쉽게 만들기 위해서는 내부가 아닌 외부에서 클릭 이벤트에 대한 로직을 주입받도록 구현해, 외부에서 컴포넌트의 동작을 컨트롤하게 해주세요. 하나의 컴포넌트는 최대한 하나의 역할만 하도록 하는 것이 좋습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다.



export default HeaderButton;
Empty file added src/components/TextButton.tsx

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r: 개발 중 만들어진 빈 파일은 삭제해주세요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제 했습니다.

Empty file.
13 changes: 11 additions & 2 deletions src/context/CardContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {CardType} from "../type/CardType.ts";
interface CardContextType {
cards: CardType[]
addCard: (newCard: CardType) => void
modifyCard: (newCard: CardType) => void
}

const CardContext = createContext<CardContextType>({
cards: [],
addCard: () => {}
addCard: () => {},
modifyCard: () => {}
});

export const CardProvider: React.FC<{ children: React.ReactNode}> = ({ children }) => {
Expand All @@ -18,8 +20,15 @@ export const CardProvider: React.FC<{ children: React.ReactNode}> = ({ children
setCards(prevCards => [...prevCards, newCard]);
}

const modifyCard = (newCard: CardType) => {
setCards(prevCards => {
const cards = prevCards.slice(0, prevCards.length -1);
return [...cards, newCard];
})
}
Comment on lines +27 to +32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 지금은 잘 동작하지만 추후 선택한 카드의 정보를 변경하는 경우 로직이 많이 변경되어 다른 컴포넌트에 수정이 전파될 것 같네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 부분은 다 수정했는데, 이 부분은 힌트를 좀 더 부탁드립니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막 요소만 수정가능한 것 같아 특정 카드를 선택해 정보를 수정하는 경우 id를 통한 비교가 이루어져야 할 것 같다는 의미였습니다. 지금은 잘 동작하니 넘어가셔도 좋습니다.


return (
<CardContext.Provider value={{ cards, addCard }}>
<CardContext.Provider value={{ cards, addCard, modifyCard }}>
{children}
</CardContext.Provider>
)
Expand Down
27 changes: 27 additions & 0 deletions src/context/StepperContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, {createContext, useContext, useState} from 'react'

interface StepperContextType {
step: string
setCurrentStep: (step: string) => void
}

const StepperContext = createContext<StepperContextType>({
step: "/",
setCurrentStep: () => {}
});

export const StepperProvider: React.FC<{ children: React.ReactNode}> = ({ children }) => {
const [step, setStep] = useState<string>("/")

const setCurrentStep = (step: string) => {
setStep(step)
}

return (
<StepperContext.Provider value={{ step, setCurrentStep }}>
{children}
</StepperContext.Provider>
)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a: Context API 사용해 stepper 잘 구현해주셨네요! 💯


export const useStepper = () => useContext(StepperContext);
Loading