From 0c1df6dfe06c082aa23f3671836835e50552762f Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Mon, 11 Aug 2025 15:53:19 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat=20[#8]=20`Toast`=20component=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/ic-check-circle-green.svg | 3 ++ src/assets/ic-xmark.svg | 3 ++ src/components/toast/toast.jsx | 50 ++++++++++++++++++++++++++++ src/pages/test-page.jsx | 4 +++ 4 files changed, 60 insertions(+) create mode 100644 src/assets/ic-check-circle-green.svg create mode 100644 src/assets/ic-xmark.svg create mode 100644 src/components/toast/toast.jsx diff --git a/src/assets/ic-check-circle-green.svg b/src/assets/ic-check-circle-green.svg new file mode 100644 index 0000000..18be8ec --- /dev/null +++ b/src/assets/ic-check-circle-green.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/ic-xmark.svg b/src/assets/ic-xmark.svg new file mode 100644 index 0000000..c490a80 --- /dev/null +++ b/src/assets/ic-xmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/toast/toast.jsx b/src/components/toast/toast.jsx new file mode 100644 index 0000000..dadd5c3 --- /dev/null +++ b/src/components/toast/toast.jsx @@ -0,0 +1,50 @@ +import styled from "styled-components"; +import checkImg from "../../assets/ic-check-circle-green.svg"; +import closeImg from "../../assets/ic-xmark.svg"; + +const StyledToast = styled.div` + background-color: rgba(0, 0, 0, 0.8); + border-radius: 8px; + min-width: 524px; + height: 64px; + padding: 0 30px; + display: flex; + align-items: center; + gap: 12px; + + font-size: 16px; + font-weight: 400; + line-height: 26px; + color: white; + + p { + margin: 0; + flex-grow: 1; + } +`; + +const Icon = styled.div` + width: 24px; + height: 24px; + + img { + width: 100%; + height: 100%; + } +`; + +function Toast({ message }) { + return ( + + + 확인 + +

{message}

+ + 닫기 + +
+ ); +} + +export default Toast; diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 2ae1ed2..39fcdbf 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -14,6 +14,7 @@ import BUTTON_SIZE from "../components/button/button-size"; import ToggleButton from "../components/button/toggle-button"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; +import Toast from "../components/toast/toast"; function TestPage() { const [option1, setOption1] = useState(); @@ -159,6 +160,9 @@ function TestPage() { onSelect={handleDropdownSelect2} /> +
+ +
); } From 6543e98df55592a17606e9dd3cfd3f6c4b1177ae Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Mon, 11 Aug 2025 16:31:36 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat=20[#8]=20`Toast`=EB=A5=BC=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=8B=9C=EA=B0=84=20=EB=B3=B4=EC=97=AC=EC=A3=BC?= =?UTF-8?q?=EA=B3=A0=20=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8A=94=20`useToast`?= =?UTF-8?q?=20custom=20hook=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/toast/toast.jsx | 18 +++++++++++++++++- src/components/toast/useToast.jsx | 21 +++++++++++++++++++++ src/pages/test-page.jsx | 17 +++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/components/toast/useToast.jsx diff --git a/src/components/toast/toast.jsx b/src/components/toast/toast.jsx index dadd5c3..d427e5f 100644 --- a/src/components/toast/toast.jsx +++ b/src/components/toast/toast.jsx @@ -11,16 +11,32 @@ const StyledToast = styled.div` display: flex; align-items: center; gap: 12px; - font-size: 16px; font-weight: 400; line-height: 26px; color: white; + white-space: nowrap; + position: fixed; + left: 50%; + bottom: 70px; + transform: translateX(-50%); p { margin: 0; flex-grow: 1; } + + @media (max-width: 1199px) { + bottom: 50px; + } + + @media (max-width: 767px) { + min-width: 0; + transform: none; + left: 20px; + right: 20px; + bottom: 88px; + } `; const Icon = styled.div` diff --git a/src/components/toast/useToast.jsx b/src/components/toast/useToast.jsx new file mode 100644 index 0000000..8b461b6 --- /dev/null +++ b/src/components/toast/useToast.jsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; + +function useToast(timeout = 2500) { + const [showsToast, setShowsToast] = useState(false); + + useEffect(() => { + if (!showsToast) return; + + const id = setTimeout(() => { + setShowsToast(false); + }, timeout); + + return () => { + clearTimeout(id); + }; + }, [showsToast, setShowsToast, timeout]); + + return { showsToast, setShowsToast }; +} + +export { useToast }; diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 39fcdbf..29cfa5e 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -15,11 +15,14 @@ import ToggleButton from "../components/button/toggle-button"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; import Toast from "../components/toast/toast"; +import { useToast } from "../components/toast/useToast"; function TestPage() { + /* Dropdown type TextField */ const [option1, setOption1] = useState(); const [option2, setOption2] = useState(); const [dropdown2Error, setDropdown2Error] = useState("Error Message"); + const handleDropdownSelect1 = (option) => { setOption1(option); }; @@ -28,6 +31,11 @@ function TestPage() { setDropdown2Error(null); }; + /* Toast */ + const { showsToast, setShowsToast } = useToast(); + + const handleToastClick = () => setShowsToast(true); + return (
-
- +
+ + {showsToast && }
); From 3519f0ccbf80d84242e39cf6e40125b23bcd6a24 Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Mon, 11 Aug 2025 16:35:25 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat=20[#8]=20=EB=8B=AB=EA=B8=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9C=BC=EB=A1=9C=20`Toast`=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=AB=EC=9D=84=20=EC=88=98=20=EC=9E=88=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/toast/toast.jsx | 13 ++++++++++--- src/pages/test-page.jsx | 8 +++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/toast/toast.jsx b/src/components/toast/toast.jsx index d427e5f..e24c6e6 100644 --- a/src/components/toast/toast.jsx +++ b/src/components/toast/toast.jsx @@ -49,16 +49,23 @@ const Icon = styled.div` } `; -function Toast({ message }) { +const IconButton = styled(Icon)` + background: none; + border: none; + padding: 0; + cursor: pointer; +`; + +function Toast({ message, onDismiss }) { return ( 확인

{message}

- + 닫기 - +
); } diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 29cfa5e..96651ea 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -35,6 +35,7 @@ function TestPage() { const { showsToast, setShowsToast } = useToast(); const handleToastClick = () => setShowsToast(true); + const handleToastDismiss = () => setShowsToast(false); return (
- {showsToast && } + {showsToast && ( + + )}
); From 77d369b54687670119fd8cc70ae548a5c4e5ad43 Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Tue, 12 Aug 2025 19:58:05 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor=20[]=20`useDropdown`=EC=9D=84=20`h?= =?UTF-8?q?ooks`=20=EC=95=84=EB=9E=98=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/text-field/dropdown-input/dropdown-input.jsx | 2 +- src/hooks/{dropdown => }/use-dropdown.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/hooks/{dropdown => }/use-dropdown.jsx (92%) diff --git a/src/components/text-field/dropdown-input/dropdown-input.jsx b/src/components/text-field/dropdown-input/dropdown-input.jsx index 09c937b..5524bde 100644 --- a/src/components/text-field/dropdown-input/dropdown-input.jsx +++ b/src/components/text-field/dropdown-input/dropdown-input.jsx @@ -1,7 +1,7 @@ import styled from "styled-components"; import arrowDownImg from "../../../assets/ic-chevron-down.svg"; import arrowUpImg from "../../../assets/ic-chevron-up.svg"; -import { useDropdown } from "../../../hooks/dropdown/use-dropdown"; +import { useDropdown } from "../../../hooks/use-dropdown"; import INPUT_STYLES from "../input-styles"; import Dropdown from "./dropdown"; import DropdownOption from "./dropdown-option"; diff --git a/src/hooks/dropdown/use-dropdown.jsx b/src/hooks/use-dropdown.jsx similarity index 92% rename from src/hooks/dropdown/use-dropdown.jsx rename to src/hooks/use-dropdown.jsx index cdc06af..16f1601 100644 --- a/src/hooks/dropdown/use-dropdown.jsx +++ b/src/hooks/use-dropdown.jsx @@ -1,5 +1,5 @@ import { useContext, useRef, useState } from "react"; -import DropdownContext from "../../components/text-field/dropdown-input/dropdown-context"; +import DropdownContext from "../components/text-field/dropdown-input/dropdown-context"; function makeRect({ x, y, width } = { x: 0, y: 0, width: 0 }) { return { From 07200f3ec3da8111c29eb9b890b424df991ada7a Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Tue, 12 Aug 2025 19:58:23 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor=20[]=20`useToast`=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B4=EB=A6=84=EC=9D=84=20kebab=20case=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{components/toast/useToast.jsx => hooks/use-toast.jsx} | 0 src/pages/test-page.jsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{components/toast/useToast.jsx => hooks/use-toast.jsx} (100%) diff --git a/src/components/toast/useToast.jsx b/src/hooks/use-toast.jsx similarity index 100% rename from src/components/toast/useToast.jsx rename to src/hooks/use-toast.jsx diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 96651ea..f0ddc35 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -15,7 +15,7 @@ import ToggleButton from "../components/button/toggle-button"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; import Toast from "../components/toast/toast"; -import { useToast } from "../components/toast/useToast"; +import { useToast } from "../hooks/use-toast"; function TestPage() { /* Dropdown type TextField */ From b2dd6b27265f548aa6559eab3271906fd5854f46 Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Mon, 11 Aug 2025 19:18:45 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat=20[#10]=20`Modal`=20component=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/ic-person.svg | 4 + src/components/modal/modal.jsx | 175 +++++++++++++++++++++++++++++++++ src/pages/test-page.jsx | 8 ++ src/utils/formatter.js | 8 ++ 4 files changed, 195 insertions(+) create mode 100644 src/assets/ic-person.svg create mode 100644 src/components/modal/modal.jsx create mode 100644 src/utils/formatter.js diff --git a/src/assets/ic-person.svg b/src/assets/ic-person.svg new file mode 100644 index 0000000..9aa412e --- /dev/null +++ b/src/assets/ic-person.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx new file mode 100644 index 0000000..fe17c58 --- /dev/null +++ b/src/components/modal/modal.jsx @@ -0,0 +1,175 @@ +import styled, { css } from "styled-components"; +import defaultProfileImg from "../../assets/ic-person.svg"; +import { formatDate } from "../../utils/formatter"; +import Badge from "../badge/badge"; +import BADGE_TYPE from "../badge/badge-type"; +import { PrimaryButton } from "../button/button"; +import BUTTON_SIZE from "../button/button-size"; +import Colors from "../color/colors"; + +/* ProfileImage */ + +const profileImageStyle = css` + width: 56px; + height: 56px; + border-radius: 28px; +`; + +const UserProfileImage = styled.div` + ${profileImageStyle} + + img { + width: 100%; + height: 100%; + } +`; + +const DefaultProfileImage = styled.div` + ${profileImageStyle} + background-color: ${Colors.gray(300)}; + display: flex; + justify-content: center; + align-items: center; + + img { + width: 32px; + height: 32px; + } +`; + +function ProfileImage({ profileImg }) { + const img = 프로필 사진; + return profileImg ? ( + {img} + ) : ( + {img} + ); +} + +/* UserInfo */ + +const Name = styled.span` + font-size: 20px; + font-weight: 400; + line-height: 24px; + + span { + font-weight: 700; + } +`; + +const StyledUserInfo = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +`; + +function UserInfo({ name, type }) { + return ( + + + From.{` ${name}`} + + + + ); +} + +/* UserProfile */ + +const StyledUserProfile = styled.div` + display: flex; + align-items: center; + gap: 16px; +`; + +function UserProfile({ profileImg, name }) { + return ( + + + + + ); +} + +/* Header */ + +const Date = styled.span` + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: ${Colors.gray(400)}; +`; + +const StyledHeader = styled.div` + width: 100%; + border-bottom: 1px solid ${Colors.gray(200)}; + + & > div { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 19px; + } +`; + +function Header({ profileImg, name, date }) { + return ( + +
+ + {formatDate(date, ".")} +
+
+ ); +} + +/* Modal */ + +const Content = styled.p` + margin: 16px 0 24px; + height: 240px; + overflow: scroll; + font-size: 18px; + font-weight: 400; + line-height: 28px; + color: #5a5a5a; + padding-right: 16px; + + &::-webkit-scrollbar { + width: 4px; + height: 0px; + } + + &::-webkit-scrollbar-thumb { + background-color: ${Colors.gray(300)}; + border-radius: 2px; + + &:hover { + background-color: ${Colors.gray(400)}; + } + } +`; + +const StyledModal = styled.div` + width: 600px; + border-radius: 16px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); + padding: 40px; + display: flex; + flex-direction: column; + align-items: center; +`; + +function Modal({ user, date, content }) { + return ( + +
+ {content} + + + ); +} + +export default Modal; diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index f0ddc35..1e08dec 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -12,6 +12,7 @@ import { } from "../components/button/button"; import BUTTON_SIZE from "../components/button/button-size"; import ToggleButton from "../components/button/toggle-button"; +import Modal from "../components/modal/modal"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; import Toast from "../components/toast/toast"; @@ -182,6 +183,13 @@ function TestPage() { /> )} +
+ +
); } diff --git a/src/utils/formatter.js b/src/utils/formatter.js new file mode 100644 index 0000000..14596a1 --- /dev/null +++ b/src/utils/formatter.js @@ -0,0 +1,8 @@ +function formatDate(date, token = "-") { + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, "0"); + const day = `${date.getDate()}`.padStart(2, "0"); + return `${year}${token}${month}${token}${day}`; +} + +export { formatDate }; From 8a82af25ac16d663550e22385775721de3031ed9 Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Tue, 12 Aug 2025 20:05:53 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat=20[#10]=20Modal=EC=9D=98=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20context=20=EB=B0=8F=20provider=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.jsx | 13 +++++++++++-- src/components/modal/modal-context.js | 5 +++++ src/components/modal/modal-provider.jsx | 10 ++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/components/modal/modal-context.js create mode 100644 src/components/modal/modal-provider.jsx diff --git a/src/app.jsx b/src/app.jsx index 7fb23d8..47815ca 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -1,18 +1,27 @@ import { BrowserRouter, Route, Routes } from "react-router"; +import ModalProvider from "./components/modal/modal-provider"; import DropdownProvider from "./components/text-field/dropdown-input/dropdown-provider"; import MessagePage from "./pages/message-list"; import TestPage from "./pages/test-page"; +function Provider({ children }) { + return ( + + {children} + + ); +} + function App() { return ( - + } /> } /> - + ); } diff --git a/src/components/modal/modal-context.js b/src/components/modal/modal-context.js new file mode 100644 index 0000000..2cee28d --- /dev/null +++ b/src/components/modal/modal-context.js @@ -0,0 +1,5 @@ +import { createContext } from "react"; + +const ModalContext = createContext(); + +export default ModalContext; diff --git a/src/components/modal/modal-provider.jsx b/src/components/modal/modal-provider.jsx new file mode 100644 index 0000000..f7b6d44 --- /dev/null +++ b/src/components/modal/modal-provider.jsx @@ -0,0 +1,10 @@ +import { useState } from "react"; +import ModalContext from "./modal-context"; + +function ModalProvider({ children }) { + const [showsModal, setShowsModal] = useState(false); + const value = { showsModal, setShowsModal }; + return {children}; +} + +export default ModalProvider; From 79888c9081cd537be010979d7db7484bbe93e306 Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Tue, 12 Aug 2025 20:05:59 +0900 Subject: [PATCH 8/8] =?UTF-8?q?Button=20=EB=93=B1=20trigger=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20Modal=20=EB=9D=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 1 + src/components/modal/modal.jsx | 43 ++++++++++++++++++++++++++++++---- src/hooks/use-modal.jsx | 9 +++++++ src/pages/test-page.jsx | 20 ++++++++++++---- 4 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 src/hooks/use-modal.jsx diff --git a/index.html b/index.html index e57d567..80aec14 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,7 @@
+ diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index fe17c58..d8b4aa2 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -1,5 +1,7 @@ +import { createPortal } from "react-dom"; import styled, { css } from "styled-components"; import defaultProfileImg from "../../assets/ic-person.svg"; +import { useModal } from "../../hooks/use-modal"; import { formatDate } from "../../utils/formatter"; import Badge from "../badge/badge"; import BADGE_TYPE from "../badge/badge-type"; @@ -153,6 +155,7 @@ const Content = styled.p` `; const StyledModal = styled.div` + background-color: white; width: 600px; border-radius: 16px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); @@ -162,13 +165,43 @@ const StyledModal = styled.div` align-items: center; `; +/* Container */ + +const ModalContainer = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; +`; + function Modal({ user, date, content }) { + const { setShowsModal } = useModal(); + + const ModalPortal = ({ children }) => { + return createPortal(children, document.getElementById("modal")); + }; + + const handleConfirmClick = () => setShowsModal(false); + return ( - -
- {content} - - + + + +
+ {content} + + + + ); } diff --git a/src/hooks/use-modal.jsx b/src/hooks/use-modal.jsx new file mode 100644 index 0000000..d215967 --- /dev/null +++ b/src/hooks/use-modal.jsx @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import ModalContext from "../components/modal/modal-context"; + +function useModal() { + const { showsModal, setShowsModal } = useContext(ModalContext); + return { showsModal, setShowsModal }; +} + +export { useModal }; diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index 1e08dec..98dc546 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -16,6 +16,7 @@ import Modal from "../components/modal/modal"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; import Toast from "../components/toast/toast"; +import { useModal } from "../hooks/use-modal"; import { useToast } from "../hooks/use-toast"; function TestPage() { @@ -38,6 +39,10 @@ function TestPage() { const handleToastClick = () => setShowsToast(true); const handleToastDismiss = () => setShowsToast(false); + /* Modal */ + const { showsModal, setShowsModal } = useModal(); + const handleModalClick = () => setShowsModal(true); + return (
- + {showsModal && ( + + )}
);