From 24b3b696b792f37ad4d865150dae001802faaf3c Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 09:29:58 +0900 Subject: [PATCH 001/498] =?UTF-8?q?feat:=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.tsx | 1 + shared/ui/modal/index.tsx | 60 ++++++++++++++++++++++++++++++ shared/ui/modal/styles.module.scss | 57 ++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 shared/ui/modal/index.tsx create mode 100644 shared/ui/modal/styles.module.scss diff --git a/app/layout.tsx b/app/layout.tsx index 922c9aac..62afa32c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -18,6 +18,7 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => { {children} + ) diff --git a/shared/ui/modal/index.tsx b/shared/ui/modal/index.tsx new file mode 100644 index 00000000..c645bf13 --- /dev/null +++ b/shared/ui/modal/index.tsx @@ -0,0 +1,60 @@ +'use client' + +import { ReactNode } from 'react' + +import classNames from 'classnames/bind' +import { createPortal } from 'react-dom' + +import { Button } from '../button' +import styles from './styles.module.scss' + +const cx = classNames.bind(styles) + +interface Props { + title?: string + icon?: ReactNode + isModalOpen: boolean + hasTwoButtons?: boolean + confirmModal?: () => void + closeModal: () => void +} + +const Modal = ({ + title, + icon, + isModalOpen, + hasTwoButtons = false, + // confirmModal, + closeModal, +}: Props) => { + if (!isModalOpen) return null + + const modalRoot = document.getElementById('modal-root') + + if (!modalRoot) return null + + return createPortal( + <> +
오버레이
+
+ {/*
X
*/} +
{icon}
+

{title}

+ + {hasTwoButtons ? ( +
+ {/* */} + +
+ ) : ( + + )} +
+ , + modalRoot + ) +} + +export default Modal diff --git a/shared/ui/modal/styles.module.scss b/shared/ui/modal/styles.module.scss new file mode 100644 index 00000000..a9798201 --- /dev/null +++ b/shared/ui/modal/styles.module.scss @@ -0,0 +1,57 @@ +.overlay { + position: fixed; + width: 100%; + height: 100%; + background-color: $color-black; + opacity: 0.2; + z-index: 2; +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: $color-gray-100; + border: 1px solid $color-gray-200; + border-radius: 8px; + z-index: 3; + padding: 20px; + width: 360px; + height: 280px; +} + +.close-icon { + position: absolute; + top: 10px; + right: 10px; + font-size: 20px; + cursor: pointer; +} + +.icon { + margin-bottom: 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.title { + margin-bottom: 20px; + font-size: 18px; + font-weight: bold; + text-align: center; +} + +.two-buttons { + display: flex; + justify-content: space-between; + width: 100%; + gap: 10px; +} + +.close-button { + margin-top: 20px; + padding: 10px 20px; + cursor: pointer; +} From e8eb0e0681f81bb7e26e21fc04f3ebf750800bac Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 09:44:00 +0900 Subject: [PATCH 002/498] =?UTF-8?q?feat:=20constants=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EC=97=90=20Modal=20=ED=83=80=EC=9D=B4=ED=8B=80=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/constants/modal-titles.ts | 6 ++++++ shared/ui/modal/index.tsx | 4 +++- shared/ui/modal/styles.module.scss | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 shared/constants/modal-titles.ts diff --git a/shared/constants/modal-titles.ts b/shared/constants/modal-titles.ts new file mode 100644 index 00000000..22ea04a4 --- /dev/null +++ b/shared/constants/modal-titles.ts @@ -0,0 +1,6 @@ +export const MODAL_TITLES = { + SUBSCRIBE: '전략을 구독합니다. \n 구독한 전략은 나의 관심전략 \n 페이지에서 확인 가능합니다.', + VERIFICATIONCODE: '인증코드가 이메일로 발송되었습니다.\n 메일을 확인해주세요.', + LOGIN: '로그인이 필요합니다.\n 로그인하시겠습니까?', + DELETE: '리뷰를 삭제하겠습니까?', +} diff --git a/shared/ui/modal/index.tsx b/shared/ui/modal/index.tsx index c645bf13..b180f330 100644 --- a/shared/ui/modal/index.tsx +++ b/shared/ui/modal/index.tsx @@ -5,6 +5,8 @@ import { ReactNode } from 'react' import classNames from 'classnames/bind' import { createPortal } from 'react-dom' +import { MODAL_TITLES } from '@/shared/constants/modal-titles' + import { Button } from '../button' import styles from './styles.module.scss' @@ -20,7 +22,7 @@ interface Props { } const Modal = ({ - title, + title = MODAL_TITLES.SUBSCRIBE, icon, isModalOpen, hasTwoButtons = false, diff --git a/shared/ui/modal/styles.module.scss b/shared/ui/modal/styles.module.scss index a9798201..fe46648e 100644 --- a/shared/ui/modal/styles.module.scss +++ b/shared/ui/modal/styles.module.scss @@ -41,6 +41,7 @@ font-size: 18px; font-weight: bold; text-align: center; + white-space: pre-line; } .two-buttons { From 8a7539f90ea313c666fc97b90681758a36088d81 Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 10:25:19 +0900 Subject: [PATCH 003/498] =?UTF-8?q?chore:=20Modal=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20SVG=20=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/close-icon.svg | 4 ++++ public/icons/index.tsx | 4 ++++ public/icons/modal-alert.svg | 13 +++++++++++++ public/icons/modal-check.svg | 14 ++++++++++++++ public/icons/modal-subscribe.svg | 14 ++++++++++++++ shared/styles/stories/icons.stories.tsx | 8 ++++++++ 6 files changed, 57 insertions(+) create mode 100644 public/icons/close-icon.svg create mode 100644 public/icons/modal-alert.svg create mode 100644 public/icons/modal-check.svg create mode 100644 public/icons/modal-subscribe.svg diff --git a/public/icons/close-icon.svg b/public/icons/close-icon.svg new file mode 100644 index 00000000..08036833 --- /dev/null +++ b/public/icons/close-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/index.tsx b/public/icons/index.tsx index 9b314739..17294274 100644 --- a/public/icons/index.tsx +++ b/public/icons/index.tsx @@ -25,3 +25,7 @@ export { default as StrategyRankingIcon } from './strategy-ranking.svg' export { default as StrategyIcon } from './strategy.svg' export { default as TradersIcon } from './traders.svg' export { default as StarIcon } from './star.svg' +export { default as CloseIcon } from './close-icon.svg' +export { default as ModalAlertIcon } from './modal-alert.svg' +export { default as ModalSubscribeIcon } from './modal-subscribe.svg' +export { default as ModalCheckIcon } from './modal-check.svg' diff --git a/public/icons/modal-alert.svg b/public/icons/modal-alert.svg new file mode 100644 index 00000000..a4790be8 --- /dev/null +++ b/public/icons/modal-alert.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/modal-check.svg b/public/icons/modal-check.svg new file mode 100644 index 00000000..6be252cf --- /dev/null +++ b/public/icons/modal-check.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/modal-subscribe.svg b/public/icons/modal-subscribe.svg new file mode 100644 index 00000000..4fe28229 --- /dev/null +++ b/public/icons/modal-subscribe.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/shared/styles/stories/icons.stories.tsx b/shared/styles/stories/icons.stories.tsx index 8e9d5a93..8b9ebdb1 100644 --- a/shared/styles/stories/icons.stories.tsx +++ b/shared/styles/stories/icons.stories.tsx @@ -10,8 +10,12 @@ import { ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, + CloseIcon, DailyGraphIcon, FileIcon, + ModalAlertIcon, + ModalCheckIcon, + ModalSubscribeIcon, MoneyIcon, MonthlyGraphIcon, NoticeIcon, @@ -61,6 +65,10 @@ const icons = [ { name: 'StrategyIcon', icon: StrategyIcon }, { name: 'TradersIcon', icon: TradersIcon }, { name: 'StarIcon', icon: StarIcon }, + { name: 'CloseIcon', icon: CloseIcon }, + { name: 'ModalAlertIcon', icon: ModalAlertIcon }, + { name: 'ModalSubscribeIcon', icon: ModalSubscribeIcon }, + { name: 'ModalCheckIcon', icon: ModalCheckIcon }, ] export const Icons: Story = { From ae0d490e5bc3151ebd6d54a1bfd139b482b2c3eb Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Nov 2024 11:51:25 +0900 Subject: [PATCH 004/498] =?UTF-8?q?feat:=20=EC=9B=90=ED=98=95=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/circle.svg | 3 +++ public/icons/index.tsx | 1 + 2 files changed, 4 insertions(+) create mode 100644 public/icons/circle.svg diff --git a/public/icons/circle.svg b/public/icons/circle.svg new file mode 100644 index 00000000..d7d31c87 --- /dev/null +++ b/public/icons/circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/index.tsx b/public/icons/index.tsx index 9b314739..8d8b637c 100644 --- a/public/icons/index.tsx +++ b/public/icons/index.tsx @@ -25,3 +25,4 @@ export { default as StrategyRankingIcon } from './strategy-ranking.svg' export { default as StrategyIcon } from './strategy.svg' export { default as TradersIcon } from './traders.svg' export { default as StarIcon } from './star.svg' +export { default as CircleIcon } from './circle.svg' From d8eeae778651f22647472036a50ce87f6470178d Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Nov 2024 11:56:56 +0900 Subject: [PATCH 005/498] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 - shared/styles/base/_mixins.scss | 2 +- shared/ui/check-box/check-box.stories.tsx | 34 +++++++++++++++ shared/ui/check-box/index.tsx | 39 +++++++++++++++++ shared/ui/check-box/styles.module.scss | 53 +++++++++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 shared/ui/check-box/check-box.stories.tsx create mode 100644 shared/ui/check-box/index.tsx create mode 100644 shared/ui/check-box/styles.module.scss diff --git a/.eslintrc.js b/.eslintrc.js index 89b5764e..7999be84 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,6 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', - 'plugin:jsx-a11y/recommended', 'plugin:prettier/recommended', 'next/core-web-vitals', 'next/typescript', diff --git a/shared/styles/base/_mixins.scss b/shared/styles/base/_mixins.scss index 3ae51ca0..339b00a1 100644 --- a/shared/styles/base/_mixins.scss +++ b/shared/styles/base/_mixins.scss @@ -112,4 +112,4 @@ -webkit-box-orient: vertical; overflow: hidden; } -} \ No newline at end of file +} diff --git a/shared/ui/check-box/check-box.stories.tsx b/shared/ui/check-box/check-box.stories.tsx new file mode 100644 index 00000000..20e5c537 --- /dev/null +++ b/shared/ui/check-box/check-box.stories.tsx @@ -0,0 +1,34 @@ +import { useState } from 'react' + +import type { Meta, StoryObj } from '@storybook/react' + +import Checkbox from './index' + +const meta = { + title: 'Components/Checkbox', + component: Checkbox, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type StoryType = StoryObj + +const CheckboxWithHook = ({ ...props }) => { + const [isChecked, setIsChecked] = useState(false) + return +} + +export const Gray500: StoryType = { + render: () => , +} + +export const Gray600: StoryType = { + render: () => , +} + +export const Gray800: StoryType = { + render: () => , +} diff --git a/shared/ui/check-box/index.tsx b/shared/ui/check-box/index.tsx new file mode 100644 index 00000000..920c29ff --- /dev/null +++ b/shared/ui/check-box/index.tsx @@ -0,0 +1,39 @@ +'use client' + +import { CircleIcon } from '@/public/icons' +import classNames from 'classnames/bind' + +import styles from './styles.module.scss' + +const cx = classNames.bind(styles) + +interface Props { + label?: string + isChecked: boolean + onChange: (checked: boolean) => void + className?: string + textColor?: 'gray500' | 'gray600' | 'gray800' +} + +const Checkbox = ({ label, isChecked, onChange, className = '', textColor = 'gray500' }: Props) => { + const handleClick = () => { + onChange(!isChecked) + } + + return ( +
+
+ + {isChecked &&
} +
+ {label && {label}} +
+ ) +} + +export default Checkbox diff --git a/shared/ui/check-box/styles.module.scss b/shared/ui/check-box/styles.module.scss new file mode 100644 index 00000000..5c6efb6e --- /dev/null +++ b/shared/ui/check-box/styles.module.scss @@ -0,0 +1,53 @@ +.checkboxContainer { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + position: relative; +} + +.innerCircle { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + &.gray500 { + background-color: $color-gray-500; + } + + &.gray600 { + background-color: $color-gray-600; + } + + &.gray800 { + background-color: $color-gray-800; + } +} + +.label { + font-size: $text-c1; + font-weight: $text-medium; + + &.gray500 { + color: $color-gray-500; + } + + &.gray600 { + color: $color-gray-600; + } + + &.gray800 { + color: $color-gray-800; + } +} From 213b4c0d9cbf193cfefac1fec0c6423574f789f9 Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 12:16:20 +0900 Subject: [PATCH 006/498] =?UTF-8?q?design:=20Modal=20SCSS=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20modal-root=20=EC=88=98=EC=A0=95=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.tsx | 2 +- public/mockServiceWorker.js | 9 ++++++--- shared/ui/modal/index.tsx | 17 +++++++++------- shared/ui/modal/styles.module.scss | 32 ++++++++++++++++++++---------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 62afa32c..ad62da4b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,11 +14,11 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => { return ( + {children} - ) diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 742f3083..8241ef8c 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -145,7 +145,7 @@ async function handleRequest(event, requestId) { headers: Object.fromEntries(responseClone.headers.entries()), }, }, - [responseClone.body] + [responseClone.body], ) })() } @@ -240,7 +240,7 @@ async function getResponse(event, client, requestId) { keepalive: request.keepalive, }, }, - [requestBuffer] + [requestBuffer], ) switch (clientMessage.type) { @@ -268,7 +268,10 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data) } - client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean))) + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) }) } diff --git a/shared/ui/modal/index.tsx b/shared/ui/modal/index.tsx index b180f330..f054d75a 100644 --- a/shared/ui/modal/index.tsx +++ b/shared/ui/modal/index.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react' +import { CloseIcon } from '@/public/icons' import classNames from 'classnames/bind' import { createPortal } from 'react-dom' @@ -17,16 +18,16 @@ interface Props { icon?: ReactNode isModalOpen: boolean hasTwoButtons?: boolean - confirmModal?: () => void + confirmButton: () => void closeModal: () => void } const Modal = ({ - title = MODAL_TITLES.SUBSCRIBE, + title = MODAL_TITLES.LOGIN, icon, isModalOpen, hasTwoButtons = false, - // confirmModal, + confirmButton, closeModal, }: Props) => { if (!isModalOpen) return null @@ -37,15 +38,17 @@ const Modal = ({ return createPortal( <> -
오버레이
-
- {/*
X
*/} +
+
+
{icon}

{title}

{hasTwoButtons ? (
- {/* */} +
) : ( diff --git a/shared/ui/modal/styles.module.scss b/shared/ui/modal/styles.module.scss index fe46648e..bc63f582 100644 --- a/shared/ui/modal/styles.module.scss +++ b/shared/ui/modal/styles.module.scss @@ -19,40 +19,52 @@ padding: 20px; width: 360px; height: 280px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + + &.has-two-buttons { + height: 240px; + } } .close-icon { position: absolute; - top: 10px; - right: 10px; - font-size: 20px; + top: 12px; + right: 12px; cursor: pointer; } .icon { - margin-bottom: 10px; display: flex; justify-content: center; align-items: center; + margin-top: 20px; } .title { - margin-bottom: 20px; - font-size: 18px; + @include typo-b1; font-weight: bold; text-align: center; white-space: pre-line; + color: $color-gray-800; } .two-buttons { display: flex; - justify-content: space-between; + justify-content: center; width: 100%; - gap: 10px; + gap: 18px; +} +.confirm-button { + border-color: $color-orange-500; + color: $color-orange-500; + cursor: pointer; + width: 90px; } .close-button { - margin-top: 20px; - padding: 10px 20px; cursor: pointer; + align-self: center; } From 63a142c6af6aa9191b1a8ae8c5b32a946cd9fee1 Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 12:22:58 +0900 Subject: [PATCH 007/498] =?UTF-8?q?refactor:=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/modal/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shared/ui/modal/index.tsx b/shared/ui/modal/index.tsx index f054d75a..b72762ca 100644 --- a/shared/ui/modal/index.tsx +++ b/shared/ui/modal/index.tsx @@ -6,8 +6,6 @@ import { CloseIcon } from '@/public/icons' import classNames from 'classnames/bind' import { createPortal } from 'react-dom' -import { MODAL_TITLES } from '@/shared/constants/modal-titles' - import { Button } from '../button' import styles from './styles.module.scss' @@ -23,7 +21,7 @@ interface Props { } const Modal = ({ - title = MODAL_TITLES.LOGIN, + title, icon, isModalOpen, hasTwoButtons = false, From 53857b0521960c00636cfca1bbc248d2f618e887 Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 13:15:10 +0900 Subject: [PATCH 008/498] =?UTF-8?q?feat:=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/modal/modal.stories.tsx | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 shared/ui/modal/modal.stories.tsx diff --git a/shared/ui/modal/modal.stories.tsx b/shared/ui/modal/modal.stories.tsx new file mode 100644 index 00000000..c9182d0e --- /dev/null +++ b/shared/ui/modal/modal.stories.tsx @@ -0,0 +1,43 @@ +import { Meta, StoryObj } from '@storybook/react' +import { ModalAlertIcon } from 'public/icons/index' + +import Modal from './index' + +const meta: Meta = { + title: 'Components/Modal', + component: Modal, + args: { + title: '기본 모달 제목', + icon: , + isModalOpen: true, + hasTwoButtons: false, + confirmButton: () => alert('확인 버튼 클릭'), + closeModal: () => alert('닫기 버튼 클릭'), + }, + argTypes: { + title: { control: 'text' }, + isModalOpen: { control: 'boolean' }, + hasTwoButtons: { control: 'boolean' }, + confirmButton: { action: 'confirmButton' }, + closeModal: { action: 'closeModal' }, + }, + tags: ['autodocs'], +} + +type StoryType = StoryObj + +export const Default: StoryType = {} + +export const TwoButtons: StoryType = { + args: { + hasTwoButtons: true, + }, +} + +export const WithCustomIcon: StoryType = { + args: { + icon: , + }, +} + +export default meta From 6c61ae94f76b28f502c53074434026fafdf93272 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Nov 2024 13:25:25 +0900 Subject: [PATCH 009/498] =?UTF-8?q?design:=20mixin=20typo-c1=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/check-box/styles.module.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shared/ui/check-box/styles.module.scss b/shared/ui/check-box/styles.module.scss index 5c6efb6e..db9f7d33 100644 --- a/shared/ui/check-box/styles.module.scss +++ b/shared/ui/check-box/styles.module.scss @@ -36,8 +36,7 @@ } .label { - font-size: $text-c1; - font-weight: $text-medium; + @include typo-c1; &.gray500 { color: $color-gray-500; From ba1ac646fc44ba986d90a7037f38f6505f51895b Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 13:43:11 +0900 Subject: [PATCH 010/498] =?UTF-8?q?chore:=20Modal=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EB=B6=81=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/mockServiceWorker.js | 9 +++------ shared/styles/base/_mixins.scss | 2 +- shared/ui/modal/modal.stories.tsx | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 8241ef8c..742f3083 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -145,7 +145,7 @@ async function handleRequest(event, requestId) { headers: Object.fromEntries(responseClone.headers.entries()), }, }, - [responseClone.body], + [responseClone.body] ) })() } @@ -240,7 +240,7 @@ async function getResponse(event, client, requestId) { keepalive: request.keepalive, }, }, - [requestBuffer], + [requestBuffer] ) switch (clientMessage.type) { @@ -268,10 +268,7 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data) } - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ) + client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean))) }) } diff --git a/shared/styles/base/_mixins.scss b/shared/styles/base/_mixins.scss index 3ae51ca0..339b00a1 100644 --- a/shared/styles/base/_mixins.scss +++ b/shared/styles/base/_mixins.scss @@ -112,4 +112,4 @@ -webkit-box-orient: vertical; overflow: hidden; } -} \ No newline at end of file +} diff --git a/shared/ui/modal/modal.stories.tsx b/shared/ui/modal/modal.stories.tsx index c9182d0e..f75728e0 100644 --- a/shared/ui/modal/modal.stories.tsx +++ b/shared/ui/modal/modal.stories.tsx @@ -34,7 +34,7 @@ export const TwoButtons: StoryType = { }, } -export const WithCustomIcon: StoryType = { +export const WithIcon: StoryType = { args: { icon: , }, From 9b19f582d775272e38d6879464ca03f9fc2e66b8 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Nov 2024 18:41:32 +0900 Subject: [PATCH 011/498] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=EB=90=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/checked-circle.svg | 6 ++++++ public/icons/circle.svg | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 public/icons/checked-circle.svg diff --git a/public/icons/checked-circle.svg b/public/icons/checked-circle.svg new file mode 100644 index 00000000..3e86839c --- /dev/null +++ b/public/icons/checked-circle.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/icons/circle.svg b/public/icons/circle.svg index d7d31c87..6f7cdea2 100644 --- a/public/icons/circle.svg +++ b/public/icons/circle.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file From f82cfcbaf7dfd0b5e7613ccd488f62da18f886b1 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Nov 2024 18:42:20 +0900 Subject: [PATCH 012/498] =?UTF-8?q?design:=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=83=81=ED=83=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/index.tsx | 1 + shared/ui/check-box/index.tsx | 5 ++--- shared/ui/check-box/styles.module.scss | 22 ++++++---------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/public/icons/index.tsx b/public/icons/index.tsx index 8d8b637c..aeb424f9 100644 --- a/public/icons/index.tsx +++ b/public/icons/index.tsx @@ -26,3 +26,4 @@ export { default as StrategyIcon } from './strategy.svg' export { default as TradersIcon } from './traders.svg' export { default as StarIcon } from './star.svg' export { default as CircleIcon } from './circle.svg' +export { default as CheckedCircleIcon } from './checked-circle.svg' diff --git a/shared/ui/check-box/index.tsx b/shared/ui/check-box/index.tsx index 920c29ff..2ad48b8b 100644 --- a/shared/ui/check-box/index.tsx +++ b/shared/ui/check-box/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { CircleIcon } from '@/public/icons' +import { CheckedCircleIcon, CircleIcon } from '@/public/icons' import classNames from 'classnames/bind' import styles from './styles.module.scss' @@ -28,8 +28,7 @@ const Checkbox = ({ label, isChecked, onChange, className = '', textColor = 'gra [`checked${textColor}`]: isChecked, })} > - - {isChecked &&
} + {isChecked ? : }
{label && {label}}
diff --git a/shared/ui/check-box/styles.module.scss b/shared/ui/check-box/styles.module.scss index db9f7d33..17ddf3f5 100644 --- a/shared/ui/check-box/styles.module.scss +++ b/shared/ui/check-box/styles.module.scss @@ -11,27 +11,17 @@ justify-content: center; flex-shrink: 0; position: relative; -} - -.innerCircle { - position: absolute; - width: 8px; - height: 8px; - border-radius: 50%; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - &.gray500 { - background-color: $color-gray-500; + &.checkedgray500 svg circle { + fill: $color-gray-500; } - &.gray600 { - background-color: $color-gray-600; + &.checkedgray600 svg circle { + fill: $color-gray-600; } - &.gray800 { - background-color: $color-gray-800; + &.checkedgray800 svg circle { + fill: $color-gray-800; } } From 2559d9151a83ee02a82826332273716d4bbc3bb3 Mon Sep 17 00:00:00 2001 From: nanafromjeju Date: Mon, 18 Nov 2024 20:02:34 +0900 Subject: [PATCH 013/498] =?UTF-8?q?fix:=20Modal=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EB=B6=81=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.tsx | 2 +- .../{modal-titles.ts => modal-contents.ts} | 2 +- shared/ui/modal/index.tsx | 6 +- shared/ui/modal/modal.stories.tsx | 86 ++++++++++++++----- shared/ui/modal/styles.module.scss | 3 +- 5 files changed, 69 insertions(+), 30 deletions(-) rename shared/constants/{modal-titles.ts => modal-contents.ts} (91%) diff --git a/app/layout.tsx b/app/layout.tsx index ad62da4b..f1411d18 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,10 +14,10 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => { return ( - {children} + diff --git a/shared/constants/modal-titles.ts b/shared/constants/modal-contents.ts similarity index 91% rename from shared/constants/modal-titles.ts rename to shared/constants/modal-contents.ts index 22ea04a4..90304617 100644 --- a/shared/constants/modal-titles.ts +++ b/shared/constants/modal-contents.ts @@ -1,4 +1,4 @@ -export const MODAL_TITLES = { +export const MODAL_CONTENTS = { SUBSCRIBE: '전략을 구독합니다. \n 구독한 전략은 나의 관심전략 \n 페이지에서 확인 가능합니다.', VERIFICATIONCODE: '인증코드가 이메일로 발송되었습니다.\n 메일을 확인해주세요.', LOGIN: '로그인이 필요합니다.\n 로그인하시겠습니까?', diff --git a/shared/ui/modal/index.tsx b/shared/ui/modal/index.tsx index b72762ca..dfa959f6 100644 --- a/shared/ui/modal/index.tsx +++ b/shared/ui/modal/index.tsx @@ -14,7 +14,7 @@ const cx = classNames.bind(styles) interface Props { title?: string icon?: ReactNode - isModalOpen: boolean + isOpen: boolean hasTwoButtons?: boolean confirmButton: () => void closeModal: () => void @@ -23,12 +23,12 @@ interface Props { const Modal = ({ title, icon, - isModalOpen, + isOpen, hasTwoButtons = false, confirmButton, closeModal, }: Props) => { - if (!isModalOpen) return null + if (!isOpen) return null const modalRoot = document.getElementById('modal-root') diff --git a/shared/ui/modal/modal.stories.tsx b/shared/ui/modal/modal.stories.tsx index f75728e0..b4c2d1f9 100644 --- a/shared/ui/modal/modal.stories.tsx +++ b/shared/ui/modal/modal.stories.tsx @@ -1,43 +1,83 @@ +import React from 'react' + import { Meta, StoryObj } from '@storybook/react' import { ModalAlertIcon } from 'public/icons/index' +import { Button } from '@/shared/ui/button' + import Modal from './index' const meta: Meta = { title: 'Components/Modal', component: Modal, - args: { - title: '기본 모달 제목', - icon: , - isModalOpen: true, - hasTwoButtons: false, - confirmButton: () => alert('확인 버튼 클릭'), - closeModal: () => alert('닫기 버튼 클릭'), - }, - argTypes: { - title: { control: 'text' }, - isModalOpen: { control: 'boolean' }, - hasTwoButtons: { control: 'boolean' }, - confirmButton: { action: 'confirmButton' }, - closeModal: { action: 'closeModal' }, - }, tags: ['autodocs'], } +export default meta + type StoryType = StoryObj -export const Default: StoryType = {} +const ModalStory = ({ + title, + icon, + hasTwoButtons, +}: { + title?: string + icon?: React.ReactNode + hasTwoButtons?: boolean +}) => { + const [isOpen, setIsOpen] = React.useState(false) + + React.useEffect(() => { + if (typeof window !== 'undefined' && !document.getElementById('modal-root')) { + const modalRoot = document.createElement('div') + modalRoot.id = 'modal-root' + document.body.appendChild(modalRoot) + } + }, []) + + return ( +
+ + setIsOpen(false)} + confirmButton={() => { + alert('확인') + setIsOpen(false) + }} + /> +
+ ) +} + +export const Default: StoryType = { + render: () => , +} export const TwoButtons: StoryType = { - args: { - hasTwoButtons: true, - }, + render: () => , } export const WithIcon: StoryType = { - args: { - icon: , - }, + render: () => } />, } -export default meta +export const LongText: StoryType = { + render: () => ( + + ), +} + +export const FullFeatured: StoryType = { + render: () => ( + } + hasTwoButtons={true} + /> + ), +} diff --git a/shared/ui/modal/styles.module.scss b/shared/ui/modal/styles.module.scss index bc63f582..2c81b2a7 100644 --- a/shared/ui/modal/styles.module.scss +++ b/shared/ui/modal/styles.module.scss @@ -1,7 +1,6 @@ .overlay { position: fixed; - width: 100%; - height: 100%; + inset: 0; background-color: $color-black; opacity: 0.2; z-index: 2; From 01155d83114394987afb4a0fad7fd17c8759e5a0 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Mon, 18 Nov 2024 22:16:23 +0900 Subject: [PATCH 014/498] =?UTF-8?q?feat:=20dropdown=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/dropdown/dropdown.module.scss | 47 ++++++++++ .../ui/dropdown/hooks/use-dropdown-context.ts | 12 +++ shared/ui/dropdown/hooks/use-dropdown.ts | 63 +++++++++++++ shared/ui/dropdown/index.tsx | 92 +++++++++++++++++++ shared/ui/dropdown/option/range-option.tsx | 36 ++++++++ 5 files changed, 250 insertions(+) create mode 100644 shared/ui/dropdown/dropdown.module.scss create mode 100644 shared/ui/dropdown/hooks/use-dropdown-context.ts create mode 100644 shared/ui/dropdown/hooks/use-dropdown.ts create mode 100644 shared/ui/dropdown/index.tsx create mode 100644 shared/ui/dropdown/option/range-option.tsx diff --git a/shared/ui/dropdown/dropdown.module.scss b/shared/ui/dropdown/dropdown.module.scss new file mode 100644 index 00000000..0c5ba769 --- /dev/null +++ b/shared/ui/dropdown/dropdown.module.scss @@ -0,0 +1,47 @@ +// NOTE: color, padding 등 세부 수치들 미정, 일단 제일 가까운 걸로 함 + +.container { + width: 100%; + padding: 7px 11px; + border: 1px solid $color-gray-300; + border-radius: 4px; + display: flex; + align-items: center; + @include typo-c2; +} + +.trigger { + width: 100%; + display: flex; + justify-content: space-between; + color: $color-gray-400; + background-color: #fff; +} + +.options { + list-style: none; + padding: 0; + margin: 0; + background-color: #fff; +} + +.option { + border-top-color: transparent; + cursor: pointer; + + .icon { + color: $color-gray-300; + } + + .selected-icon { + color: $color-gray-800; + } + + &.selected { + background-color: $color-gray-300; + } + + &:not(.selected):hover { + background-color: #f0f0f0; + } +} diff --git a/shared/ui/dropdown/hooks/use-dropdown-context.ts b/shared/ui/dropdown/hooks/use-dropdown-context.ts new file mode 100644 index 00000000..6c4e5d3c --- /dev/null +++ b/shared/ui/dropdown/hooks/use-dropdown-context.ts @@ -0,0 +1,12 @@ +// hooks/useDropdown.ts +import { useContext } from 'react' + +import { DropdownContext } from '..' + +export const useDropdownContext = () => { + const context = useContext(DropdownContext) + if (!context) { + throw new Error('useDropdown must be used within a DropdownProvider') + } + return context +} diff --git a/shared/ui/dropdown/hooks/use-dropdown.ts b/shared/ui/dropdown/hooks/use-dropdown.ts new file mode 100644 index 00000000..3182799a --- /dev/null +++ b/shared/ui/dropdown/hooks/use-dropdown.ts @@ -0,0 +1,63 @@ +import { useEffect, useRef, useState } from 'react' + +interface UseDropdownProps { + onChange?: (value: string | string[]) => void + isMultiple?: boolean +} + +export const useDropdown = ({ onChange, isMultiple = false }: UseDropdownProps) => { + const [isOpen, setIsOpen] = useState(false) + const [selectedValues, setSelectedValues] = useState([]) + const dropdownRef = useRef(null) + + const toggleOpen = () => setIsOpen((prev) => !prev) + + const handleSelect = (value: string) => { + if (isMultiple) { + // 다중 선택 모드 + setSelectedValues((prev) => { + if (prev.includes(value)) { + const result = prev.filter((item) => item !== value) + onChange?.(result) + return result + } + const result = [...prev, value] + onChange?.(result) + return result + }) + } else { + // 단일 선택 모드 + switchValue(value) + } + } + + const switchValue = (value: string) => { + setSelectedValues((prev) => { + const result = prev[0] === value ? [] : [value] + onChange?.(result) + return result + }) + } + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + return { + isOpen, + toggleOpen, + selectedValues, + handleSelect, + switchValue, + dropdownRef, + } +} diff --git a/shared/ui/dropdown/index.tsx b/shared/ui/dropdown/index.tsx new file mode 100644 index 00000000..71d1df8b --- /dev/null +++ b/shared/ui/dropdown/index.tsx @@ -0,0 +1,92 @@ +'use client' + +import { CSSProperties, ReactNode, createContext } from 'react' + +import { CheckboxIcon, ChevronDownIcon, ChevronUpIcon } from '@/public/icons' +import classNames from 'classnames/bind' + +import styles from './dropdown.module.scss' +import { useDropdown } from './hooks/use-dropdown' +import { useDropdownContext } from './hooks/use-dropdown-context' +import RangeOption from './option/range-option' + +const cx = classNames.bind(styles) + +interface DropdownContextProps { + isOpen: boolean + toggleOpen: () => void + selectedValues: string[] + handleSelect: (value: string) => void +} + +const defaultContext = { + isOpen: false, + toggleOpen: () => {}, + selectedValues: [], + handleSelect: () => {}, +} + +export const DropdownContext = createContext(defaultContext) + +export interface DropdownProps { + Trigger: ReactNode + onChange?: (value: string | string[]) => void + isMultiple: boolean + containerStyle?: CSSProperties + labelStyle?: CSSProperties + children?: ReactNode +} + +const Dropdown = ({ + Trigger, + onChange, + isMultiple = false, + containerStyle, + labelStyle, + children, +}: DropdownProps) => { + const { isOpen, toggleOpen, selectedValues, handleSelect, dropdownRef } = useDropdown({ + onChange, + isMultiple, + }) + + return ( + +
+ + {isOpen &&
    {children}
} +
+
+ ) +} +interface ItemProps { + value: string + label: string + hasCheck?: boolean + isMultiple?: boolean + style?: CSSProperties +} + +const Item = ({ value, label, hasCheck = false, style }: ItemProps) => { + const context = useDropdownContext() + + const { handleSelect, selectedValues } = context + const isSelected = selectedValues.includes(value) + + return ( +
  • handleSelect(value)} + aria-hidden + > + {label} + {hasCheck && } +
  • + ) +} + +export default Object.assign(Dropdown, { Item, RangeOption }) diff --git a/shared/ui/dropdown/option/range-option.tsx b/shared/ui/dropdown/option/range-option.tsx new file mode 100644 index 00000000..04e1cd12 --- /dev/null +++ b/shared/ui/dropdown/option/range-option.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react' + +// import { DropdownContext } from '..' +import styles from '../dropdown.module.scss' + +const RangeOption = ({ onRangeChange }: { onRangeChange: (min: string, max: string) => void }) => { + const [range, setRange] = useState({ min: '', max: '' }) + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setRange((prev) => ({ ...prev, [name]: value })) + onRangeChange(range.min, range.max) + } + + return ( +
    + + ~ + +
    + ) +} + +export default RangeOption From 7426cce17a6fd192d85b5b8d991a651a8dece78d Mon Sep 17 00:00:00 2001 From: hoyoen Date: Mon, 18 Nov 2024 22:17:41 +0900 Subject: [PATCH 015/498] =?UTF-8?q?feat:=20select=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/select/index.tsx | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 shared/ui/select/index.tsx diff --git a/shared/ui/select/index.tsx b/shared/ui/select/index.tsx new file mode 100644 index 00000000..a54ff2d8 --- /dev/null +++ b/shared/ui/select/index.tsx @@ -0,0 +1,59 @@ +'use client' + +import { CSSProperties } from 'react' + +import Dropdown from '../dropdown' + +interface OptionModel { + value: string + label: string +} + +interface SelectProps { + title?: string + isRounded?: boolean + isMultiple?: boolean + hasCheck?: boolean + containerStyle?: CSSProperties + titleStyle?: CSSProperties + onChange?: (value: string | string[]) => void + options: OptionModel[] +} + +const Select = ({ + isRounded = false, + isMultiple = false, + hasCheck = false, + containerStyle, + titleStyle, + onChange, + options, + title = options[0].label, +}: SelectProps) => { + if (!options.length) return null + + const roundStyle = { borderRadius: '40px', overflow: 'hidden' } + const labelStyle = isRounded ? { ...roundStyle, ...titleStyle } : titleStyle + + return ( + {title}} + isMultiple={isMultiple} + onChange={onChange} + containerStyle={containerStyle} + labelStyle={labelStyle} + > + {options.map(({ value, label }) => ( + + ))} + + ) +} + +export default Select From daf0c75324bfd740a7c58b22d25a7dc7b7f5f088 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Mon, 18 Nov 2024 22:37:18 +0900 Subject: [PATCH 016/498] =?UTF-8?q?feat:=20select=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9E=91=EC=84=B1=20=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/select/select.stories.tsx | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 shared/ui/select/select.stories.tsx diff --git a/shared/ui/select/select.stories.tsx b/shared/ui/select/select.stories.tsx new file mode 100644 index 00000000..e59c296a --- /dev/null +++ b/shared/ui/select/select.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Select from '.' + +const meta = { + title: 'Components/Select', + component: Select, + decorators: [ + (Story) => ( +
    + +
    + ), + ], + tags: ['autodocs'], +} satisfies Meta + +export default meta +type StoryType = StoryObj + +const options = [ + { + value: '1-', + label: '1년이하', + }, + { + value: '1+', + label: '1년~2년', + }, + { + value: '2+', + label: '2년~3년', + }, + { + value: '3+', + label: '3년이상', + }, +] + +export const Default: StoryType = { + args: { + title: '수익률', + isRounded: false, + isMultiple: false, + hasCheck: false, + onChange: () => {}, + options, + }, +} + +export const Rounded: StoryType = { + args: { + ...Default.args, + isRounded: true, + }, +} + +export const Muliple: StoryType = { + args: { + ...Default.args, + isMultiple: true, + }, +} + +export const WithCheck: StoryType = { + args: { + ...Default.args, + hasCheck: true, + }, +} From 534ed3703d46c3d41b4d5e41c3c332c201ce7e79 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Mon, 18 Nov 2024 22:44:13 +0900 Subject: [PATCH 017/498] =?UTF-8?q?fix:=20With=20Check=20option=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B0=98=EC=98=81=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/dropdown/dropdown.module.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shared/ui/dropdown/dropdown.module.scss b/shared/ui/dropdown/dropdown.module.scss index 0c5ba769..78964ab7 100644 --- a/shared/ui/dropdown/dropdown.module.scss +++ b/shared/ui/dropdown/dropdown.module.scss @@ -31,6 +31,7 @@ .icon { color: $color-gray-300; + margin-left: auto; } .selected-icon { @@ -45,3 +46,7 @@ background-color: #f0f0f0; } } + +.with-check-container { + +} \ No newline at end of file From 045657ada5d9757750548f4ac05e740a7778133c Mon Sep 17 00:00:00 2001 From: hoyoen Date: Mon, 18 Nov 2024 23:07:37 +0900 Subject: [PATCH 018/498] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/dropdown/dropdown.module.scss | 4 ---- .../ui/dropdown/hooks/use-dropdown-context.ts | 1 - shared/ui/dropdown/hooks/use-dropdown.ts | 9 +++----- shared/ui/dropdown/index.tsx | 2 +- shared/ui/select/index.tsx | 22 +++++++++---------- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/shared/ui/dropdown/dropdown.module.scss b/shared/ui/dropdown/dropdown.module.scss index 78964ab7..db645d3c 100644 --- a/shared/ui/dropdown/dropdown.module.scss +++ b/shared/ui/dropdown/dropdown.module.scss @@ -46,7 +46,3 @@ background-color: #f0f0f0; } } - -.with-check-container { - -} \ No newline at end of file diff --git a/shared/ui/dropdown/hooks/use-dropdown-context.ts b/shared/ui/dropdown/hooks/use-dropdown-context.ts index 6c4e5d3c..549cf11b 100644 --- a/shared/ui/dropdown/hooks/use-dropdown-context.ts +++ b/shared/ui/dropdown/hooks/use-dropdown-context.ts @@ -1,4 +1,3 @@ -// hooks/useDropdown.ts import { useContext } from 'react' import { DropdownContext } from '..' diff --git a/shared/ui/dropdown/hooks/use-dropdown.ts b/shared/ui/dropdown/hooks/use-dropdown.ts index 3182799a..d8474240 100644 --- a/shared/ui/dropdown/hooks/use-dropdown.ts +++ b/shared/ui/dropdown/hooks/use-dropdown.ts @@ -16,12 +16,9 @@ export const useDropdown = ({ onChange, isMultiple = false }: UseDropdownProps) if (isMultiple) { // 다중 선택 모드 setSelectedValues((prev) => { - if (prev.includes(value)) { - const result = prev.filter((item) => item !== value) - onChange?.(result) - return result - } - const result = [...prev, value] + const result = prev.includes(value) + ? prev.filter((item) => item !== value) + : [...prev, value] onChange?.(result) return result }) diff --git a/shared/ui/dropdown/index.tsx b/shared/ui/dropdown/index.tsx index 71d1df8b..a042f73e 100644 --- a/shared/ui/dropdown/index.tsx +++ b/shared/ui/dropdown/index.tsx @@ -62,7 +62,7 @@ const Dropdown = ({ ) } -interface ItemProps { +export interface ItemProps { value: string label: string hasCheck?: boolean diff --git a/shared/ui/select/index.tsx b/shared/ui/select/index.tsx index a54ff2d8..7eff3941 100644 --- a/shared/ui/select/index.tsx +++ b/shared/ui/select/index.tsx @@ -2,23 +2,21 @@ import { CSSProperties } from 'react' -import Dropdown from '../dropdown' +import Dropdown, { DropdownProps, ItemProps } from '../dropdown' interface OptionModel { value: string label: string } -interface SelectProps { - title?: string - isRounded?: boolean - isMultiple?: boolean - hasCheck?: boolean - containerStyle?: CSSProperties - titleStyle?: CSSProperties - onChange?: (value: string | string[]) => void - options: OptionModel[] -} +type SelectType = Pick & + Pick & { + title?: string + isRounded?: boolean + titleStyle?: CSSProperties + onChange?: (value: string | string[]) => void + options: OptionModel[] + } const Select = ({ isRounded = false, @@ -29,7 +27,7 @@ const Select = ({ onChange, options, title = options[0].label, -}: SelectProps) => { +}: SelectType) => { if (!options.length) return null const roundStyle = { borderRadius: '40px', overflow: 'hidden' } From 918eb2d8ee4de8ac698dc9094a3c3a4caf111f34 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 01:08:39 +0900 Subject: [PATCH 019/498] =?UTF-8?q?feat:=20Logo=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/logo/index.tsx | 28 ++++++++++++++++++++++++++++ shared/ui/logo/logo.module.scss | 14 ++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 shared/ui/logo/index.tsx create mode 100644 shared/ui/logo/logo.module.scss diff --git a/shared/ui/logo/index.tsx b/shared/ui/logo/index.tsx new file mode 100644 index 00000000..cdda7f60 --- /dev/null +++ b/shared/ui/logo/index.tsx @@ -0,0 +1,28 @@ +import Link from 'next/link' + +import ImageLogo from '@/public/images/logo.svg' +import TextLogo from '@/public/images/text-logo.svg' +import classNames from 'classnames/bind' + +import { PATH } from '@/shared/constants/path' + +import styles from './logo.module.scss' + +const cx = classNames.bind(styles) + +interface Props { + href?: string + hasText?: boolean + className?: string +} + +const Logo = ({ href = PATH.HOME, hasText = false, className }: Props) => { + return ( + + + {hasText && } + + ) +} + +export default Logo diff --git a/shared/ui/logo/logo.module.scss b/shared/ui/logo/logo.module.scss new file mode 100644 index 00000000..e03cbdda --- /dev/null +++ b/shared/ui/logo/logo.module.scss @@ -0,0 +1,14 @@ +.container { + display: flex; + align-items: center; + gap: 12px; + + svg { + flex-shrink: 0; + } +} + +.text { + color: $color-gray-500; + line-height: normal; +} From 6bceba25cce27e8cbf9b7175255769d532c14669 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 01:11:54 +0900 Subject: [PATCH 020/498] =?UTF-8?q?feat:=20ButtonGroup=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/button/index.tsx | 17 +++++++++++++++-- shared/ui/link-button/link-button.stories.tsx | 5 +++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/shared/ui/button/index.tsx b/shared/ui/button/index.tsx index 5baf0728..0f39f29a 100644 --- a/shared/ui/button/index.tsx +++ b/shared/ui/button/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { ComponentProps } from 'react' +import { ComponentProps, ReactNode } from 'react' import classNames from 'classnames/bind' @@ -17,7 +17,7 @@ interface Props extends ComponentProps<'button'> { onClick: () => void } -export const Button = ({ +const _Button = ({ children, size = 'medium', variant = 'outline', @@ -35,3 +35,16 @@ export const Button = ({ {children} ) + +interface ButtonGruopProps { + gap?: string + children: ReactNode +} + +const ButtonGroup = ({ gap = '8px', children }: ButtonGruopProps) => { + return
    {children}
    +} + +_Button.ButtonGroup = ButtonGroup + +export { _Button as Button } diff --git a/shared/ui/link-button/link-button.stories.tsx b/shared/ui/link-button/link-button.stories.tsx index 0895d836..a0afe38c 100644 --- a/shared/ui/link-button/link-button.stories.tsx +++ b/shared/ui/link-button/link-button.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react' +import { Button } from '../button' import { LinkButton } from './index' const meta = { @@ -76,13 +77,13 @@ export const Filled: StoryType = { export const GroupExample: StoryType = { render: () => ( -
    + 로그인 회원가입 -
    + ), } From a01ceab5b13382b212b0d171cb861f1e4008ffad Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 01:13:41 +0900 Subject: [PATCH 021/498] =?UTF-8?q?feat:=20HeaderLinks=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/header/_ui/header-links/index.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 shared/ui/header/_ui/header-links/index.tsx diff --git a/shared/ui/header/_ui/header-links/index.tsx b/shared/ui/header/_ui/header-links/index.tsx new file mode 100644 index 00000000..9b53e1ca --- /dev/null +++ b/shared/ui/header/_ui/header-links/index.tsx @@ -0,0 +1,21 @@ +import { PATH } from '@/shared/constants/path' +import { Button } from '@/shared/ui/button' +import { LinkButton } from '@/shared/ui/link-button' + +const HeaderLinks = () => { + return ( + + + 대시보드 + + + 로그인 + + + 회원가입 + + + ) +} + +export default HeaderLinks From 6e98adb3532e1f29f4cd78b504e104b0a2166c09 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 01:19:22 +0900 Subject: [PATCH 022/498] =?UTF-8?q?feat:=20Header=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/header/header.module.scss | 9 +++++++++ shared/ui/header/index.tsx | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 shared/ui/header/header.module.scss create mode 100644 shared/ui/header/index.tsx diff --git a/shared/ui/header/header.module.scss b/shared/ui/header/header.module.scss new file mode 100644 index 00000000..0b610743 --- /dev/null +++ b/shared/ui/header/header.module.scss @@ -0,0 +1,9 @@ +.container { + height: 80px; + padding: 0 42px; + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(251, 251, 251, 0.5); + backdrop-filter: blur(60px); +} diff --git a/shared/ui/header/index.tsx b/shared/ui/header/index.tsx new file mode 100644 index 00000000..fdabb109 --- /dev/null +++ b/shared/ui/header/index.tsx @@ -0,0 +1,22 @@ +import classNames from 'classnames/bind' + +import Logo from '../logo' +import HeaderLinks from './_ui/header-links' +import styles from './header.module.scss' + +const cx = classNames.bind(styles) + +interface Props { + hasLinks?: boolean +} + +const Header = ({ hasLinks = false }: Props) => { + return ( +
    + + {hasLinks && } +
    + ) +} + +export default Header From 85d8af47467cc34a77e3e7e910c56d89e57d9240 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 01:57:31 +0900 Subject: [PATCH 023/498] =?UTF-8?q?refactor:=20Header=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/header/header.module.scss | 2 -- shared/ui/header/index.tsx | 16 +++++++++------- shared/ui/header/logo-header/index.tsx | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 shared/ui/header/logo-header/index.tsx diff --git a/shared/ui/header/header.module.scss b/shared/ui/header/header.module.scss index 0b610743..29ec90a9 100644 --- a/shared/ui/header/header.module.scss +++ b/shared/ui/header/header.module.scss @@ -4,6 +4,4 @@ display: flex; justify-content: space-between; align-items: center; - background: rgba(251, 251, 251, 0.5); - backdrop-filter: blur(60px); } diff --git a/shared/ui/header/index.tsx b/shared/ui/header/index.tsx index fdabb109..67d0068f 100644 --- a/shared/ui/header/index.tsx +++ b/shared/ui/header/index.tsx @@ -1,20 +1,22 @@ +import { CSSProperties, ReactNode } from 'react' + import classNames from 'classnames/bind' -import Logo from '../logo' -import HeaderLinks from './_ui/header-links' import styles from './header.module.scss' const cx = classNames.bind(styles) interface Props { - hasLinks?: boolean + Left?: ReactNode + Right?: ReactNode + styles?: CSSProperties } -const Header = ({ hasLinks = false }: Props) => { +const Header = ({ Left, Right }: Props) => { return ( -
    - - {hasLinks && } +
    + {Left} + {Right}
    ) } diff --git a/shared/ui/header/logo-header/index.tsx b/shared/ui/header/logo-header/index.tsx new file mode 100644 index 00000000..dc957a65 --- /dev/null +++ b/shared/ui/header/logo-header/index.tsx @@ -0,0 +1,25 @@ +import Header from '..' +import Logo from '../../logo' +import HeaderLinks from '../_ui/header-links' + +const headerStyles = { + background: 'rgba(251, 251, 251, 0.5)', + backdropFilter: 'blur(60px)', +} + +interface Props { + hasLinks?: boolean + hasText?: boolean +} + +const LogoHeader = ({ hasLinks = false, hasText = false }: Props) => { + return ( +
    } + Right={hasLinks && } + styles={headerStyles} + /> + ) +} + +export default LogoHeader From 1306d405c5305ea597aae8e11b97c545c940c7bf Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 02:39:18 +0900 Subject: [PATCH 024/498] =?UTF-8?q?feat:=20BackHeader=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back-header/back-header.module.scss | 13 +++++++ shared/ui/header/back-header/index.tsx | 37 +++++++++++++++++++ shared/ui/header/logo-header/index.tsx | 1 + 3 files changed, 51 insertions(+) create mode 100644 shared/ui/header/back-header/back-header.module.scss create mode 100644 shared/ui/header/back-header/index.tsx diff --git a/shared/ui/header/back-header/back-header.module.scss b/shared/ui/header/back-header/back-header.module.scss new file mode 100644 index 00000000..b05c0f39 --- /dev/null +++ b/shared/ui/header/back-header/back-header.module.scss @@ -0,0 +1,13 @@ +.container { + @include typo-c2; + display: flex; + align-items: center; + gap: 8px; + background-color: transparent; + color: $color-gray-500; + + svg { + width: 9px; + flex-shrink: 0; + } +} diff --git a/shared/ui/header/back-header/index.tsx b/shared/ui/header/back-header/index.tsx new file mode 100644 index 00000000..d0968701 --- /dev/null +++ b/shared/ui/header/back-header/index.tsx @@ -0,0 +1,37 @@ +'use client' + +// NOTE: 이름 공모전 개최 +import { useRouter } from 'next/navigation' + +import { ChevronLeftIcon } from '@/public/icons' +import classNames from 'classnames/bind' + +import Header from '..' +import styles from './back-header.module.scss' + +const cx = classNames.bind(styles) + +const headerStyles = { + background: 'rgba(251, 251, 251, 0.5)', + backdropFilter: 'blur(60px)', +} + +const BackHeader = () => { + return
    } styles={headerStyles} /> +} + +const Left = () => { + const router = useRouter() + const onButtonClick = () => { + router.back() + } + + return ( + + ) +} + +export default BackHeader diff --git a/shared/ui/header/logo-header/index.tsx b/shared/ui/header/logo-header/index.tsx index dc957a65..9848864b 100644 --- a/shared/ui/header/logo-header/index.tsx +++ b/shared/ui/header/logo-header/index.tsx @@ -1,3 +1,4 @@ +// NOTE: 이름 공모전 개최 import Header from '..' import Logo from '../../logo' import HeaderLinks from '../_ui/header-links' From 0fcafef13b238c6ca7c64fbdbfb99c0b8046707b Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 03:24:39 +0900 Subject: [PATCH 025/498] =?UTF-8?q?feat:=20BackHeader=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9E=91=EC=84=B1=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back-header/back-header.stories.tsx | 20 +++++++++++++++++++ shared/ui/header/back-header/index.tsx | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 shared/ui/header/back-header/back-header.stories.tsx diff --git a/shared/ui/header/back-header/back-header.stories.tsx b/shared/ui/header/back-header/back-header.stories.tsx new file mode 100644 index 00000000..cfe1ff5c --- /dev/null +++ b/shared/ui/header/back-header/back-header.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import BackHeader from '.' + +const meta = { + title: 'Components/Header/BackHeader', + component: BackHeader, + tags: ['autodocs'], +} satisfies Meta +export default meta +type StoryType = StoryObj + +export const Default: StoryType = { + render: () => , + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} diff --git a/shared/ui/header/back-header/index.tsx b/shared/ui/header/back-header/index.tsx index d0968701..2b1a5d94 100644 --- a/shared/ui/header/back-header/index.tsx +++ b/shared/ui/header/back-header/index.tsx @@ -1,6 +1,7 @@ 'use client' // NOTE: 이름 공모전 개최 +// TODO: 아이콘 바꿔야함 import { useRouter } from 'next/navigation' import { ChevronLeftIcon } from '@/public/icons' @@ -23,7 +24,7 @@ const BackHeader = () => { const Left = () => { const router = useRouter() const onButtonClick = () => { - router.back() + router?.back() } return ( From b74b5edaceb4d2858ca2e77d4374cf0ec3e007c0 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 04:03:00 +0900 Subject: [PATCH 026/498] =?UTF-8?q?feat:=20LogoHeader=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9E=91=EC=84=B1=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/images/index.ts | 2 ++ shared/ui/header/index.tsx | 2 +- shared/ui/header/logo-header/index.tsx | 3 +- .../logo-header/logo-header.stories.tsx | 32 +++++++++++++++++++ shared/ui/logo/index.tsx | 3 +- 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 public/images/index.ts create mode 100644 shared/ui/header/logo-header/logo-header.stories.tsx diff --git a/public/images/index.ts b/public/images/index.ts new file mode 100644 index 00000000..fc0dc062 --- /dev/null +++ b/public/images/index.ts @@ -0,0 +1,2 @@ +export { default as ImageLogo } from './logo.svg' +export { default as TextLogo } from './text-logo.svg' diff --git a/shared/ui/header/index.tsx b/shared/ui/header/index.tsx index 67d0068f..82f70bce 100644 --- a/shared/ui/header/index.tsx +++ b/shared/ui/header/index.tsx @@ -12,7 +12,7 @@ interface Props { styles?: CSSProperties } -const Header = ({ Left, Right }: Props) => { +const Header = ({ Left, Right, styles }: Props) => { return (
    {Left} diff --git a/shared/ui/header/logo-header/index.tsx b/shared/ui/header/logo-header/index.tsx index 9848864b..94a23ebc 100644 --- a/shared/ui/header/logo-header/index.tsx +++ b/shared/ui/header/logo-header/index.tsx @@ -1,6 +1,7 @@ // NOTE: 이름 공모전 개최 +import Logo from '@/shared/ui/logo' + import Header from '..' -import Logo from '../../logo' import HeaderLinks from '../_ui/header-links' const headerStyles = { diff --git a/shared/ui/header/logo-header/logo-header.stories.tsx b/shared/ui/header/logo-header/logo-header.stories.tsx new file mode 100644 index 00000000..0cbe7b57 --- /dev/null +++ b/shared/ui/header/logo-header/logo-header.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import LogoHeader from '.' + +const meta = { + title: 'Components/Header/LogoHeader', + component: LogoHeader, + tags: ['autodocs'], +} satisfies Meta +export default meta +type StoryType = StoryObj + +export const Default: StoryType = { + args: { + hasLinks: true, + hasText: false, + }, +} + +export const WithoutLogoText: StoryType = { + args: { + ...Default.args, + hasText: false, + }, +} + +export const WithoutLinks: StoryType = { + args: { + ...Default.args, + hasLinks: false, + }, +} diff --git a/shared/ui/logo/index.tsx b/shared/ui/logo/index.tsx index cdda7f60..81003307 100644 --- a/shared/ui/logo/index.tsx +++ b/shared/ui/logo/index.tsx @@ -1,7 +1,6 @@ import Link from 'next/link' -import ImageLogo from '@/public/images/logo.svg' -import TextLogo from '@/public/images/text-logo.svg' +import { ImageLogo, TextLogo } from '@/public/images' import classNames from 'classnames/bind' import { PATH } from '@/shared/constants/path' From 8e2cee942b1048f2deced41b8919c9fc238b7ec3 Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 10:43:31 +0900 Subject: [PATCH 027/498] =?UTF-8?q?feat:=20=EB=B3=84=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20margin,=20size=20large=20=EC=B6=94=EA=B0=80=20(#73)?= =?UTF-8?q?'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/total-star/star-icon.tsx | 3 +-- shared/ui/total-star/styles.module.scss | 13 +++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/shared/ui/total-star/star-icon.tsx b/shared/ui/total-star/star-icon.tsx index 78a4d64e..9b1f9a22 100644 --- a/shared/ui/total-star/star-icon.tsx +++ b/shared/ui/total-star/star-icon.tsx @@ -3,12 +3,11 @@ import { StarIcon } from '@/public/icons' import classNames from 'classnames/bind' -import { SizeType } from '@/shared/ui/total-star' - import styles from './styles.module.scss' const cx = classNames.bind(styles) +type SizeType = 'small' | 'medium' | 'large' interface Props { size?: SizeType } diff --git a/shared/ui/total-star/styles.module.scss b/shared/ui/total-star/styles.module.scss index 034e2209..09e01e30 100644 --- a/shared/ui/total-star/styles.module.scss +++ b/shared/ui/total-star/styles.module.scss @@ -23,12 +23,17 @@ } .icon-wrapper { display: block; + margin: -3px; &.small { - width: 22px; - height: 22px; + width: 20px; + height: 20px; } &.medium { - width: 28px; - height: 28px; + width: 26px; + height: 26px; + } + &.large { + width: 32px; + height: 32px; } } From 46c7a6ec9535a63a038b00df0ac51612d92a3be9 Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 10:44:22 +0900 Subject: [PATCH 028/498] =?UTF-8?q?feat:=20=EB=B3=84=EC=A0=90=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8,=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20=EC=B6=94=EA=B0=80=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategies/_ui/star-rating/index.tsx | 37 +++++++++++++++++++ .../_ui/star-rating/star-rating.stories.tsx | 24 ++++++++++++ .../_ui/star-rating/styles.module.scss | 13 +++++++ 3 files changed, 74 insertions(+) create mode 100644 app/(dashboard)/strategies/_ui/star-rating/index.tsx create mode 100644 app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx create mode 100644 app/(dashboard)/strategies/_ui/star-rating/styles.module.scss diff --git a/app/(dashboard)/strategies/_ui/star-rating/index.tsx b/app/(dashboard)/strategies/_ui/star-rating/index.tsx new file mode 100644 index 00000000..3115a387 --- /dev/null +++ b/app/(dashboard)/strategies/_ui/star-rating/index.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useState } from 'react' + +import classNames from 'classnames/bind' + +import Star from '@/shared/ui/total-star/star-icon' + +import styles from './styles.module.scss' + +const cx = classNames.bind(styles) + +interface Props { + starRating?: number +} + +const StarRating = ({ starRating }: Props) => { + const [clickedIdx, setClickedIdx] = useState(0) + + return ( +
    + {starRating + ? [...Array(Math.floor(starRating))].map((_, idx) => ) + : [...Array(5)].map((_, idx) => ( + + ))} +
    + ) +} + +export default StarRating diff --git a/app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx b/app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx new file mode 100644 index 00000000..e5eaa2b9 --- /dev/null +++ b/app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx @@ -0,0 +1,24 @@ +import StarRating from '@/app/(dashboard)/strategies/_ui/star-rating' +import type { Meta, StoryFn } from '@storybook/react' + +const meta: Meta = { + title: 'components/StarRating', + component: StarRating, + tags: ['autodocs'], +} + +const starRating: StoryFn<{ starRating: number | undefined }> = ({ starRating }) => ( + +) + +export const Rated = starRating.bind({}) +Rated.args = { + starRating: 5, +} + +export const Rating = starRating.bind({}) +Rating.args = { + starRating: undefined, +} + +export default meta diff --git a/app/(dashboard)/strategies/_ui/star-rating/styles.module.scss b/app/(dashboard)/strategies/_ui/star-rating/styles.module.scss new file mode 100644 index 00000000..d7747a7f --- /dev/null +++ b/app/(dashboard)/strategies/_ui/star-rating/styles.module.scss @@ -0,0 +1,13 @@ +.container { + display: flex; + padding: 0; + color: $color-yellow; + .click-star { + margin: -1px; + background-color: transparent; + color: #e3e3e3; + &.onColor { + color: $color-yellow; + } + } +} From 3fb6ef53a50db7841e5cfc4ad83b9e40e751153b Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 11:37:16 +0900 Subject: [PATCH 029/498] =?UTF-8?q?rename:=20=EC=A0=84=EB=9E=B5=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8F=B4=EB=8D=94=20=ED=95=98=EC=9C=84=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategies/{ => [strategyId]}/_ui/star-rating/index.tsx | 0 .../{ => [strategyId]}/_ui/star-rating/star-rating.stories.tsx | 2 +- .../{ => [strategyId]}/_ui/star-rating/styles.module.scss | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename app/(dashboard)/strategies/{ => [strategyId]}/_ui/star-rating/index.tsx (100%) rename app/(dashboard)/strategies/{ => [strategyId]}/_ui/star-rating/star-rating.stories.tsx (85%) rename app/(dashboard)/strategies/{ => [strategyId]}/_ui/star-rating/styles.module.scss (100%) diff --git a/app/(dashboard)/strategies/_ui/star-rating/index.tsx b/app/(dashboard)/strategies/[strategyId]/_ui/star-rating/index.tsx similarity index 100% rename from app/(dashboard)/strategies/_ui/star-rating/index.tsx rename to app/(dashboard)/strategies/[strategyId]/_ui/star-rating/index.tsx diff --git a/app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx b/app/(dashboard)/strategies/[strategyId]/_ui/star-rating/star-rating.stories.tsx similarity index 85% rename from app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx rename to app/(dashboard)/strategies/[strategyId]/_ui/star-rating/star-rating.stories.tsx index e5eaa2b9..688b6ca5 100644 --- a/app/(dashboard)/strategies/_ui/star-rating/star-rating.stories.tsx +++ b/app/(dashboard)/strategies/[strategyId]/_ui/star-rating/star-rating.stories.tsx @@ -1,4 +1,4 @@ -import StarRating from '@/app/(dashboard)/strategies/_ui/star-rating' +import StarRating from '@/app/(dashboard)/strategies/[strategyId]/_ui/star-rating' import type { Meta, StoryFn } from '@storybook/react' const meta: Meta = { diff --git a/app/(dashboard)/strategies/_ui/star-rating/styles.module.scss b/app/(dashboard)/strategies/[strategyId]/_ui/star-rating/styles.module.scss similarity index 100% rename from app/(dashboard)/strategies/_ui/star-rating/styles.module.scss rename to app/(dashboard)/strategies/[strategyId]/_ui/star-rating/styles.module.scss From c1fc07a55f5c99e7c6e6d45d41c628597227fac2 Mon Sep 17 00:00:00 2001 From: deun Date: Tue, 19 Nov 2024 13:34:39 +0900 Subject: [PATCH 030/498] =?UTF-8?q?design:=20landing=20layout=EC=9D=98=20?= =?UTF-8?q?=EA=B7=B8=EB=9D=BC=EB=8D=B0=EC=9D=B4=EC=85=98=20overflow=20hidd?= =?UTF-8?q?en=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20z-index=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#66)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/styles/global.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/styles/global.scss b/shared/styles/global.scss index b47aba00..fd350584 100644 --- a/shared/styles/global.scss +++ b/shared/styles/global.scss @@ -9,6 +9,7 @@ body { line-height: 132%; color: $color-black; background-color: $color-gray-100; + overflow-x: hidden; } input, @@ -39,6 +40,7 @@ textarea { &::before { content: ''; + z-index: zIndex(hidden); position: absolute; top: -160px; right: -130px; From 9b5ff46c3e6db8abf2b1f73895f864a8d557d02a Mon Sep 17 00:00:00 2001 From: deun Date: Tue, 19 Nov 2024 13:36:25 +0900 Subject: [PATCH 031/498] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5=20=ED=86=B5=ED=95=A9=20=ED=8F=89=EA=B7=A0=20=EC=A7=80?= =?UTF-8?q?=ED=91=9C=20=EC=B0=A8=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#66)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../average-metrics-chart.tsx | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 app/(landing)/(home)/_ui/average-metrics-container/average-metrics-chart.tsx diff --git a/app/(landing)/(home)/_ui/average-metrics-container/average-metrics-chart.tsx b/app/(landing)/(home)/_ui/average-metrics-container/average-metrics-chart.tsx new file mode 100644 index 00000000..b2b0b155 --- /dev/null +++ b/app/(landing)/(home)/_ui/average-metrics-container/average-metrics-chart.tsx @@ -0,0 +1,185 @@ +'use client' + +import dynamic from 'next/dynamic' + +import Highcharts from 'highcharts' +import mouseWheelZoom from 'highcharts/modules/mouse-wheel-zoom' + +mouseWheelZoom(Highcharts) + +const HighchartsReact = dynamic(() => import('highcharts-react-official'), { + ssr: false, +}) + +export interface AverageMetricsChartDataModel { + dates: string[] + averagePrice: number[] + topSmScore: number[] + topStrategyPrice: number[] +} + +interface Props { + data: AverageMetricsChartDataModel +} + +const AverageMetricsChart = ({ data }: Props) => { + const chartOptions: Highcharts.Options = { + chart: { + type: 'areaspline', + width: 1000, + height: 450, + backgroundColor: '#FFFFFF', + zooming: { + mouseWheel: { + enabled: true, + }, + type: 'x', + }, + }, + title: { text: undefined }, + xAxis: { + categories: data.dates, + labels: { enabled: false }, + gridLineWidth: 0, + tickLength: 0, + lineColor: '#E3E3E3', + startOnTick: true, + endOnTick: true, + tickmarkPlacement: 'on', + }, + yAxis: [ + { + title: { + text: '통합기준가', + style: { + color: '#797979', + fontSize: '10px', + }, + }, + labels: { + style: { + color: '#797979', + fontSize: '10px', + }, + }, + }, + { + title: { + text: '기준가', + style: { + color: '#797979', + fontSize: '10px', + }, + }, + opposite: true, + labels: { + style: { + color: '#797979', + fontSize: '10px', + }, + }, + }, + ], + legend: { + enabled: true, + align: 'left', + verticalAlign: 'top', + layout: 'vertical', + x: 70, + y: -10, + itemStyle: { + color: '#4D4D4D', + fontSize: '12px', + }, + floating: true, + backgroundColor: '#FFFFFF', + borderColor: '#A7A7A7', + borderRadius: 4, + borderWidth: 1, + padding: 16, + }, + tooltip: { + useHTML: true, + headerFormat: '
    {point.key}
    ', + pointFormat: '{point.y:.2f}', + footerFormat: '', + borderColor: '#ECECEC', + borderWidth: 1, + shadow: false, + backgroundColor: '#FFFFFF', + style: { + padding: '10px', + }, + }, + + plotOptions: { + series: { + animation: { + duration: 2000, + }, + marker: { + enabled: false, + }, + }, + areaspline: { + fillOpacity: 0.5, + lineWidth: 2, + marker: { + enabled: false, + }, + fillColor: { + linearGradient: { + x1: 0, + y1: 0, + x2: 0, + y2: 1, + }, + stops: [ + [0, '#FF4F1F'], + [1, '#FFFFFF'], + ], + }, + }, + spline: { + lineWidth: 2, + marker: { + enabled: false, + }, + }, + }, + series: [ + { + type: 'areaspline', + name: '평균', + data: data.averagePrice, + color: '#FF4F1F', + yAxis: 0, + stickyTracking: false, + pointPlacement: 'on', + }, + { + type: 'spline', + name: 'SM SCORE 1위', + data: data.topSmScore, + color: '#6877FF', + yAxis: 1, + stickyTracking: false, + pointPlacement: 'on', + }, + { + type: 'spline', + name: '구독 1위', + data: data.topStrategyPrice, + color: '#FFE070', + yAxis: 1, + stickyTracking: false, + pointPlacement: 'on', + }, + ], + credits: { enabled: false }, + } + + return +} + +export default AverageMetricsChart From abfdedf255393b9890d3a4860fb3ab7d53097cb0 Mon Sep 17 00:00:00 2001 From: deun Date: Tue, 19 Nov 2024 13:38:11 +0900 Subject: [PATCH 032/498] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5=20=ED=86=B5=ED=95=A9=20=ED=8F=89=EA=B7=A0=20=EC=A7=80?= =?UTF-8?q?=ED=91=9C=20=EC=B0=A8=ED=8A=B8=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#66)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_ui/average-metrics-container/index.tsx | 48 +++++++++++++++++++ .../style.module.scss | 33 +++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 app/(landing)/(home)/_ui/average-metrics-container/index.tsx create mode 100644 app/(landing)/(home)/_ui/average-metrics-container/style.module.scss diff --git a/app/(landing)/(home)/_ui/average-metrics-container/index.tsx b/app/(landing)/(home)/_ui/average-metrics-container/index.tsx new file mode 100644 index 00000000..89e04959 --- /dev/null +++ b/app/(landing)/(home)/_ui/average-metrics-container/index.tsx @@ -0,0 +1,48 @@ +import classNames from 'classnames/bind' + +import AverageMetricsChart, { AverageMetricsChartDataModel } from './average-metrics-chart' +import styles from './style.module.scss' + +const cx = classNames.bind(styles) + +const AverageMetricsContainer = () => { + // 임시 데이터 + const chartData: AverageMetricsChartDataModel = { + dates: [ + 'Jan 1, 2023', + 'Feb 1, 2023', + 'Mar 1, 2023', + 'Apr 1, 2023', + 'May 1, 2023', + 'Jun 1, 2023', + 'Jul 1, 2023', + 'Aug 1, 2023', + 'Sep 1, 2023', + 'Oct 1, 2023', + 'Nov 1, 2023', + 'Dec 1, 2023', + ], + averagePrice: [200, 220, 260, 310, 290, 340, 400, 380, 450, 530, 510, 580], + topSmScore: [240, 270, 320, 330, 350, 420, 450, 470, 550, 530, 600, 680], + topStrategyPrice: [350, 330, 380, 450, 430, 500, 580, 560, 630, 710, 690, 760], + } + + const startDate = chartData.dates[0] + const endDate = chartData.dates.at(-1) + + return ( + <> +
    +
    +
    + FROM {startDate}TO + {endDate} +
    + +
    +
    + + ) +} + +export default AverageMetricsContainer diff --git a/app/(landing)/(home)/_ui/average-metrics-container/style.module.scss b/app/(landing)/(home)/_ui/average-metrics-container/style.module.scss new file mode 100644 index 00000000..2cb5392c --- /dev/null +++ b/app/(landing)/(home)/_ui/average-metrics-container/style.module.scss @@ -0,0 +1,33 @@ +.container { + width: 100%; + max-width: 1260px; + margin: 0 auto; + padding: 48px 0 60px; + border-radius: 10px; + background-color: $color-white; +} + +.contents-wrapper { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + width: 100%; + max-width: 1000px; + margin: 0 auto; +} + +.date-wrapper { + align-self: flex-end; + padding-right: 24px; + margin-bottom: 20px; + @include typo-c1; + + .date { + padding: 2px 8px; + margin: 0 6px; + border-radius: 20px; + color: $color-gray-500; + background-color: $color-gray-200; + } +} From cf89ffb77982bee7fa02efd263007bb2d2f915b5 Mon Sep 17 00:00:00 2001 From: deun Date: Tue, 19 Nov 2024 13:38:54 +0900 Subject: [PATCH 033/498] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=EC=A0=84?= =?UTF-8?q?=EB=9E=B5=20=ED=86=B5=ED=95=A9=20=ED=8F=89=EA=B7=A0=20=EC=A7=80?= =?UTF-8?q?=ED=91=9C=20=EC=B0=A8=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=EC=97=90=20=EC=B6=94=EA=B0=80=20(#66)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../average-metrics.stories.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/(landing)/(home)/_ui/average-metrics-container/average-metrics.stories.tsx diff --git a/app/(landing)/(home)/_ui/average-metrics-container/average-metrics.stories.tsx b/app/(landing)/(home)/_ui/average-metrics-container/average-metrics.stories.tsx new file mode 100644 index 00000000..aa036746 --- /dev/null +++ b/app/(landing)/(home)/_ui/average-metrics-container/average-metrics.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AverageMetricsContainer from './index' + +const meta = { + title: 'Components/AverageMetricsChart', + component: AverageMetricsContainer, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta + +type StoryType = StoryObj + +export const Default: StoryType = {} + +export default meta From c6ca9948ef5ff34d3073edd307aeb02da73d6130 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 13:57:54 +0900 Subject: [PATCH 034/498] =?UTF-8?q?fix:=20Back=20Header=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20label=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/header/back-header/index.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/shared/ui/header/back-header/index.tsx b/shared/ui/header/back-header/index.tsx index 2b1a5d94..5e72f18a 100644 --- a/shared/ui/header/back-header/index.tsx +++ b/shared/ui/header/back-header/index.tsx @@ -17,20 +17,24 @@ const headerStyles = { backdropFilter: 'blur(60px)', } -const BackHeader = () => { - return
    } styles={headerStyles} /> +interface Props { + label: string } -const Left = () => { +const BackHeader = ({ label }: Props) => { + return
    } styles={headerStyles} /> +} + +const Left = ({ label }: Props) => { const router = useRouter() - const onButtonClick = () => { - router?.back() + const onClick = () => { + router.back() } return ( - ) } From 659b2c425360ce79e96d08826c6ab2b890686cce Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 13:59:30 +0900 Subject: [PATCH 035/498] =?UTF-8?q?fix:=20Header=EA=B0=80=20=EC=9A=94?= =?UTF-8?q?=EC=86=8C=EC=9D=98=20=EC=83=81=EB=8B=A8=EC=97=90=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20header-height=20=EB=B3=80=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/styles/base/_variables.scss | 3 +++ shared/ui/header/header.module.scss | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/shared/styles/base/_variables.scss b/shared/styles/base/_variables.scss index 20d1dfc1..bb73983c 100644 --- a/shared/styles/base/_variables.scss +++ b/shared/styles/base/_variables.scss @@ -68,3 +68,6 @@ $z-index: ( /* Width */ $max-width: 1440px; $navigation-width: 90px; + +// height +$header-height: 80px; diff --git a/shared/ui/header/header.module.scss b/shared/ui/header/header.module.scss index 29ec90a9..32b8ddc8 100644 --- a/shared/ui/header/header.module.scss +++ b/shared/ui/header/header.module.scss @@ -1,5 +1,8 @@ .container { - height: 80px; + position: sticky; + top: 0; + height: $header-height; + width: 100%; padding: 0 42px; display: flex; justify-content: space-between; From d4feb295210c47abf70855c117a347c34e97636f Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 14:01:19 +0900 Subject: [PATCH 036/498] =?UTF-8?q?fix:=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=EC=97=90=EC=84=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=B4=20header=EA=B0=80=20=EC=83=81?= =?UTF-8?q?=EB=8B=A8=EC=97=90=20=EA=B3=A0=EC=A0=95=EB=90=A8=EC=9D=84=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20decorator=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back-header/back-header.stories.tsx | 22 ++++++++++++++++++- .../logo-header/logo-header.stories.tsx | 22 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/shared/ui/header/back-header/back-header.stories.tsx b/shared/ui/header/back-header/back-header.stories.tsx index cfe1ff5c..20ad48c9 100644 --- a/shared/ui/header/back-header/back-header.stories.tsx +++ b/shared/ui/header/back-header/back-header.stories.tsx @@ -5,13 +5,33 @@ import BackHeader from '.' const meta = { title: 'Components/Header/BackHeader', component: BackHeader, + decorators: [ + (Story) => ( +
    + +
    + {Array.from({ length: 5 }, (_, idx) => ( +
    + Scrollable Content {idx + 1} +
    + ))} +
    +
    + ), + ], tags: ['autodocs'], } satisfies Meta export default meta type StoryType = StoryObj export const Default: StoryType = { - render: () => , + render: () => , parameters: { nextjs: { appDirectory: true, diff --git a/shared/ui/header/logo-header/logo-header.stories.tsx b/shared/ui/header/logo-header/logo-header.stories.tsx index 0cbe7b57..95b887a6 100644 --- a/shared/ui/header/logo-header/logo-header.stories.tsx +++ b/shared/ui/header/logo-header/logo-header.stories.tsx @@ -5,6 +5,26 @@ import LogoHeader from '.' const meta = { title: 'Components/Header/LogoHeader', component: LogoHeader, + decorators: [ + (Story) => ( +
    + +
    + {Array.from({ length: 5 }, (_, idx) => ( +
    + Scrollable Content {idx + 1} +
    + ))} +
    +
    + ), + ], tags: ['autodocs'], } satisfies Meta export default meta @@ -13,7 +33,7 @@ type StoryType = StoryObj export const Default: StoryType = { args: { hasLinks: true, - hasText: false, + hasText: true, }, } From 4e44c74f07e230598f1ff6f49c6df8976ad13f11 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 15:22:04 +0900 Subject: [PATCH 037/498] =?UTF-8?q?fix:=20header=20styles=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EC=88=98=EC=A0=95=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/header/back-header/index.tsx | 2 +- .../back-header/{back-header.module.scss => styles.module.scss} | 0 shared/ui/header/index.tsx | 2 +- shared/ui/header/{header.module.scss => styles.module.scss} | 0 shared/ui/logo/index.tsx | 2 +- shared/ui/logo/{logo.module.scss => styles.module.scss} | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename shared/ui/header/back-header/{back-header.module.scss => styles.module.scss} (100%) rename shared/ui/header/{header.module.scss => styles.module.scss} (100%) rename shared/ui/logo/{logo.module.scss => styles.module.scss} (100%) diff --git a/shared/ui/header/back-header/index.tsx b/shared/ui/header/back-header/index.tsx index 5e72f18a..620cfbbd 100644 --- a/shared/ui/header/back-header/index.tsx +++ b/shared/ui/header/back-header/index.tsx @@ -8,7 +8,7 @@ import { ChevronLeftIcon } from '@/public/icons' import classNames from 'classnames/bind' import Header from '..' -import styles from './back-header.module.scss' +import styles from './styles.module.scss' const cx = classNames.bind(styles) diff --git a/shared/ui/header/back-header/back-header.module.scss b/shared/ui/header/back-header/styles.module.scss similarity index 100% rename from shared/ui/header/back-header/back-header.module.scss rename to shared/ui/header/back-header/styles.module.scss diff --git a/shared/ui/header/index.tsx b/shared/ui/header/index.tsx index 82f70bce..0428d2bb 100644 --- a/shared/ui/header/index.tsx +++ b/shared/ui/header/index.tsx @@ -2,7 +2,7 @@ import { CSSProperties, ReactNode } from 'react' import classNames from 'classnames/bind' -import styles from './header.module.scss' +import styles from './styles.module.scss' const cx = classNames.bind(styles) diff --git a/shared/ui/header/header.module.scss b/shared/ui/header/styles.module.scss similarity index 100% rename from shared/ui/header/header.module.scss rename to shared/ui/header/styles.module.scss diff --git a/shared/ui/logo/index.tsx b/shared/ui/logo/index.tsx index 81003307..00062944 100644 --- a/shared/ui/logo/index.tsx +++ b/shared/ui/logo/index.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames/bind' import { PATH } from '@/shared/constants/path' -import styles from './logo.module.scss' +import styles from './styles.module.scss' const cx = classNames.bind(styles) diff --git a/shared/ui/logo/logo.module.scss b/shared/ui/logo/styles.module.scss similarity index 100% rename from shared/ui/logo/logo.module.scss rename to shared/ui/logo/styles.module.scss From dc5f410f3cbaac7e6a7ebea7b9246cb81f76a115 Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 15:32:23 +0900 Subject: [PATCH 038/498] =?UTF-8?q?fix:=20dropdown=20styles=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EC=88=98=EC=A0=95=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/dropdown/index.tsx | 2 +- shared/ui/dropdown/option/range-option.tsx | 2 +- shared/ui/dropdown/{dropdown.module.scss => styles.module.scss} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename shared/ui/dropdown/{dropdown.module.scss => styles.module.scss} (100%) diff --git a/shared/ui/dropdown/index.tsx b/shared/ui/dropdown/index.tsx index a042f73e..e4be7162 100644 --- a/shared/ui/dropdown/index.tsx +++ b/shared/ui/dropdown/index.tsx @@ -5,10 +5,10 @@ import { CSSProperties, ReactNode, createContext } from 'react' import { CheckboxIcon, ChevronDownIcon, ChevronUpIcon } from '@/public/icons' import classNames from 'classnames/bind' -import styles from './dropdown.module.scss' import { useDropdown } from './hooks/use-dropdown' import { useDropdownContext } from './hooks/use-dropdown-context' import RangeOption from './option/range-option' +import styles from './styles.module.scss' const cx = classNames.bind(styles) diff --git a/shared/ui/dropdown/option/range-option.tsx b/shared/ui/dropdown/option/range-option.tsx index 04e1cd12..8b86cd5d 100644 --- a/shared/ui/dropdown/option/range-option.tsx +++ b/shared/ui/dropdown/option/range-option.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' // import { DropdownContext } from '..' -import styles from '../dropdown.module.scss' +import styles from '../styles.module.scss' const RangeOption = ({ onRangeChange }: { onRangeChange: (min: string, max: string) => void }) => { const [range, setRange] = useState({ min: '', max: '' }) diff --git a/shared/ui/dropdown/dropdown.module.scss b/shared/ui/dropdown/styles.module.scss similarity index 100% rename from shared/ui/dropdown/dropdown.module.scss rename to shared/ui/dropdown/styles.module.scss From e2ec427ba387c3e87ed58c89bde70a6da6f09c54 Mon Sep 17 00:00:00 2001 From: deun Date: Tue, 19 Nov 2024 15:39:13 +0900 Subject: [PATCH 039/498] =?UTF-8?q?rename:=20styles.module.scss=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20(#66)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(landing)/(home)/_ui/average-metrics-container/index.tsx | 2 +- .../{style.module.scss => styles.module.scss} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/(landing)/(home)/_ui/average-metrics-container/{style.module.scss => styles.module.scss} (100%) diff --git a/app/(landing)/(home)/_ui/average-metrics-container/index.tsx b/app/(landing)/(home)/_ui/average-metrics-container/index.tsx index 89e04959..3f221b29 100644 --- a/app/(landing)/(home)/_ui/average-metrics-container/index.tsx +++ b/app/(landing)/(home)/_ui/average-metrics-container/index.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames/bind' import AverageMetricsChart, { AverageMetricsChartDataModel } from './average-metrics-chart' -import styles from './style.module.scss' +import styles from './styles.module.scss' const cx = classNames.bind(styles) diff --git a/app/(landing)/(home)/_ui/average-metrics-container/style.module.scss b/app/(landing)/(home)/_ui/average-metrics-container/styles.module.scss similarity index 100% rename from app/(landing)/(home)/_ui/average-metrics-container/style.module.scss rename to app/(landing)/(home)/_ui/average-metrics-container/styles.module.scss From 2b4e5d1e463682de197c5555e204832cd16f0eeb Mon Sep 17 00:00:00 2001 From: hoyoen Date: Tue, 19 Nov 2024 16:21:13 +0900 Subject: [PATCH 040/498] =?UTF-8?q?fix:=20props=EB=AA=85=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EA=B5=90=EC=A0=95=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/ui/button/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/ui/button/index.tsx b/shared/ui/button/index.tsx index 0f39f29a..7075e6c2 100644 --- a/shared/ui/button/index.tsx +++ b/shared/ui/button/index.tsx @@ -36,12 +36,12 @@ const _Button = ({ ) -interface ButtonGruopProps { +interface ButtonGroupProps { gap?: string children: ReactNode } -const ButtonGroup = ({ gap = '8px', children }: ButtonGruopProps) => { +const ButtonGroup = ({ gap = '8px', children }: ButtonGroupProps) => { return
    {children}
    } From 8937899a1dc8c878af567e2f42dbf55184f2e654 Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 18:11:10 +0900 Subject: [PATCH 041/498] =?UTF-8?q?feat:=20svg=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B6=94=EA=B0=80=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/close.svg | 3 +++ public/icons/index.tsx | 1 + public/icons/open.svg | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 public/icons/close.svg diff --git a/public/icons/close.svg b/public/icons/close.svg new file mode 100644 index 00000000..0c4360d2 --- /dev/null +++ b/public/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/index.tsx b/public/icons/index.tsx index aeb424f9..0c4cc750 100644 --- a/public/icons/index.tsx +++ b/public/icons/index.tsx @@ -15,6 +15,7 @@ export { default as MoneyIcon } from './money.svg' export { default as MonthlyGraphIcon } from './monthly-graph.svg' export { default as NoticeIcon } from './notice.svg' export { default as OpenIcon } from './open.svg' +export { default as CloseIcon } from './close.svg' export { default as PencilIcon } from './pencil.svg' export { default as ProfileIcon } from './profile.svg' export { default as QuestionIcon } from './question.svg' diff --git a/public/icons/open.svg b/public/icons/open.svg index 1e60bbe1..1160a20b 100644 --- a/public/icons/open.svg +++ b/public/icons/open.svg @@ -1,3 +1,3 @@ - - + + From 639df56a96f81282fbb37986aa13f19750bfa8df Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 18:12:58 +0900 Subject: [PATCH 042/498] =?UTF-8?q?feat:=20=EC=A0=84=EB=9E=B5=20=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8,=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=B6=94=EA=B0=80=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[strategyId]/_ui/introduction/index.tsx | 58 +++++++++++++++++++ .../_ui/introduction/introduction.stories.tsx | 27 +++++++++ .../_ui/introduction/styles.module.scss | 45 ++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx create mode 100644 app/(dashboard)/strategies/[strategyId]/_ui/introduction/introduction.stories.tsx create mode 100644 app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss diff --git a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx new file mode 100644 index 00000000..6df8bea7 --- /dev/null +++ b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +import { CloseIcon, OpenIcon } from '@/public/icons' +import classNames from 'classnames/bind' + +import styles from './styles.module.scss' + +const cx = classNames.bind(styles) + +interface Props { + content: string +} + +const StrategyIntroduction = ({ content }: Props) => { + const [shouldShowMore, setShouldShowMore] = useState(false) + const [isContentOverflow, setIsContentOverflow] = useState(false) + const contentRef = useRef(null) + + useEffect(() => { + checkOverflow() + }, [content]) + + const checkOverflow = () => { + if (contentRef.current) { + setIsContentOverflow(contentRef.current.scrollHeight > contentRef.current.offsetHeight) + } + } + + return ( +
    +

    전략 상세 소개

    +
    +

    {content}

    +
    + {isContentOverflow && ( +
    + +
    + )} +
    + ) +} + +export default StrategyIntroduction diff --git a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/introduction.stories.tsx b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/introduction.stories.tsx new file mode 100644 index 00000000..14386fef --- /dev/null +++ b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/introduction.stories.tsx @@ -0,0 +1,27 @@ +import StrategyIntroduction from '@/app/(dashboard)/strategies/[strategyId]/_ui/introduction' +import { Meta, StoryFn } from '@storybook/react' + +const meta: Meta = { + title: 'components/StrategyIntroduction', + component: StrategyIntroduction, + tags: ['autodocs'], +} + +export default meta + +const introduction: StoryFn<{ content: string }> = ({ content }) => ( +
    + +
    +) + +export const Default = introduction.bind({}) +Default.args = { + content: '안녕하세요. 전랙에 대한 설명입니다.', +} + +export const MaxContent = introduction.bind({}) +MaxContent.args = { + content: + '전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 안녕하세요. 안녕하세요. 안녕하세요..전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 전략에 대한 상세한 설명을 입력해주세요. 안녕하세요. 안녕하세요.', +} diff --git a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss new file mode 100644 index 00000000..8bf35364 --- /dev/null +++ b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss @@ -0,0 +1,45 @@ +.container { + width: 100%; + background-color: $color-white; + border-radius: 5px; + padding: 20px; + .title { + @include typo-b2; + margin-bottom: 15px; + } + + .content { + width: 100%; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + line-clamp: 4; + -webkit-line-clamp: 4; + &.expand { + display: contents; + } + p { + @include typo-c1; + line-height: 18px; + color: $color-gray-600; + } + } + + .button-wrapper { + width: 100%; + display: flex; + justify-content: flex-end; + margin-top: 10px; + button { + padding: 2px; + color: $color-gray-500; + background-color: transparent; + svg { + margin-left: 10px; + } + span { + @include typo-c1; + } + } + } +} From d49625d3be512dfab0b3e09974d827f8a1c206ec Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 18:54:58 +0900 Subject: [PATCH 043/498] =?UTF-8?q?feat:=20svg=EC=95=84=EC=9D=B4=EC=BD=98,?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20=EC=B6=94=EA=B0=80=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategies/[strategyId]/_ui/introduction/index.tsx | 8 ++++---- .../[strategyId]/_ui/introduction/styles.module.scss | 9 ++++----- public/icons/close.svg | 4 ++-- public/icons/open.svg | 4 ++-- shared/styles/stories/icons.stories.tsx | 2 ++ 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx index 6df8bea7..239ab22f 100644 --- a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx +++ b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx @@ -38,15 +38,15 @@ const StrategyIntroduction = ({ content }: Props) => {
    diff --git a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss index 8bf35364..c652a5c3 100644 --- a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss +++ b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/styles.module.scss @@ -31,14 +31,13 @@ justify-content: flex-end; margin-top: 10px; button { - padding: 2px; + @include typo-c1; + display: flex; + align-items: center; color: $color-gray-500; background-color: transparent; svg { - margin-left: 10px; - } - span { - @include typo-c1; + margin: -3px 0 0 7px; } } } diff --git a/public/icons/close.svg b/public/icons/close.svg index 0c4360d2..40d24485 100644 --- a/public/icons/close.svg +++ b/public/icons/close.svg @@ -1,3 +1,3 @@ - - + + diff --git a/public/icons/open.svg b/public/icons/open.svg index 1160a20b..850afa8d 100644 --- a/public/icons/open.svg +++ b/public/icons/open.svg @@ -1,3 +1,3 @@ - - + + diff --git a/shared/styles/stories/icons.stories.tsx b/shared/styles/stories/icons.stories.tsx index 8e9d5a93..cc6dc7bc 100644 --- a/shared/styles/stories/icons.stories.tsx +++ b/shared/styles/stories/icons.stories.tsx @@ -10,6 +10,7 @@ import { ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, + CloseIcon, DailyGraphIcon, FileIcon, MoneyIcon, @@ -51,6 +52,7 @@ const icons = [ { name: 'MonthlyGraphIcon', icon: MonthlyGraphIcon }, { name: 'NoticeIcon', icon: NoticeIcon }, { name: 'OpenIcon', icon: OpenIcon }, + { name: 'CloseIcon', icon: CloseIcon }, { name: 'PencilIcon', icon: PencilIcon }, { name: 'ProfileIcon', icon: ProfileIcon }, { name: 'QuestionIcon', icon: QuestionIcon }, From 0c2483924ad2308f042cc0c4959263f8c0984f2a Mon Sep 17 00:00:00 2001 From: ssumanlife Date: Tue, 19 Nov 2024 18:59:48 +0900 Subject: [PATCH 044/498] =?UTF-8?q?rename:=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategies/[strategyId]/_ui/introduction/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx index 239ab22f..b9ee51cd 100644 --- a/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx +++ b/app/(dashboard)/strategies/[strategyId]/_ui/introduction/index.tsx @@ -15,7 +15,7 @@ interface Props { const StrategyIntroduction = ({ content }: Props) => { const [shouldShowMore, setShouldShowMore] = useState(false) - const [isContentOverflow, setIsContentOverflow] = useState(false) + const [isOverflow, setIsOverflow] = useState(false) const contentRef = useRef(null) useEffect(() => { @@ -24,7 +24,7 @@ const StrategyIntroduction = ({ content }: Props) => { const checkOverflow = () => { if (contentRef.current) { - setIsContentOverflow(contentRef.current.scrollHeight > contentRef.current.offsetHeight) + setIsOverflow(contentRef.current.scrollHeight > contentRef.current.offsetHeight) } } @@ -34,7 +34,7 @@ const StrategyIntroduction = ({ content }: Props) => {

    {content}

    - {isContentOverflow && ( + {isOverflow && (
    +
    + + +
    +
    +

    + {errors} +

    +
    +
    + +
    + 아이디 찾기 + | + 비밀번호 찾기 +
    +
    + + + +
    +
    + ) } export default SignInPage diff --git a/app/(landing)/signin/styles.module.scss b/app/(landing)/signin/styles.module.scss new file mode 100644 index 00000000..270b03f5 --- /dev/null +++ b/app/(landing)/signin/styles.module.scss @@ -0,0 +1,99 @@ +.container { + display: flex; + justify-content: center; + align-items: center; +} + +.loginBox { + max-width: 400px; +} + +.title { + @include typo-h2; + margin-bottom: 70px; +} + +.form { + display: flex; + flex-direction: column; + gap: 24px; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 9px; + + label { + @include typo-b2; + } +} + +.input { + border-radius: 2px; + @include typo-b3; + &::placeholder { + color: $color-gray-400; + } + + &:required { + border-color: $color-gray-500; + } + + &:focus { + outline: none; + } +} + +.error-container { + min-height: 24px; + margin-top: -18px; + + .error-message { + @include typo-c1; + color: $color-orange-700; + + &.visible { + opacity: 1; + } + + &.hidden { + opacity: 0; + } + } +} + +.options { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + margin-top: -25px; +} + +.remember { + display: flex; + align-items: center; + cursor: pointer; + gap: 8px; +} + +.links { + display: flex; + align-items: center; + gap: 8px; + + a { + color: $color-gray-600; + @include typo-c1; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.divider { + color: #ddd; +} From 40c0c3d794f5f0ab639fd72b4f7f281206234bea Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:21:22 +0900 Subject: [PATCH 122/498] =?UTF-8?q?feat:=20MSW=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EC=9D=B8=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?API=20=EB=AA=A8=ED=82=B9=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mocks/handlers.ts | 4 +- mocks/handlers/auth.ts | 87 +++++++++++++++++++++++++++++++++++++++++ mocks/handlers/index.ts | 1 + 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 mocks/handlers/auth.ts diff --git a/mocks/handlers.ts b/mocks/handlers.ts index 4af399c8..ff6c12bb 100644 --- a/mocks/handlers.ts +++ b/mocks/handlers.ts @@ -1,5 +1,5 @@ -import { postHandlers, userHandlers } from './handlers/' +import { authHandlers, postHandlers, userHandlers } from './handlers/' -const handlers = [...userHandlers, ...postHandlers] +const handlers = [...userHandlers, ...postHandlers, ...authHandlers] export { handlers } diff --git a/mocks/handlers/auth.ts b/mocks/handlers/auth.ts new file mode 100644 index 00000000..bdc1d7e8 --- /dev/null +++ b/mocks/handlers/auth.ts @@ -0,0 +1,87 @@ +import { jwtDecode } from 'jwt-decode' +import { HttpResponse, http } from 'msw' + +import { ERROR_MESSAGES } from '@/shared/constants/error-messages' +import { LoginFormDataModel, TokenPayloadModel, UserModel } from '@/shared/types/auth' + +const MOCK_USERS: Record = { + 'test@example.com': { + id: '1', + email: 'test@example.com', + name: 'user1', + role: 'user', + createdAt: '2024-01-01T00:00:00Z', + }, + 'admin@example.com': { + id: '2', + email: 'admin@example.com', + name: 'user2', + role: 'admin', + createdAt: '2024-01-01T00:00:00Z', + }, +} + +const createToken = (user: UserModel, isAdmin: boolean) => { + const now = Math.floor(Date.now() / 1000) + const exp = now + (isAdmin ? 1800 : 7 * 24 * 60 * 60) + return `mock_${btoa(JSON.stringify({ user, exp, iat: now }))}` +} + +export const authHandlers = [ + http.post('/api/users/login', async ({ request }) => { + const { email, password } = (await request.json()) as LoginFormDataModel + const user = MOCK_USERS[email] + + if (user && password === 'password123') { + const isAdmin = user.role === 'admin' + const accessToken = createToken(user, isAdmin) + const refreshToken = createToken(user, isAdmin) + + return HttpResponse.json({ + isSuccess: true, + message: '로그인 성공', + data: { accessToken, refreshToken, user }, + }) + } + + return HttpResponse.json( + { + isSuccess: false, + message: ERROR_MESSAGES.AUTH.INVALID_CREDENTIALS, + code: 'INVALID_CREDENTIALS', + }, + { status: 400 } + ) + }), + + http.post('/api/users/reissue/refreshtoken', async ({ request }) => { + const authHeader = request.headers.get('Authorization') + const refreshToken = authHeader?.replace('Bearer ', '') + + if (!refreshToken) { + return HttpResponse.json( + { isSuccess: false, message: ERROR_MESSAGES.AUTH.INVALID_TOKEN }, + { status: 401 } + ) + } + + try { + const decoded = jwtDecode(refreshToken) + const isAdmin = decoded.user.role === 'admin' + + const accessToken = createToken(decoded.user, isAdmin) + const newRefreshToken = createToken(decoded.user, isAdmin) + + return HttpResponse.json({ + isSuccess: true, + message: '토큰 갱신 성공', + data: { accessToken, refreshToken: newRefreshToken }, + }) + } catch { + return HttpResponse.json( + { isSuccess: false, message: ERROR_MESSAGES.AUTH.REFRESH_FAILED }, + { status: 401 } + ) + } + }), +] diff --git a/mocks/handlers/index.ts b/mocks/handlers/index.ts index 3f70ad6f..a3042340 100644 --- a/mocks/handlers/index.ts +++ b/mocks/handlers/index.ts @@ -1,2 +1,3 @@ export { postHandlers } from './post' export { userHandlers } from './user' +export { authHandlers } from './auth' From 6cea8680ff92d8b953d5e21b927da8fa0a096747 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:24:06 +0900 Subject: [PATCH 123/498] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=93=B1=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/api/auth.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 shared/api/auth.ts diff --git a/shared/api/auth.ts b/shared/api/auth.ts new file mode 100644 index 00000000..341a216f --- /dev/null +++ b/shared/api/auth.ts @@ -0,0 +1,48 @@ +import { useMutation } from '@tanstack/react-query' + +import { + removeAccessToken, + removeRefreshToken, + setAccessToken, + setRefreshToken, +} from '@/shared/lib/auth-tokens' +import { useAuthStore } from '@/shared/stores/use-auth-store' +import type { LoginFormDataModel, LoginResponseType } from '@/shared/types/auth' + +import axiosInstance from './axios' + +export const login = async (credentials: LoginFormDataModel): Promise => { + const response = await axiosInstance.post('/api/users/login', credentials) + const { user, accessToken, refreshToken } = response.data.data + + setAccessToken(accessToken) + setRefreshToken(refreshToken) + + useAuthStore.getState().setAuthState({ + isAuthenticated: true, + user, + isLoggedOut: false, + }) + + return response.data +} + +export const logout = () => { + if (typeof window !== 'undefined') { + removeAccessToken() + removeRefreshToken() + + useAuthStore.getState().setAuthState({ + isAuthenticated: false, + user: null, + isKeepLoggedIn: false, + isLoggedOut: true, + }) + } +} + +export const useLogin = () => { + return useMutation({ + mutationFn: login, + }) +} From 69d0fac2362bf1d2a8f5746595cf7785ada3d0ae Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:27:13 +0900 Subject: [PATCH 124/498] =?UTF-8?q?feat:=20Axios=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=20=EB=B0=8F=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#78)=20-?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EA=B8=B0=EB=B0=98=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EA=B3=BC=20=EC=9E=90=EB=8F=99=20=EA=B0=B1=EC=8B=A0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=B4=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/api/axios.ts | 86 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 shared/api/axios.ts diff --git a/shared/api/axios.ts b/shared/api/axios.ts new file mode 100644 index 00000000..c5ca04e5 --- /dev/null +++ b/shared/api/axios.ts @@ -0,0 +1,86 @@ +import axios from 'axios' +import { AxiosError, InternalAxiosRequestConfig } from 'axios' + +import { getAccessToken } from '@/shared/lib/auth-tokens' +import { useAuthStore } from '@/shared/stores/use-auth-store' +import { getUserFromToken, isTokenExpired, refreshToken } from '@/shared/utils/token-utils' + +import { logout } from './auth' + +export const createAxiosInstance = (options: { withInterceptors?: boolean } = {}) => { + const instance = axios.create() + + if (options.withInterceptors && typeof window !== 'undefined') { + instance.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + const { isLoggedOut } = useAuthStore.getState() + const accessToken = getAccessToken() + + if (isLoggedOut || !accessToken) { + return config + } + + try { + const user = getUserFromToken(accessToken) + const isAdmin = user?.role === 'admin' + + if (isAdmin && isTokenExpired(accessToken)) { + handleLogout() + return config + } + + config.headers.Authorization = `Bearer ${accessToken}` + } catch (error) { + console.error('토큰 디코딩 실패:', error) + handleLogout() + } + + return config + }, + (error) => Promise.reject(error) + ) + + instance.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + if (!error.config) return Promise.reject(error) + + const originalRequest = error.config + const { isLoggedOut } = useAuthStore.getState() + + if (isLoggedOut) { + return Promise.reject(error) + } + + if (error.response?.status === 401) { + try { + const newToken = await refreshToken() + if (newToken) { + originalRequest.headers.Authorization = `Bearer ${newToken}` + return instance(originalRequest) + } + } catch (refreshError) { + console.error('Token refresh failed:', refreshError) + } + + handleLogout() + } + + return Promise.reject(error) + } + ) + } + + return instance +} + +const handleLogout = () => { + if (typeof window !== 'undefined') { + sessionStorage.removeItem('sessionToken') + logout() + } +} + +const axiosInstance = createAxiosInstance({ withInterceptors: true }) + +export default axiosInstance From ce22887dd1172c5b972d6b2664ca771c4e0cf30d Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:27:48 +0900 Subject: [PATCH 125/498] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=84=B8=EB=B6=84=ED=99=94=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/constants/error-messages.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/shared/constants/error-messages.ts b/shared/constants/error-messages.ts index 494a61c4..37841955 100644 --- a/shared/constants/error-messages.ts +++ b/shared/constants/error-messages.ts @@ -1,6 +1,19 @@ export const ERROR_MESSAGES = { - EMAIL: '이메일 형식이 잘못되었습니다.', - PASSWORD: '비밀번호는 8자리 이상, 문자와 숫자를 포함해야 합니다.', - PHONE: '전화번호는 10자리 이상이어야 합니다.', - REQUIRED: '필수 입력란입니다.', + AUTH: { + INVALID_CREDENTIALS: '이메일 또는 비밀번호가 올바르지 않습니다.', + SESSION_EXPIRED: '로그인이 만료되었습니다. 다시 로그인해주세요.', + LOGIN_FAILED: '서버 오류로 로그인에 실패했습니다.', + INVALID_TOKEN: '유효하지 않은 인증입니다.', + ALREADY_LOGGED_IN: '이미 로그인되어 있습니다.', + REFRESH_FAILED: '토큰 갱신에 실패했습니다.', + }, + FORM: { + REQUIRED_FIELDS: '이메일과 비밀번호를 입력해주세요.', + EMAIL: '이메일 형식이 올바르지 않습니다.', + PASSWORD: '비밀번호는 8자 이상, 영문과 숫자를 포함해야 합니다.', + PHONE: '휴대폰 번호 형식이 올바르지 않습니다.', + }, + NETWORK: { + ERROR: '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + }, } as const From f54c7930b5791ef402ff124db219a1ccfcfae57d Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:34:15 +0900 Subject: [PATCH 126/498] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EA=B4=80=EB=A6=AC=EC=99=80=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=B6=94=EA=B0=80=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/hooks/custom/use-auth.ts | 122 ++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 shared/hooks/custom/use-auth.ts diff --git a/shared/hooks/custom/use-auth.ts b/shared/hooks/custom/use-auth.ts new file mode 100644 index 00000000..ce2c1563 --- /dev/null +++ b/shared/hooks/custom/use-auth.ts @@ -0,0 +1,122 @@ +import { useEffect } from 'react' + +import { useRouter } from 'next/navigation' + +import { useQuery } from '@tanstack/react-query' + +import { logout } from '@/shared/api/auth' +import { PATH } from '@/shared/constants/path' +import { getAccessToken, getRefreshToken } from '@/shared/lib/auth-tokens' +import { useAuthStore } from '@/shared/stores/use-auth-store' +import { getTimeUntilExpiry, getUserFromToken, isTokenExpired } from '@/shared/utils/token-utils' + +const TOKEN_CHECK_INTERVAL = 600000 // 10분 +const ADMIN_EXPIRY_WARNING = 600000 // 10분 + +export const useAuth = () => { + const router = useRouter() + const { user, isAuthenticated, isKeepLoggedIn } = useAuthStore() + + useEffect(() => { + const initializeAuth = () => { + const accessToken = getAccessToken() + const refreshToken = getRefreshToken() + + if (!accessToken || !refreshToken) { + handleLogout() + return + } + + const user = getUserFromToken(accessToken) + if (!user) { + handleLogout() + return + } + + if (user.role === 'admin' && isTokenExpired(accessToken)) { + handleLogout() + return + } + + if (user.role === 'user') { + const sessionToken = sessionStorage.getItem('sessionToken') + if (!isKeepLoggedIn && !sessionToken) { + handleLogout() + return + } + } + + useAuthStore.getState().setAuthState({ + isAuthenticated: true, + user, + isLoggedOut: false, + }) + } + + initializeAuth() + }, [isKeepLoggedIn]) + + const { data: tokenStatus } = useQuery({ + queryKey: ['tokenStatus'], + queryFn: async () => { + const accessToken = getAccessToken() + const refreshToken = getRefreshToken() + + if (!accessToken || !refreshToken) { + handleLogout() + return null + } + + const user = getUserFromToken(accessToken) + if (!user) { + handleLogout() + return null + } + + const isExpired = isTokenExpired(accessToken) + const timeUntilExpiry = getTimeUntilExpiry(accessToken) + + if (user.role === 'admin') { + if (isExpired) { + handleLogout() + return null + } + return { + isValid: !isExpired, + timeUntilExpiry, + isNearExpiry: timeUntilExpiry < ADMIN_EXPIRY_WARNING, + } + } + + if (user.role === 'user') { + const sessionToken = sessionStorage.getItem('sessionToken') + if (!isKeepLoggedIn && !sessionToken) { + handleLogout() + return null + } + } + + return { + isValid: !isExpired, + timeUntilExpiry, + isNearExpiry: false, + } + }, + refetchInterval: TOKEN_CHECK_INTERVAL, + }) + + const handleLogout = () => { + sessionStorage.removeItem('sessionToken') + logout() + router.replace(PATH.SIGN_IN) + } + + return { + user, + isAuthenticated, + isKeepLoggedIn, + tokenStatus, + isAdminNearExpiry: tokenStatus?.isNearExpiry, + logout: handleLogout, + } +} From 5d7bb627ca68e644ba5fc6a5f82716b58037eba6 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:35:24 +0900 Subject: [PATCH 127/498] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EC=A7=80=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=ED=86=A0=ED=81=B0=20=EA=B4=80=EB=A6=AC=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/lib/auth-tokens.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 shared/lib/auth-tokens.ts diff --git a/shared/lib/auth-tokens.ts b/shared/lib/auth-tokens.ts new file mode 100644 index 00000000..9f88d0a0 --- /dev/null +++ b/shared/lib/auth-tokens.ts @@ -0,0 +1,23 @@ +export const setAccessToken = (token: string) => { + localStorage.setItem('accessToken', token) +} + +export const setRefreshToken = (token: string) => { + localStorage.setItem('refreshToken', token) +} + +export const getAccessToken = () => { + return localStorage.getItem('accessToken') +} + +export const getRefreshToken = () => { + return localStorage.getItem('refreshToken') +} + +export const removeAccessToken = () => { + localStorage.removeItem('accessToken') +} + +export const removeRefreshToken = () => { + localStorage.removeItem('refreshToken') +} From a28318a4ab8d993d4b15242f181a362140506cd7 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:36:33 +0900 Subject: [PATCH 128/498] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=20=EC=B6=94=EA=B0=80=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/stores/use-auth-store.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 shared/stores/use-auth-store.ts diff --git a/shared/stores/use-auth-store.ts b/shared/stores/use-auth-store.ts new file mode 100644 index 00000000..9a6c411d --- /dev/null +++ b/shared/stores/use-auth-store.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand' + +import type { UserModel } from '../types/auth' + +interface AuthStateModel { + isAuthenticated: boolean + user: UserModel | null + isKeepLoggedIn: boolean + isLoggedOut: boolean + setKeepLoggedIn: (value: boolean) => void + setAuthState: (state: Partial) => void +} + +export const useAuthStore = create((set) => ({ + isAuthenticated: false, + user: null, + isKeepLoggedIn: false, + isLoggedOut: false, + + setKeepLoggedIn: (value) => set({ isKeepLoggedIn: value }), + setAuthState: (state) => set(state), +})) From dcd0cbf9a92d4e707e4192e5ae8afcec7cb1bb42 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:37:47 +0900 Subject: [PATCH 129/498] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98=20=EB=B0=8F?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EA=B0=80=EB=93=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/types/auth.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 shared/types/auth.ts diff --git a/shared/types/auth.ts b/shared/types/auth.ts new file mode 100644 index 00000000..52622c9a --- /dev/null +++ b/shared/types/auth.ts @@ -0,0 +1,50 @@ +export type UserRoleType = 'admin' | 'user' + +export interface UserModel { + id: string + email: string + name: string + role: UserRoleType + createdAt?: string +} + +export interface LoginFormDataModel { + email: string + password: string +} + +export interface TokenDataModel { + accessToken: string + refreshToken: string +} + +export interface AuthResponseModel { + accessToken: string + refreshToken: string + user: UserModel + keepLoggedIn?: boolean +} + +export interface ApiResponseModel { + isSuccess: boolean + message: string + data?: T + code?: string +} + +export type LoginResponseType = ApiResponseModel +export type TokenResponseType = ApiResponseModel + +export interface TokenPayloadModel { + user: UserModel + exp: number + iat: number +} + +export const isAdmin = (user: UserModel | null): user is UserModel & { role: 'admin' } => { + return user?.role === 'admin' +} + +export const isUser = (user: UserModel | null): user is UserModel & { role: 'user' } => { + return user?.role === 'user' +} From 06421be43b306c3597441569bb7464e084a92382 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:39:34 +0900 Subject: [PATCH 130/498] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=EC=9D=B4=EB=82=98=20=EA=B2=80=EC=A6=9D=20=EB=93=B1=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=B2=98=EB=A6=AC=EC=97=90=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=EB=90=9C=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=93=A4=20=EC=B6=94=EA=B0=80=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/utils/token-utils.ts | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 shared/utils/token-utils.ts diff --git a/shared/utils/token-utils.ts b/shared/utils/token-utils.ts new file mode 100644 index 00000000..c0e56866 --- /dev/null +++ b/shared/utils/token-utils.ts @@ -0,0 +1,83 @@ +import { jwtDecode } from 'jwt-decode' + +import { getRefreshToken, setAccessToken, setRefreshToken } from '@/shared/lib/auth-tokens' +import type { TokenPayloadModel, TokenResponseType } from '@/shared/types/auth' + +import axiosInstance from '../api/axios' + +let isRefreshing = false +let refreshSubscribers: ((token: string) => void)[] = [] + +export const refreshToken = async (): Promise => { + const refreshToken = getRefreshToken() + if (!refreshToken) return null + + if (isRefreshing) { + return new Promise((resolve) => { + refreshSubscribers.push((token) => resolve(token)) + }) + } + + try { + isRefreshing = true + + const response = await axiosInstance.post( + '/api/users/reissue/refreshtoken', + {}, + { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + } + ) + + if (!response.data.data) { + return null + } + + const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data.data + + if (newAccessToken && newRefreshToken) { + setAccessToken(newAccessToken) + setRefreshToken(newRefreshToken) + refreshSubscribers.forEach((cb) => cb(newAccessToken)) + return newAccessToken + } + + return null + } catch (error) { + console.error('Token refresh failed:', error) + return null + } finally { + isRefreshing = false + refreshSubscribers = [] + } +} + +export const isTokenExpired = (token: string): boolean => { + try { + const decoded = jwtDecode(token) + return decoded.exp * 1000 < Date.now() + } catch { + return true + } +} + +export const getTimeUntilExpiry = (token: string): number => { + try { + const decoded = jwtDecode(token) + return decoded.exp * 1000 - Date.now() + } catch { + return 0 + } +} + +export const getUserFromToken = (token: string | null): TokenPayloadModel['user'] | null => { + if (!token) return null + try { + const decoded = jwtDecode(token) + return decoded.user + } catch { + return null + } +} From 3e1469d9088a418dc23f9ad927e5299ccaf32df7 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Nov 2024 16:41:30 +0900 Subject: [PATCH 131/498] =?UTF-8?q?feat:=20=EA=B8=B0=ED=83=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=82=AC=ED=95=AD=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/constants/path.ts | 4 ++++ shared/ui/button/index.tsx | 2 +- shared/ui/check-box/index.tsx | 10 ++++++++-- shared/ui/input/index.tsx | 4 +--- shared/ui/side-navigation/user-navigation.tsx | 15 +++++++++++++-- shared/utils/validation.ts | 8 +++----- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/shared/constants/path.ts b/shared/constants/path.ts index b7ad1f31..72909a11 100644 --- a/shared/constants/path.ts +++ b/shared/constants/path.ts @@ -26,4 +26,8 @@ export const PATH = { ADMIN_NOTICES: '/admin/notices', ADMIN_STRATEGIES: '/admin/strategies', ADMIN_QUESTIONS: '/admin/questions', + + // Etc + FIND_ID: '/find-id', + FIND_PASSWORD: '/find-password', } as const diff --git a/shared/ui/button/index.tsx b/shared/ui/button/index.tsx index 7075e6c2..14f40ef7 100644 --- a/shared/ui/button/index.tsx +++ b/shared/ui/button/index.tsx @@ -14,7 +14,7 @@ export type ButtonVariantType = 'outline' | 'filled' interface Props extends ComponentProps<'button'> { size?: ButtonSizeType variant?: ButtonVariantType - onClick: () => void + onClick?: () => void } const _Button = ({ diff --git a/shared/ui/check-box/index.tsx b/shared/ui/check-box/index.tsx index 2ad48b8b..8602e905 100644 --- a/shared/ui/check-box/index.tsx +++ b/shared/ui/check-box/index.tsx @@ -9,13 +9,19 @@ const cx = classNames.bind(styles) interface Props { label?: string - isChecked: boolean + isChecked?: boolean onChange: (checked: boolean) => void className?: string textColor?: 'gray500' | 'gray600' | 'gray800' } -const Checkbox = ({ label, isChecked, onChange, className = '', textColor = 'gray500' }: Props) => { +const Checkbox = ({ + label, + isChecked = false, + onChange, + className = '', + textColor = 'gray500', +}: Props) => { const handleClick = () => { onChange(!isChecked) } diff --git a/shared/ui/input/index.tsx b/shared/ui/input/index.tsx index bd5be9af..9e027fe6 100644 --- a/shared/ui/input/index.tsx +++ b/shared/ui/input/index.tsx @@ -4,8 +4,6 @@ import { ComponentProps } from 'react' import classNames from 'classnames/bind' -import { ErrorMessageType } from '@/shared/types/error-message' - import styles from './styles.module.scss' const cx = classNames.bind(styles) @@ -14,7 +12,7 @@ export type InputSizeType = 'small' | 'medium' | 'large' | 'full' interface Props extends ComponentProps<'input'> { inputSize?: InputSizeType - errorMessage?: ErrorMessageType | null + errorMessage?: string | null } export const Input = ({ inputSize = 'medium', errorMessage, className, ...props }: Props) => { diff --git a/shared/ui/side-navigation/user-navigation.tsx b/shared/ui/side-navigation/user-navigation.tsx index c680bb66..5ba238ae 100644 --- a/shared/ui/side-navigation/user-navigation.tsx +++ b/shared/ui/side-navigation/user-navigation.tsx @@ -1,10 +1,11 @@ 'use client' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { ChangeIcon, ProfileIcon, SignOutIcon } from '@/public/icons' import classNames from 'classnames/bind' +import { logout } from '@/shared/api/auth' import { fetchUser } from '@/shared/api/user' import { PATH } from '@/shared/constants/path' import NavButtonItem from '@/shared/ui/side-navigation/nav-button-item' @@ -16,11 +17,21 @@ const cx = classNames.bind(styles) const UserNavigation = () => { const path = usePathname() + const router = useRouter() const isAdminPage = path.startsWith(PATH.ADMIN) const user = fetchUser() const isAdmin = user.role.includes('admin') + const handleLogout = async () => { + try { + logout() + router.replace(PATH.SIGN_IN) + } catch (error) { + console.error('로그아웃 실패:', error) + } + } + return (