diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 581ba2d85..a384f619a 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/package.json b/package.json index 31487c311..02fb4b314 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "react-dom": "^18.2.0", "react-merge-refs": "^2.1.1", "react-router-dom": "^6.22.2", - "react-scripts": "^5.0.1" + "react-scripts": "^5.0.1", + "uuid": "^9.0.1" }, "devDependencies": { "@storybook/addon-essentials": "^7.6.17", diff --git a/src/App.tsx b/src/App.tsx index a51773910..794592b74 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { Link, Route, Routes } from "react-router-dom"; import MyCards from "./domains/MyCards/MyCards"; import RegisterPage from "./domains/RegisterPage/RegisterPage"; +import ModifyPage from "./domains/MyCards/ModifyPage/ModifyPage"; function App() { return ( @@ -15,6 +16,9 @@ function App() { } /> + + } /> + diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..b86b8eb81 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,35 @@ +import { v4 } from "uuid"; +import { Card } from "./domains/RegisterPage/CardRegister/types"; + +export const initialCards: Card[] = [ + { + uuid: v4(), + cardType: "윤호", + cardNumber: { + firstNumber: "1234", + secondNumber: "4321", + thirdNumber: "2121", + fourthNumber: "1111", + }, + cardHolder: "SUN", + cvc: "111", + holderName: "SUM", + expiration: { month: "12", year: "12" }, + createdAt: new Date(), + }, + { + uuid: v4(), + cardType: "은규", + cardNumber: { + firstNumber: "2222", + secondNumber: "2222", + thirdNumber: "2222", + fourthNumber: "2222", + }, + cardHolder: "SUN", + cvc: "111", + holderName: "KYU", + expiration: { month: "11", year: "11" }, + createdAt: new Date(), + }, +]; diff --git a/src/domains/MyCards/ModifyPage/ModifyPage.tsx b/src/domains/MyCards/ModifyPage/ModifyPage.tsx new file mode 100644 index 000000000..94ed5288e --- /dev/null +++ b/src/domains/MyCards/ModifyPage/ModifyPage.tsx @@ -0,0 +1,40 @@ +import { useNavigate, useSearchParams } from "react-router-dom"; +import { initialCards } from "../../../constants"; +import useLocalStorage from "../../../hooks/useLocalStorage"; +import CardNaming, { + NameQuery, +} from "../../RegisterPage/CardNaming/CardNaming"; +import { Card } from "../../RegisterPage/CardRegister/types"; +import { omitObj } from "../../../utils"; + +export default function ModifyPage() { + const [cards, setCards] = useLocalStorage("mycards", initialCards); + const [param] = useSearchParams(); + const navigate = useNavigate(); + const uuid = param.get("uuid"); + + if (!uuid) throw new Error("index search 파라미터가 필수입니다."); + + function isThis(value: Card) { + return value.uuid === uuid; + } + + const targetCard = cards.find(isThis); + + if (!targetCard) return
존재하지 않은 카드입니다.
; + + function changeName(name: NameQuery) { + const copied = [...cards]; + const targetCard = copied.find(isThis); + if (targetCard) targetCard["holderName"] = name.cardName; + setCards(copied); + navigate("/mycards"); + } + + return ( + >(targetCard, ["createdAt"])} + onSubmit={changeName} + /> + ); +} diff --git a/src/domains/MyCards/MyCards.module.css b/src/domains/MyCards/MyCards.module.css index 613806bed..f94a37027 100644 --- a/src/domains/MyCards/MyCards.module.css +++ b/src/domains/MyCards/MyCards.module.css @@ -28,6 +28,22 @@ font-size: 20px; } +.card__box { + position: relative; +} + +.card__delBtn { + position: absolute; + z-index: 2; + right: -15px; + top: -5px; + padding: 3px 5px; + + border: 1px solid #04c09e; + background-color: white; + border-radius: 20px; +} + a { color: #000; text-decoration: none; diff --git a/src/domains/MyCards/MyCards.tsx b/src/domains/MyCards/MyCards.tsx index 2bbe04e2c..3985bea7d 100644 --- a/src/domains/MyCards/MyCards.tsx +++ b/src/domains/MyCards/MyCards.tsx @@ -1,36 +1,53 @@ import { Link } from "react-router-dom"; import PlasticCard from "../component/PlaticCard/PlaticCard"; import styles from "./MyCards.module.css"; +import Button from "../../components/Button/Button"; +import useCards from "../../hooks/useCards"; export default function MyCards() { + const { cards, setCards } = useCards({ + sortByKey: "createdAt", + sortMethod: "asc", + }); + + const latest = cards.sort((a, b) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + + function deleteCard(uuid: string) { + return function deleteCard() { + const filtered = latest.filter((value) => uuid !== value.uuid); + setCards(filtered); + }; + } + return (
- -
+
+ {latest.map((card) => { + return ( +
+
+ +
+ + + +
{card.holderName}
+
+ ); + })}
); diff --git a/src/domains/RegisterPage/CardNaming/CardNaming.tsx b/src/domains/RegisterPage/CardNaming/CardNaming.tsx index e620b6a2a..861da96dc 100644 --- a/src/domains/RegisterPage/CardNaming/CardNaming.tsx +++ b/src/domains/RegisterPage/CardNaming/CardNaming.tsx @@ -9,18 +9,22 @@ import PlasticCard from "../../component/PlaticCard/PlaticCard"; import Input from "../../../components/Input/Input"; import { ChangeEvent, useState } from "react"; -type CardQuery = { +export type CardQuery = { cardNumber: CardNumber; expiration: ExpirationDate; - holder: string; + cardHolder: string; cvc: string; - password: TwoPasswordDigits; cardType: CardType; + password?: TwoPasswordDigits; +}; + +export type NameQuery = { + cardName: string; }; interface CardNamingProps { - card?: CardQuery; - onSubmit: (value: { cardName: string }) => void; + card?: Omit; + onSubmit: (value: NameQuery) => void; } export default function CardNaming({ @@ -29,7 +33,7 @@ export default function CardNaming({ }: Readonly) { const [cardName, setCardName] = useState(""); function completeRegist() { - onSubmit({ cardName }); + onSubmit({ cardName: cardName ?? card?.cardType }); } function changeCardName(event: ChangeEvent) { @@ -46,12 +50,14 @@ export default function CardNaming({
; } export default function CardResult({ store }: CardResultProps) { + const [cards, setCards] = useLocalStorage("mycards", initialCards); const navigate = useNavigate(); useEffect(() => { - console.log(store); + const registration = store["register"] as CardRegistration; + const naming = store["naming"] as NameQuery; + + const newCard: Card = { + uuid: v4(), + ...registration, + holderName: naming.cardName || registration.cardType, + createdAt: new Date(), + expiration: registration.expirationDate, + }; + + console.log("newCard", newCard); + + cards ? setCards([...cards, newCard]) : setCards([newCard]); navigate("/mycards"); - }, []); + }, [cards]); return <>; } diff --git a/src/domains/RegisterPage/RegisterPage.tsx b/src/domains/RegisterPage/RegisterPage.tsx index 9ce7ee270..06d6014ee 100644 --- a/src/domains/RegisterPage/RegisterPage.tsx +++ b/src/domains/RegisterPage/RegisterPage.tsx @@ -1,6 +1,6 @@ import useFunnel from "../../hooks/useFunnel/useFunnel"; import CardRegister from "./CardRegister/CardRegister"; -import CardNaming from "./CardNaming/CardNaming"; +import CardNaming, { CardQuery } from "./CardNaming/CardNaming"; import CardResult from "./CardResult/CardResult"; export default function RegisterPage() { @@ -21,6 +21,7 @@ export default function RegisterPage() { { setStep("result", value); }} diff --git a/src/domains/component/PlaticCard/PlasticCard.module.css b/src/domains/component/PlaticCard/PlasticCard.module.css index 0046541d0..2d2c3d04c 100644 --- a/src/domains/component/PlaticCard/PlasticCard.module.css +++ b/src/domains/component/PlaticCard/PlasticCard.module.css @@ -1,4 +1,5 @@ .card__container { + position: relative; width: 185px; height: 115px; border-radius: 4px; diff --git a/src/hooks/useCards.ts b/src/hooks/useCards.ts new file mode 100644 index 000000000..7de42453d --- /dev/null +++ b/src/hooks/useCards.ts @@ -0,0 +1,42 @@ +import { useMemo } from "react"; +import { initialCards } from "../constants"; +import { Card } from "../domains/RegisterPage/CardRegister/types"; +import useLocalStorage from "./useLocalStorage"; + +type useCardsArgs = { + sortByKey?: keyof Card; + sortMethod?: "asc" | "dcs"; +}; + +export default function useCards({ + sortByKey = "createdAt", + sortMethod = "asc", +}: useCardsArgs) { + const [cards, setCards] = useLocalStorage("mycards", initialCards); + + const sortedCards = useMemo(() => { + const isAscended = sortMethod === "asc"; + return cards.sort((a, b) => { + const keyA = isAscended + ? a[sortByKey].toString() + : b[sortByKey].toString(); + const keyB = isAscended + ? b[sortByKey].toString() + : a[sortByKey].toString(); + + if (keyA < keyB) return -1; + if (keyA > keyB) return 1; + + return 0; + }); + }, [cards, sortByKey, sortMethod]); + + function deleteCard(uuid: string) { + return function deleteCard() { + const filtered = sortedCards.filter((value) => uuid !== value.uuid); + setCards(filtered); + }; + } + + return { cards: sortedCards, setCards, deleteCard }; +} diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..4f52ccfb5 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,17 @@ +import { useCallback, useLayoutEffect, useState } from "react"; + +export default function useLocalStorage(key: string, initialValue: T) { + const [store, setStore] = useState(initialValue); + + const setStorage = useCallback((value: T) => { + setStore(value); + localStorage.setItem(key, JSON.stringify(value)); + }, []); + + useLayoutEffect(() => { + const existed = localStorage.getItem(key); + if (existed) return setStore(JSON.parse(existed)); + }, []); + + return [store, setStorage] as const; +} diff --git a/src/utils.ts b/src/utils.ts index 5c98b789d..01cbae940 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,3 +15,11 @@ export function isNumberFromString(number: string) { export function compareShallowValues(obj1: T, obj2: T) { return JSON.stringify(obj1) === JSON.stringify(obj2); } + +export function omitObj(obj: Record, keys: string[]) { + const copied = { ...obj }; + + keys.forEach((key) => delete copied[key]); + + return copied as T; +} diff --git a/yarn.lock b/yarn.lock index c92e86e7f..33a0989ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15293,6 +15293,7 @@ __metadata: react-scripts: ^5.0.1 storybook: ^7.6.17 typescript: ^5.2.2 + uuid: ^9.0.1 vite: ^5.1.4 languageName: unknown linkType: soft @@ -17916,7 +17917,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: