diff --git a/public/images/logo.svg b/public/images/logo.svg deleted file mode 100644 index a38087d..0000000 --- a/public/images/logo.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/public/images/logo_symbol.svg b/public/images/logo_symbol.svg new file mode 100644 index 0000000..817ff2b --- /dev/null +++ b/public/images/logo_symbol.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/logo_typo.svg b/public/images/logo_typo.svg new file mode 100644 index 0000000..40c73e4 --- /dev/null +++ b/public/images/logo_typo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/app/(user-access)/components/modal/Modal.module.css b/src/app/(user-access)/components/modal/Modal.module.css new file mode 100644 index 0000000..44aa445 --- /dev/null +++ b/src/app/(user-access)/components/modal/Modal.module.css @@ -0,0 +1,33 @@ +.modalContainer { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.modal { + width: 308px; + height: 152px; + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.message { + margin-top: 20px; + font-size: 18px; + font-weight: 500; + text-align: center; +} + +.button { + margin-top: 30px; + height: 40px; +} diff --git a/src/app/(user-access)/components/modal/Modal.tsx b/src/app/(user-access)/components/modal/Modal.tsx new file mode 100644 index 0000000..a528693 --- /dev/null +++ b/src/app/(user-access)/components/modal/Modal.tsx @@ -0,0 +1,35 @@ +import useModalStore from '../../modalStore/modalStore'; +import Button from '@/components/Button'; +import styles from './Modal.module.css'; +import { useRouter } from 'next/navigation'; + +interface AlertModalProps { + message: string; +} + +const AlertModal = ({ message }: AlertModalProps) => { + const { closeModal, messageType } = useModalStore(); + const router = useRouter(); + + const handleConfirm = () => { + closeModal(); + if (messageType === 'success') { + router.push('/login'); + } + }; + + return ( +
+
+

{message}

+
+
+ +
+
+ ); +}; + +export default AlertModal; diff --git a/src/app/(user-access)/components/modal/ModalContainer.tsx b/src/app/(user-access)/components/modal/ModalContainer.tsx new file mode 100644 index 0000000..7f5f534 --- /dev/null +++ b/src/app/(user-access)/components/modal/ModalContainer.tsx @@ -0,0 +1,17 @@ +import useModalStore from '../../modalStore/modalStore'; +import styles from './Modal.module.css'; +import AlertModal from './Modal'; + +const ModalContainer = () => { + const { isOpen, message } = useModalStore(); + + if (!isOpen) return null; + + return ( +
+ +
+ ); +}; + +export default ModalContainer; diff --git a/src/app/(user-access)/layout.module.css b/src/app/(user-access)/layout.module.css index 61e1788..56c8ced 100644 --- a/src/app/(user-access)/layout.module.css +++ b/src/app/(user-access)/layout.module.css @@ -5,16 +5,13 @@ align-items: center; min-height: 100vh; width: 100%; - background-color: var(--white); + background-color: var(--gray-100); } .authContent { width: 100%; - max-width: 400px; - box-sizing: border-box; - display: flex; - justify-content: center; - background-color: var(--white); + max-width: 351px; + background-color: var(--gray-100); border-radius: 8px; padding: 20px; } @@ -32,12 +29,6 @@ height: auto; } -@media screen and (min-width: 768px) { - .authContent { - max-width: 351px; - } -} - @media screen and (min-width: 1200px) { .authContent { max-width: 520px; diff --git a/src/api/auth/login.ts b/src/app/(user-access)/lib/loginService.ts similarity index 100% rename from src/api/auth/login.ts rename to src/app/(user-access)/lib/loginService.ts diff --git a/src/api/auth/signup.ts b/src/app/(user-access)/lib/signupService.ts similarity index 100% rename from src/api/auth/signup.ts rename to src/app/(user-access)/lib/signupService.ts diff --git a/src/app/(user-access)/login/loginPage.module.css b/src/app/(user-access)/login/loginPage.module.css index 6ae59b8..0637301 100644 --- a/src/app/(user-access)/login/loginPage.module.css +++ b/src/app/(user-access)/login/loginPage.module.css @@ -3,7 +3,6 @@ flex-direction: column; gap: 18px; width: 100%; - max-width: 400px; } .inputWrapper { @@ -33,6 +32,7 @@ border-color: var(--violet); outline: none; } + .errorMessage { color: var(--red); font-size: 12px; @@ -49,3 +49,9 @@ font-size: 20px; text-align: center; } + +.signupLink { + color: var(--violet); + text-decoration: underline; + cursor: pointer; +} diff --git a/src/app/(user-access)/login/page.tsx b/src/app/(user-access)/login/page.tsx index e92f0c2..68ddc56 100644 --- a/src/app/(user-access)/login/page.tsx +++ b/src/app/(user-access)/login/page.tsx @@ -1,7 +1,6 @@ 'use client'; import React from 'react'; -import axios from 'axios'; import { useForm, FieldValues, UseFormReturn } from 'react-hook-form'; import { useRouter } from 'next/navigation'; import useAuthStore from '@/store/authStore'; @@ -9,6 +8,10 @@ import Button from '@/components/Button'; import { ERROR_MESSAGES } from '@/constants/message'; import type { User } from '@/types/user'; import styles from './loginPage.module.css'; +import ModalContainer from '../components/modal/ModalContainer'; +import useModalStore from '../modalStore/modalStore'; +import axios from 'axios'; +import { EMAIL_REGEX } from '@/constants/regex'; type LoginFormInputs = { email: string; @@ -33,6 +36,7 @@ export default function LoginPage() { const router = useRouter(); const { setUser } = useAuthStore(); + const { openModal } = useModalStore(); const onSubmit = async (data: LoginFormInputs) => { try { @@ -43,78 +47,76 @@ export default function LoginPage() { router.replace('/mydashboard'); } catch (error) { - console.error('로그인 실패:', error); - alert('비밀번호가 일치하지 않습니다.'); + openModal('비밀번호가 일치하지 않습니다.', 'error'); } }; return ( -
-
-

오늘도 만나서 반가워요!

- - - {errors.email && ( - {errors.email.message} - )} -
+
+ + +
+

오늘도 만나서 반가워요!

+ + + {errors.email && ( + {errors.email.message} + )} +
-
- - - {errors.password && ( -

{errors.password.message}

- )} -
- -
-

- 회원이 아니신가요?{' '} - router.push('/signup')} - > - 회원가입하기 - {' '} -

-
- +
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+ +
+

+ 회원이 아니신가요?{' '} + router.push('/signup')} + > + 회원가입하기 + +

+
+ +
); } diff --git a/src/app/(user-access)/modalStore/modalStore.ts b/src/app/(user-access)/modalStore/modalStore.ts new file mode 100644 index 0000000..b333325 --- /dev/null +++ b/src/app/(user-access)/modalStore/modalStore.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +interface ModalState { + isOpen: boolean; + message: string; + messageType: string; + openModal: (message: string, messageType: string) => void; + closeModal: () => void; +} + +const useModalStore = create((set) => ({ + isOpen: false, + message: '', + messageType: '', + openModal: (message, messageType) => + set({ isOpen: true, message, messageType }), + closeModal: () => set({ isOpen: false, message: '', messageType: '' }), +})); + +export default useModalStore; diff --git a/src/app/(user-access)/signup/page.tsx b/src/app/(user-access)/signup/page.tsx index 0b98f2d..5d767df 100644 --- a/src/app/(user-access)/signup/page.tsx +++ b/src/app/(user-access)/signup/page.tsx @@ -1,3 +1,168 @@ -export default function Page() { - return
; +'use client'; + +import { useForm } from 'react-hook-form'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/Button'; +import styles from './signPage.module.css'; +import axiosInstance from '@/lib/axiosInstance'; +import { ERROR_MESSAGES } from '@/constants/message'; +import ModalContainer from '../components/modal/ModalContainer'; +import useModalStore from '../modalStore/modalStore'; +import { EMAIL_REGEX } from '@/constants/regex'; + +type SignupFormInputs = { + email: string; + nickname: string; + password: string; + confirmPassword: string; + termsAccepted: boolean; +}; + +export default function SignupPage() { + const { + register, + handleSubmit, + formState: { errors, isValid }, + watch, + } = useForm({ mode: 'onChange' }); + const router = useRouter(); + + const watchPassword = watch('password'); + const { openModal } = useModalStore(); + + const onSubmit = async (data: SignupFormInputs) => { + try { + await axiosInstance.post('/users', { + email: data.email, + nickname: data.nickname, + password: data.password, + }); + openModal('가입이 완료되었습니다!', 'success'); + } catch (error) { + openModal('이미 사용 중인 이메일입니다.', 'error'); + } + }; + + return ( +
+ +
+
+

첫 방문을 환영합니다!

+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+ +
+ + + {errors.nickname && ( +

{errors.nickname.message}

+ )} +
+
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+
+ + + value === watchPassword || ERROR_MESSAGES.PASSWORDS_MATCH, + })} + /> + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+
+ + + {errors.termsAccepted && ( +

+ {errors.termsAccepted.message} +

+ )} +
+ +
+

+ 이미 회원이신가요?{' '} + router.push('/login')} + > + 로그인하기 + +

+
+
+
+ ); } diff --git a/src/app/(user-access)/signup/signPage.module.css b/src/app/(user-access)/signup/signPage.module.css index e69de29..7d4383a 100644 --- a/src/app/(user-access)/signup/signPage.module.css +++ b/src/app/(user-access)/signup/signPage.module.css @@ -0,0 +1,108 @@ +.signupContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 50vh; + width: 100%; + background-color: var(--gray-100); +} + +.signupForm { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + max-width: 400px; +} + +.submitButton { + height: 40px; +} + +.inputWrapper { + display: flex; + flex-direction: column; +} + +.label { + color: var(--black-100); + font-weight: bold; + margin-top: 24px; + margin-bottom: 14px; +} + +.input { + width: 100%; + padding: 8px; + border: 1px solid var(--gray-300); + border-radius: 8px; +} + +.inputError { + border-color: var(--red); +} + +.input:focus { + border-color: var(--violet); + outline: none; +} + +.errorMessage { + color: var(--red); + font-size: 12px; + margin-top: 10px; +} + +.disabled { + background-color: var(--gray-400); + cursor: not-allowed; + opacity: 0.5; +} + +.greeting { + font-size: 20px; + text-align: center; + margin-top: 30px; +} + +.termsAccepted { + display: flex; + align-items: center; + gap: 8px; +} + +.linkContainer { + display: flex; + justify-content: center; +} + +.linkText { + color: var(--violet); + text-decoration: underline; + cursor: pointer; +} + +.input[type='checkbox'] { + width: 16px; + height: 16px; + margin: 0; + border: 2px solid var(--gray-300); + border-radius: 2px; + background-color: white; + cursor: pointer; + position: relative; +} + +.input[type='checkbox']:checked { + background-color: var(--white); +} + +.input[type='checkbox']:checked::before { + content: '✔'; + position: absolute; + top: 0; + left: 3px; + font-size: 12px; + color: var(--violet); +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.module.css index 8f58315..3c03e13 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.module.css +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.module.css @@ -11,6 +11,9 @@ .card.dragging { margin: 0; + box-shadow: + rgba(0, 0, 0, 0.16) 0px 3px 6px, + rgba(0, 0, 0, 0.23) 0px 3px 6px; } .cardContainer { @@ -24,8 +27,8 @@ .imageWrapper { position: relative; - width: 280px; - height: 150px; + width: 260px; + height: 121px; margin-bottom: 4px; } @@ -60,19 +63,23 @@ gap: 6px; } -.tagContainer:empty { +.imageContainer:empty { display: none; } .optionalInfo { + flex: 1; display: flex; - justify-content: space-between; + justify-content: flex-end; + align-items: center; } .dueDateWrapper { + flex: 1; display: flex; - gap: 4px; align-items: center; + gap: 4px; + width: 125px; } .icon path { @@ -89,13 +96,13 @@ width: 22px; height: 22px; font-size: 12px; + padding-bottom: 2.5px; } @media screen and (min-width: 768px) { .card { padding: 16px 20px; margin-bottom: 16px; - min-height: 93px; } .modal { @@ -114,12 +121,14 @@ .contentContainer { flex-direction: row; - gap: 12px; + align-items: center; + justify-content: space-between; + gap: 0; } .imageWrapper { - width: 90px; - height: 57px; + width: 91px; + height: 53px; margin-bottom: 0; } @@ -129,51 +138,20 @@ } .optionalInfo { - flex: 1; - } - - .modal { - width: 680px; - } - - .cardContainer { - flex-direction: row; - gap: 22px; - } - - .cardInfo { - flex: 1; - gap: 10px; - } - - .contentContainer { - flex-direction: row; - gap: 12px; - } - - .imageWrapper { - width: 90px; - height: 57px; - margin-bottom: 0; + gap: 16px; } - .title { - font-size: 16px; - line-height: 26px; - } - - .optionalInfo { - flex: 1; + .dueDateWrapper { + justify-content: flex-end; } .modal { - width: 730px; + width: 680px; } } @media screen and (min-width: 1200px) { .card { - min-height: 128px; height: auto; min-width: 312px; } @@ -187,6 +165,10 @@ gap: 0; } + .tagContainer { + max-width: none; + } + .imageWrapper { width: 274px; height: 160px; @@ -195,5 +177,17 @@ .contentContainer { flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .optionalInfo { + width: 100%; + justify-content: space-between; + gap: 0; + } + + .dueDateWrapper { + justify-content: flex-start; } } diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx index 1898f59..f9fc993 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Draggable } from 'react-beautiful-dnd'; -import { Cards } from '@/types/dashboardView'; +import { Card as CardType } from '@/types/dashboardView'; import { useModal } from '@/app/(with-header-sidebar)/mydashboard/_hooks/useModal'; import Modal from '@/app/(with-header-sidebar)/mydashboard/_components/modal/Modal'; import CardInfo from './card-detail/CardInfo'; @@ -13,20 +13,26 @@ import useCardStore from '@/store/cardStore'; import styles from './Card.module.css'; interface Props { - item: Cards; + item: CardType; index: number; columnTitle: string; } function Card({ item, index, columnTitle }: Props) { const { isOpen, openModal, isClosing, closeModal } = useModal(); + const card = useCardStore((state) => + state.cards.find((card) => card.id === item.id) + ); + + useEffect(() => { + useCardStore.getState().addCard(item); + }, [item]); if (!item || !item.id) { return null; } - useCardStore.getState().setCard(item); - const { id, title, imageUrl, tags, dueDate, assignee } = item; + const { id, title, imageUrl, tags, dueDate, assignee } = card || item; return ( <> @@ -93,7 +99,7 @@ function Card({ item, index, columnTitle }: Props) { )} className={styles.modal} > - + )} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/CloseButton.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/CloseButton.module.css new file mode 100644 index 0000000..621ec6b --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/CloseButton.module.css @@ -0,0 +1,17 @@ +.closeButtonWrapper { + position: relative; + width: 24px; + height: 24px; +} + +.closeButton { + display: inline-flex; + align-items: center; +} + +@media screen and (min-width: 768px) { + .closeButtonWrapper { + width: 36px; + height: 36px; + } +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/CloseButton.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/CloseButton.tsx new file mode 100644 index 0000000..1ed297d --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/CloseButton.tsx @@ -0,0 +1,21 @@ +import Image from 'next/image'; +import styles from './CloseButton.module.css'; + +interface Props { + onClick: () => void; +} + +export default function CloseButton({ onClick }: Props) { + return ( +
+ +
+ ); +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.module.css index 32b432f..4a18cf7 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.module.css +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.module.css @@ -5,15 +5,21 @@ width: auto; max-height: 385px; height: auto; - padding: 16px 12px; - gap: 24px; + padding: 0 12px 16px 12px; } .header { + height: 62px; + padding: 16px 0; display: flex; align-items: center; justify-content: space-between; - height: 24px; + position: sticky; + top: 0; + width: 100%; + left: 0; + background-color: var(--gray-100); + z-index: 1; } .status { @@ -71,6 +77,7 @@ width: 100%; height: 100%; border-bottom: 1px solid var(--gray-200); + padding-top: 8px; } .createCard.createCard { @@ -78,7 +85,7 @@ align-items: center; justify-content: center; width: 100%; - height: 32px; + min-height: 32px; border: 1px solid var(--gray-300); background: var(--white); font-size: 18px; @@ -122,10 +129,25 @@ @media screen and (min-width: 768px) { .column { padding: 20px; + padding-top: 0; max-height: 346px; border-bottom: 1px solid var(--gray-200); } + .header { + height: 68px; + padding: 20px 0; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + width: 100%; + left: 0; + background-color: var(--gray-100); + z-index: 1; + } + .createCard { height: 40px; } @@ -133,10 +155,11 @@ .columnContent { gap: 16px; border-bottom: none; + padding-top: 4px; } .scrollContext { - max-height: 202px; + min-height: 202px; } } @media screen and (min-width: 1200px) { diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx index 2135235..7d998d5 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx @@ -1,11 +1,12 @@ import React, { useRef, useEffect } from 'react'; import { Droppable } from 'react-beautiful-dnd'; -import { Columns } from '@/types/dashboardView'; +import { DashboardColumn } from '@/types/dashboardView'; import Button from '@/components/Button'; import Image from 'next/image'; import Card from './Card'; import useModalStore from '@/store/modalStore'; -import CreateTaskModal from './CreateTaskModal'; +import CreateTaskModal from './CreateCardModal'; +import SettingsColumnModal from './SettingsColumnModal'; import styles from './Column.module.css'; function Column({ @@ -15,7 +16,7 @@ function Column({ id, items, loadMoreData, -}: Columns) { +}: DashboardColumn) { const observerRef = useRef(null); const loadMoreRef = useRef(null); const columnRef = useRef(null); @@ -54,6 +55,10 @@ function Column({ openModal(); }; + const handleSettingsColumn = () => { + openModal(); + }; + return (
@@ -68,29 +73,28 @@ function Column({ type="button" aria-label="컬럼 설정 버튼" className={styles.settings} + onClick={handleSettingsColumn} >
-
-
- -
+
+
{items.map((item, index) => item ? ( @@ -117,8 +126,8 @@ function Column({ /> ) : null )} -
{provided.placeholder} +
)}
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/CommonColumnModal.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/CommonColumnModal.module.css new file mode 100644 index 0000000..e410aee --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/CommonColumnModal.module.css @@ -0,0 +1,73 @@ +.modal { + padding: 24px 16px; + display: flex; + flex-direction: column; + gap: 16px; + width: 327px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + color: var(--black-100); + font-size: 20px; + font-weight: 700; + line-height: 32px; +} + +.form { + display: flex; + flex-direction: column; + gap: 24px; +} + +.form label { + color: var(--black-100); + font-size: 16px; + font-weight: 500; + line-height: 26px; +} + +.footer { + display: flex; + justify-content: space-between; +} + +.footer button { + padding: 14px 46px; + width: 144px; + height: 54px; + font-size: 16px; + line-height: 26px; +} + +.negativeButton { + border: 1px solid var(--gray-300); + background: inherit; + color: var(--gray-500); + font-weight: 500; +} + +@media screen and (min-width: 768px) { + .modal { + width: 568px; + padding: 24px; + gap: 24px; + } + + .title { + font-size: 24px; + } + + .form label { + font-size: 18px; + } + + .footer button { + width: 256px; + } +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateTaskModal.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateCardModal.module.css similarity index 100% rename from src/app/(with-header-sidebar)/dashboard/[id]/components/CreateTaskModal.module.css rename to src/app/(with-header-sidebar)/dashboard/[id]/components/CreateCardModal.module.css diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateTaskModal.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateCardModal.tsx similarity index 91% rename from src/app/(with-header-sidebar)/dashboard/[id]/components/CreateTaskModal.tsx rename to src/app/(with-header-sidebar)/dashboard/[id]/components/CreateCardModal.tsx index 8aaf794..b24348a 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateTaskModal.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateCardModal.tsx @@ -11,7 +11,8 @@ import useMember from '../edit/_hooks/useMember'; import useDashboardStore from '@/store/dashboardStore'; import { ERROR_MESSAGES } from '@/constants/message'; import { createCard } from '@/lib/cardService'; -import styles from './CreateTaskModal.module.css'; +import useTriggerStore from '@/store/triggerStore'; +import styles from './CreateCardModal.module.css'; export interface TaskFormValues { assigneeUserId: number; @@ -24,6 +25,7 @@ export interface TaskFormValues { export default function CreateTaskModal({ columnId }: { columnId: number }) { const { closeModal } = useModalStore(); + const { updateTrigger } = useTriggerStore(); const { register, handleSubmit, @@ -35,7 +37,8 @@ export default function CreateTaskModal({ columnId }: { columnId: number }) { const onSubmit = async (data: TaskFormValues) => { if (dashboard) { - const response = await createCard(data, columnId, dashboard.id); + await createCard(data, columnId, dashboard.id); + updateTrigger.card(); closeModal(); } }; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateColumnModal.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateColumnModal.tsx new file mode 100644 index 0000000..e9cbfc8 --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/CreateColumnModal.tsx @@ -0,0 +1,78 @@ +import useModalStore from '@/store/modalStore'; +import { useForm } from 'react-hook-form'; +import Button from '@/components/Button'; +import Input from '@/components/Input'; +import useDashboardStore from '@/store/dashboardStore'; +import { ERROR_MESSAGES } from '@/constants/message'; +import { ColumnFormValue } from '@/types/dashboardView'; +import useColumn from '../hooks/useColumn'; +import { createColumn } from '@/lib/columnServie'; +import styles from './CommonColumnModal.module.css'; + +export default function CreateColumnModal() { + const { closeModal } = useModalStore(); + const dashboard = useDashboardStore((state) => state.dashboard); + const { columns } = useColumn(dashboard ? dashboard.id : null); + + const { + register, + handleSubmit, + setError, + formState: { errors, isValid }, + } = useForm({ mode: 'onChange' }); + + const handleError = (message: string) => { + setError('title', { + type: 'manual', + message, + }); + }; + + const onSubmit = async (data: ColumnFormValue) => { + if (dashboard) { + try { + await createColumn(dashboard.id, data.title); + closeModal(); + } catch (err) { + if (err instanceof Error) handleError(err.message); + } + } + }; + + return ( +
+

새 컬럼 생성

+
+ { + const isDuplicate = columns.some( + (column) => column.title === value + ); + return isDuplicate ? '이미 존재하는 컬럼입니다.' : true; + }, + })} + error={errors.title} + /> + +
+ + +
+
+
+ ); +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/SettingsColumnModal.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/SettingsColumnModal.tsx new file mode 100644 index 0000000..25917c7 --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/SettingsColumnModal.tsx @@ -0,0 +1,106 @@ +import useModalStore from '@/store/modalStore'; +import { useForm } from 'react-hook-form'; +import Button from '@/components/Button'; +import Input from '@/components/Input'; +import useDashboardStore from '@/store/dashboardStore'; +import { ERROR_MESSAGES } from '@/constants/message'; +import { ColumnFormValue } from '@/types/dashboardView'; +import useColumn from '../hooks/useColumn'; +import { deleteColumn, updateColumn } from '@/lib/columnServie'; +import CloseButton from './CloseButton'; +import styles from './CommonColumnModal.module.css'; + +export default function SettingsColumnModal({ + title: prevTitle, + id, +}: ColumnFormValue) { + const { closeModal } = useModalStore(); + const dashboard = useDashboardStore((state) => state.dashboard); + const { columns } = useColumn(dashboard ? dashboard.id : null); + + const { + register, + handleSubmit, + setError, + watch, + formState: { errors }, + } = useForm({ mode: 'onChange' }); + + const handleError = (message: string) => { + setError('title', { + type: 'manual', + message, + }); + }; + + const { title } = watch(); + const restColumns = columns.filter((column) => column.id !== id); + const isDuplicate = restColumns.some((column) => column.title === title); + + const onDelete = async () => { + if (!title) { + handleError('삭제하려면 해당 컬럼의 이름을 입력해주세요.'); + return; + } + + if (title !== prevTitle) { + const errorMessage = isDuplicate + ? '다른 컬럼은 삭제할 수 없습니다.' + : '존재하지 않는 컬럼입니다.'; + handleError(errorMessage); + return; + } + + await deleteColumn(`${id}`); + closeModal(); + }; + + const onUpdate = async (data: ColumnFormValue) => { + const { title } = data; + + if (title === prevTitle) { + closeModal(); + return; + } + + if (isDuplicate) { + handleError('이미 존재하는 컬럼입니다.'); + return; + } + + if (title) await updateColumn(`${id}`, title); + closeModal(); + }; + + return ( +
+
+

컬럼 관리

+ +
+ +
+ + +
+ + +
+
+
+ ); +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateTaskModal.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateCardModal.module.css similarity index 100% rename from src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateTaskModal.module.css rename to src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateCardModal.module.css diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateTaskModal.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateCardModal.tsx similarity index 90% rename from src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateTaskModal.tsx rename to src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateCardModal.tsx index 16455f8..d4a7c25 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateTaskModal.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/UpdateCardModal.tsx @@ -12,17 +12,17 @@ import useMember from '../edit/_hooks/useMember'; import useDashboardStore from '@/store/dashboardStore'; import { ERROR_MESSAGES } from '@/constants/message'; import { updateCard } from '@/lib/cardService'; -import { TaskFormValues } from './CreateTaskModal'; +import { TaskFormValues } from './CreateCardModal'; import useCardStore from '@/store/cardStore'; import StateDropdown from './StateDropdown'; import useColumn from '../hooks/useColumn'; -import styles from './UpdateTaskModal.module.css'; +import styles from './UpdateCardModal.module.css'; interface TaskUpdateFormValues extends TaskFormValues { columnId: number; } -export default function UpdateTaskModal() { +export default function UpdateCardModal({ cardId }: { cardId: number }) { const { closeModal } = useModalStore(); const { register, @@ -34,12 +34,15 @@ export default function UpdateTaskModal() { const dashboard = useDashboardStore((state) => state.dashboard); const { members } = useMember(dashboard?.id.toString() || null, 10); const { columns } = useColumn(dashboard?.id || null); - const { card, setCard } = useCardStore(); + const card = useCardStore((state) => + state.cards.find((card) => card.id === cardId) + ); + const modifyCard = useCardStore((state) => state.modifyCard); const onSubmit = async (data: TaskUpdateFormValues) => { if (card) { const response = await updateCard(data, card.columnId, card.id); - setCard(response); + modifyCard(card.id, response); closeModal(); } }; @@ -72,7 +75,7 @@ export default function UpdateTaskModal() { options={members} setValue={setValue} defaultAssignee={ - members?.filter((member) => member.userId == card?.assignee.id)[0] + members?.filter((member) => member.userId == card?.assignee?.id)[0] } className={styles.dropdown} /> diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx index b722abf..45ff4f6 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx @@ -1,10 +1,10 @@ -import type { Cards } from '@/types/dashboardView'; +import type { Card } from '@/types/dashboardView'; import Avatar from '@/components/Avatar'; import { formatDateToCustomFormat } from '@/utils/dateUtils'; import styles from './Assignment.module.css'; interface AssignmentProps { - card: Pick; + card: Pick; } export default function Assignment({ card }: AssignmentProps) { diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/CardInfo.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/CardInfo.tsx index c5fab34..3424790 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/CardInfo.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/CardInfo.tsx @@ -1,4 +1,4 @@ -import { Cards } from '@/types/dashboardView'; +import { Card } from '@/types/dashboardView'; import Assignment from './Assignment'; import Tag from '@/components/card/Tag'; import ColumnLabel from '@/components/card/ColumnLabel'; @@ -7,12 +7,12 @@ import Image from 'next/image'; import CreateCommentForm from './comments/CreateCommentForm'; import useDashboardStore from '@/store/dashboardStore'; import Comments from './comments/Comments'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import type { Comment } from '@/types/comment'; import styles from './CardInfo.module.css'; interface CardInfoProps { - card: Cards; + card: Card; columnTitle: string; } @@ -23,11 +23,15 @@ export default function CardInfo({ card, columnTitle }: CardInfoProps) { const [newComment, setNewComment] = useState(null); + useEffect(() => {}, [card]); + return (
-
- -
+ {card.assignee && ( +
+ +
+ )}
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css index 36db845..fabc813 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css @@ -10,6 +10,10 @@ align-items: center; } +.moreButton:focus { + outline: none; +} + .menuContainer { position: absolute; z-index: 999; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx index 6528cfc..c60b57d 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx @@ -3,11 +3,11 @@ import type { Menu } from '@/types/menu'; import MenuDropdown from '@/components/MenuDropdown'; import { useMenu } from '@/hooks/useMenu'; import { deleteCard } from '@/lib/cardService'; -import { useRouter } from 'next/navigation'; -import useDashboardStore from '@/store/dashboardStore'; import useModalStore from '@/store/modalStore'; -import UpdateTaskModal from '../UpdateTaskModal'; +import UpdateCardModal from '../UpdateCardModal'; import styles from './HeaderMenu.module.css'; +import useTriggerStore from '@/store/triggerStore'; +import useCardStore from '@/store/cardStore'; interface HeaderMenuProps { cardId: number; @@ -15,19 +15,19 @@ interface HeaderMenuProps { } export default function HeaderMenu({ cardId, closeModal }: HeaderMenuProps) { - const router = useRouter(); - const { dashboard } = useDashboardStore(); + const { updateTrigger } = useTriggerStore(); const { isMenuVisible, toggleMenu } = useMenu(); const { openModal } = useModalStore(); const handleDeleteClick = async () => { await deleteCard(cardId); closeModal(); - router.replace(`/dashboard/${dashboard?.id}`); + useCardStore.getState().removeCard(cardId); + updateTrigger.card(); }; const handleUpdateClick = () => { - openModal(); + openModal(); }; const cardMenus: Menu[] = [ diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/comments/CommentDetail.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/comments/CommentDetail.tsx index d448371..7e41e92 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/comments/CommentDetail.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/comments/CommentDetail.tsx @@ -1,10 +1,4 @@ -import { - useState, - ChangeEvent, - KeyboardEvent, - MouseEvent, - useRef, -} from 'react'; +import { useState, ChangeEvent, KeyboardEvent, useRef } from 'react'; import Avatar from '@/components/Avatar'; import Button from '@/components/Button'; import { formatDateToCustomFormat } from '@/utils/dateUtils'; @@ -58,7 +52,6 @@ export default function CommentDetail({ e.preventDefault(); handleSave(); toggleEditing(); - // todo: 수정성공시 토스트 박스 } }; @@ -80,7 +73,6 @@ export default function CommentDetail({ const handleDeleteOnClick = async () => { await deleteComment(id); onDelete(id); - // todo: 삭제성공시 토스트 박스 }; return ( diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/DeleteButton.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/DeleteButton.tsx index 17957f2..9828eef 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/DeleteButton.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/DeleteButton.tsx @@ -1,17 +1,21 @@ 'use client'; import { useRouter } from 'next/navigation'; -import useIdStore from '@/store/idStore'; import Button from '@/components/Button'; import { deleteDashboard } from '@/lib/boardService'; import styles from './DeleteButton.module.css'; +import useDashboardStore from '@/store/dashboardStore'; +import useTriggerStore from '@/store/triggerStore'; export default function DeleteButton() { - const id = useIdStore((state) => state.id); + const { dashboard, setDashboard } = useDashboardStore(); + const { updateTrigger } = useTriggerStore(); const router = useRouter(); const handleClick = async () => { - await deleteDashboard(id); + await deleteDashboard(dashboard!.id.toString()); + setDashboard(null); + updateTrigger.dashboard(); router.replace('/mydashboard'); }; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/EditForm.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/EditForm.tsx index 7e0a6ad..a6a25e4 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/EditForm.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/EditForm.tsx @@ -7,9 +7,11 @@ import { getBoard, updateBoard } from '@/lib/boardService'; import { Dashboard, UpdateDashboardRequestParams } from '@/types/dashboards'; import Button from '@/components/Button'; import DashboardInput from '@/components/DashboardInput'; +import useTriggerStore from '@/store/triggerStore'; import styles from './EditForm.module.css'; export default function EditForm() { + const { updateTrigger } = useTriggerStore(); const [board, setBoard] = useState(); const id = useIdStore((state) => state.id); const { @@ -26,6 +28,7 @@ export default function EditForm() { const onSubmit = async (data: UpdateDashboardRequestParams) => { const response = await updateBoard(id, data); + updateTrigger.dashboard(); setBoard(response); }; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/InvitationModal.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/InvitationModal.tsx index c735dab..dc8e04e 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/InvitationModal.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_components/InvitationModal.tsx @@ -18,10 +18,7 @@ export default function InvitationModal({ } = useForm<{ email: string }>({ mode: 'onChange' }); const { closeModal } = useModalStore(); - const onSubmit = async ({ email }: { email: string }) => { - handleInvite(email); - closeModal(); - }; + const onSubmit = async ({ email }: { email: string }) => handleInvite(email); return (
diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useInvitation.ts b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useInvitation.ts index ffa6c5c..f129324 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useInvitation.ts +++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useInvitation.ts @@ -6,6 +6,8 @@ import { } from '../_lib/invitationService'; import useApi from './useApi'; import { GetInvitationsResponse, Invitation } from '@/types/invitation'; +import { toast } from '@/store/toastStore'; +import useModalStore from '@/store/modalStore'; interface InvitationState { page: number; @@ -23,6 +25,7 @@ const useInvitation = (dashboardId: string | null, pageSize = 5) => { const [invitationState, setInvitationState] = useState( DEFAULT_INVITATION_STATE ); + const closeModal = useModalStore((state) => state.closeModal); const { isLoading, @@ -72,18 +75,25 @@ const useInvitation = (dashboardId: string | null, pageSize = 5) => { const handleCancel = async (invitationId: number) => { try { await deleteInvitation(dashboardId!, invitationId); + toast.success({ message: '취소되었습니다.' }); handleLoad(invitationState.page); } catch (error) { - throw error; + if (error instanceof Error) { + toast.error({ message: error.message }); + } } }; const handleInvite = async (email: string) => { try { await createInvitation(dashboardId!, email); + toast.success({ message: '초대되었습니다.' }); + closeModal(); handleLoad(invitationState.page); } catch (error) { - throw error; + if (error instanceof Error) { + toast.error({ message: error.message }); + } } }; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts index 2180867..f198a27 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts +++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts @@ -2,16 +2,19 @@ import { useCallback, useEffect, useState } from 'react'; import { getMembers, deleteMember } from '../_lib/memberService'; import useApi from './useApi'; import { GetMembersResponse, Member } from '@/types/member'; +import { toast } from '@/store/toastStore'; interface MemberState { page: number; totalPages: number; + totalCount: number; members: Member[]; } const DEFAULT_MEMBERS_STATE: MemberState = { page: 1, totalPages: 0, + totalCount: 0, members: [], }; @@ -43,6 +46,7 @@ const useMember = (dashboardId: string | null, pageSize = 4) => { ...prevState, members, totalPages, + totalCount, })); } catch (error) { throw error; @@ -66,9 +70,12 @@ const useMember = (dashboardId: string | null, pageSize = 4) => { const handleDelete = async (memberId: number) => { try { await deleteMember(memberId); + toast.success({ message: '삭제되었습니다' }); handleLoad(memberState.page); } catch (error) { - throw error; + if (error instanceof Error) { + toast.error({ message: error.message }); + } } }; @@ -80,6 +87,7 @@ const useMember = (dashboardId: string | null, pageSize = 4) => { page: memberState.page, members: memberState.members, totalPages: memberState.totalPages, + totalCount: memberState.totalCount, isLoading, error, handlePageChange, diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/hooks/useDashBoardView.ts b/src/app/(with-header-sidebar)/dashboard/[id]/hooks/useDashBoardView.ts index cca2745..eea4427 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/hooks/useDashBoardView.ts +++ b/src/app/(with-header-sidebar)/dashboard/[id]/hooks/useDashBoardView.ts @@ -1,12 +1,12 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import debounce from 'lodash/debounce'; import axiosInstance from '@/lib/axiosInstance'; import { DropResult } from 'react-beautiful-dnd'; -import { Columns, Cards } from '@/types/dashboardView'; +import { DashboardColumn, Card, Column } from '@/types/dashboardView'; import { COLUMN_URL, CARD_URL } from '@/constants/urls'; export default function useDashBoardView(dashboardId: string | undefined) { - const [columns, setColumns] = useState([]); + const [columns, setColumns] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [cursors, setCursors] = useState>({}); @@ -19,7 +19,7 @@ export default function useDashBoardView(dashboardId: string | undefined) { ); const columns = columnData.data; - const columnIds: number[] = columns.map((column: Columns) => column.id); + const columnIds: number[] = columns.map((column: Column) => column.id); const cardRequests = columnIds.map((columnId) => axiosInstance.get(`${CARD_URL}?size=10&columnId=${columnId}`) @@ -27,19 +27,21 @@ export default function useDashBoardView(dashboardId: string | undefined) { const cardResponses = await Promise.all(cardRequests); - const updatedColumns = columns.map((column: Columns, index: number) => { - const cardData = cardResponses[index].data.cards; - const totalCount = cardResponses[index].data.totalCount; + const updatedColumns = columns.map( + (column: DashboardColumn, index: number) => { + const cardData = cardResponses[index].data.cards; + const totalCount = cardResponses[index].data.totalCount; - return { - ...column, - items: cardData || [], - totalCount: totalCount || 0, - }; - }); + return { + ...column, + items: cardData || [], + totalCount: totalCount || 0, + }; + } + ); const initialCursors = updatedColumns.reduce( - (acc: Record, column: Columns) => { + (acc: Record, column: DashboardColumn) => { const lastCard = column.items[column.items.length - 1]; acc[column.id] = lastCard ? lastCard.id : null; return acc; @@ -56,19 +58,14 @@ export default function useDashBoardView(dashboardId: string | undefined) { } }, [dashboardId]); - useEffect(() => { - if (!dashboardId) return; - fetchData(); - }, [fetchData, dashboardId]); - const sendCardUpdateRequest = useCallback( - debounce(async (cardId: string, updatedCardData: Cards) => { + debounce(async (cardId: string, updatedCardData: Card) => { try { await axiosInstance.put(`${CARD_URL}/${cardId}`, updatedCardData); } catch (err) { if (err instanceof Error) setError(err.message); } - }, 200), + }, 100), [] ); @@ -164,7 +161,7 @@ export default function useDashBoardView(dashboardId: string | undefined) { items: [ ...column.items, ...newCards.filter( - (newCard: Cards) => + (newCard: Card) => !column.items.some( (prevCard) => prevCard.id === newCard.id ) @@ -182,9 +179,9 @@ export default function useDashBoardView(dashboardId: string | undefined) { } catch (err) { if (err instanceof Error) setError(err.message); } - }, 200), + }, 100), [cursors] ); - return { columns, loading, error, handleOnDragEnd, loadMoreData }; + return { columns, loading, error, handleOnDragEnd, loadMoreData, fetchData }; } diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/page.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/page.module.css index 34d432e..d676c43 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/page.module.css +++ b/src/app/(with-header-sidebar)/dashboard/[id]/page.module.css @@ -3,12 +3,16 @@ flex-direction: column; min-height: 100vh; background-color: var(--gray-100); + position: relative; } .createColumnSection { min-width: 284px; - width: auto; padding: 16px 12px; + position: sticky; + bottom: 0; + z-index: 1; + background-color: var(--gray-100); } .createColumn.createColumn { @@ -25,7 +29,6 @@ line-height: 26px; gap: 12px; border-radius: 8px; - margin-top: 48px; } .createColumnIcon { @@ -50,5 +53,6 @@ .createColumn.createColumn { width: 354px; + margin-top: 48px; } } diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx index 91b1b3a..0505ea1 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx @@ -8,17 +8,22 @@ import Column from './components/Column'; import Button from '@/components/Button'; import Image from 'next/image'; import useDashboardStore from '@/store/dashboardStore'; +import useModalStore from '@/store/modalStore'; +import CreateColumnModal from './components/CreateColumnModal'; +import useTriggerStore from '@/store/triggerStore'; import styles from './page.module.css'; +const DEFAULT_COLOR = 'var(--violet)'; + export default function DashBoardView() { + const { openModal } = useModalStore(); + const { trigger } = useTriggerStore(); const params = useParams(); const id = params.id; - const { columns, loading, error, handleOnDragEnd, loadMoreData } = + const { columns, loading, error, handleOnDragEnd, loadMoreData, fetchData } = useDashBoardView(`${id}`); - const dashboard = useDashboardStore((state) => state.dashboard); - const setDashboard = useDashboardStore((state) => state.setDashboard); - const color = useDashboardStore((state) => state.color); + const { dashboard, setDashboard } = useDashboardStore(); useEffect(() => { if (dashboard?.id !== Number(id)) { @@ -26,16 +31,27 @@ export default function DashBoardView() { } }, [id, dashboard?.id]); + useEffect(() => { + if (!dashboard) return; + fetchData(); + }, [dashboard, trigger]); + if (loading) return
Loading...
; if (error) return
Error: {error}
; + const COlUMN_COUNT = columns.length >= 9; + + const handleCreateColumn = () => { + openModal(); + }; + return (
{columns.map((column) => ( ))} + + {!COlUMN_COUNT && (
-
- + )}
); } diff --git a/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/CreateDashboardForm.tsx b/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/CreateDashboardForm.tsx index 9c74d3d..3f6ae83 100644 --- a/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/CreateDashboardForm.tsx +++ b/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/CreateDashboardForm.tsx @@ -1,10 +1,11 @@ import { useForm } from 'react-hook-form'; import Button from '@/components/Button'; import DashboardInput from '@/components/DashboardInput'; -import styles from './CreateDashboardForm.module.css'; -import { CreateDashboardRequestBody } from '@/types/dashboards'; +import type { CreateDashboardRequestBody } from '@/types/dashboards'; import { createDashboard } from '@/lib/boardService'; import { useRouter } from 'next/navigation'; +import styles from './CreateDashboardForm.module.css'; +import useTriggerStore from '@/store/triggerStore'; interface CreateDashboardFormProps { closeModal: () => void; @@ -13,6 +14,8 @@ interface CreateDashboardFormProps { export default function CreateDashboardForm({ closeModal, }: CreateDashboardFormProps) { + const { updateTrigger } = useTriggerStore(); + const { register, handleSubmit, @@ -23,6 +26,7 @@ export default function CreateDashboardForm({ const onSubmit = async (newDashboard: CreateDashboardRequestBody) => { const response = await createDashboard(newDashboard); closeModal(); + updateTrigger.dashboard(); router.push(`/dashboard/${response.id}`); }; diff --git a/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/DashboardCard.tsx b/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/DashboardCard.tsx index 9fc5d3a..681ad4c 100644 --- a/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/DashboardCard.tsx +++ b/src/app/(with-header-sidebar)/mydashboard/_components/dashboards/DashboardCard.tsx @@ -3,6 +3,7 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import Button from '@/components/Button'; import styles from './DashboardCard.module.css'; +import useCardStore from '@/store/cardStore'; export default function DashboardCard({ id, @@ -12,11 +13,13 @@ export default function DashboardCard({ }: Dashboard) { const router = useRouter(); + const handleClick = () => { + useCardStore.getState().clearCards(); + router.push(`/dashboard/${id}`); + }; + return ( - + )} +
+
+ ); +} + +export default function Toast() { + const { toasts, removeToast } = useToastStore(); + + return ( +
+ {toasts.map((toast) => ( + + ))} +
+ ); +} diff --git a/src/components/card/ColumnLabel.module.css b/src/components/card/ColumnLabel.module.css index d3b51b3..bddb91d 100644 --- a/src/components/card/ColumnLabel.module.css +++ b/src/components/card/ColumnLabel.module.css @@ -8,4 +8,5 @@ display: flex; gap: 6px; padding: 4px 10px; + white-space: nowrap; } diff --git a/src/components/header/DashboardMembers.module.css b/src/components/header/DashboardMembers.module.css index a93dd07..dbad012 100644 --- a/src/components/header/DashboardMembers.module.css +++ b/src/components/header/DashboardMembers.module.css @@ -2,6 +2,7 @@ display: flex; align-items: center; gap: 0; + transform: translateX(5px); } .avatar, @@ -13,3 +14,9 @@ color: #d25b68; background: #f4d7da; } + +@media screen and (min-width: 768px) { + .avatarWrapper { + transform: none; + } +} diff --git a/src/components/header/DashboardMembers.tsx b/src/components/header/DashboardMembers.tsx index 025d8ce..40e5de1 100644 --- a/src/components/header/DashboardMembers.tsx +++ b/src/components/header/DashboardMembers.tsx @@ -15,12 +15,12 @@ export default function DashboardMembers() { const dashboard = useDashboardStore((state) => state.dashboard); const { isMobile } = useWindowSize(); - const { members, totalPages } = useMember( + const { members, totalCount } = useMember( dashboard?.id.toString() || null, MEMBERS_VIEW_COUNT.desktop ); - if (totalPages === 0) { + if (totalCount === 0) { return null; } @@ -31,7 +31,7 @@ export default function DashboardMembers() { return (
{members - .slice(0, totalPages > maxViewCount ? maxViewCount - 1 : maxViewCount) + .slice(0, totalCount > maxViewCount ? maxViewCount - 1 : maxViewCount) .map(({ id, nickname, profileImageUrl }) => ( ))} - {totalPages > maxViewCount && ( + {totalCount > maxViewCount && ( )} diff --git a/src/components/header/InvitationButton.module.css b/src/components/header/InvitationButton.module.css index fb2450d..0a195fb 100644 --- a/src/components/header/InvitationButton.module.css +++ b/src/components/header/InvitationButton.module.css @@ -15,3 +15,13 @@ .button.button:hover { background-color: var(--violet-light); } + +.icon { + display: none; +} + +@media screen and (min-width: 768px) { + .icon { + display: inline-block; + } +} diff --git a/src/components/header/UserSection.module.css b/src/components/header/UserSection.module.css index b012e5b..38bf1a4 100644 --- a/src/components/header/UserSection.module.css +++ b/src/components/header/UserSection.module.css @@ -20,7 +20,8 @@ .myMenu { position: absolute; top: 75px; - right: 0; + right: 10px; + z-index: 999; } @media screen and (min-width: 768px) { diff --git a/src/components/header/UserSection.tsx b/src/components/header/UserSection.tsx index 5199fea..c12c393 100644 --- a/src/components/header/UserSection.tsx +++ b/src/components/header/UserSection.tsx @@ -1,17 +1,22 @@ +import { useRef } from 'react'; import UserInfo from './UserInfo'; import Button from '../Button'; import type { Menu } from '@/types/menu'; import { useMenu } from '@/hooks/useMenu'; import { useRouter } from 'next/navigation'; import MenuDropdown from '../MenuDropdown'; -import styles from './UserSection.module.css'; import useMe from '@/hooks/useMe'; +import useClickOutside from '@/hooks/useClickOutside'; +import styles from './UserSection.module.css'; export default function UserSection() { const router = useRouter(); const { clearUser } = useMe(); const { isMenuVisible, toggleMenu, closeMenu } = useMenu(); + const containerRef = useRef(null); + useClickOutside(containerRef, closeMenu); + const navigateTo = (href: string) => { router.push(href); closeMenu(); @@ -19,7 +24,9 @@ export default function UserSection() { const handleLogout = () => { clearUser(); - router.replace('/'); + setTimeout(() => { + router.replace('/'); + }, 0); // 비동기적 push 호출 보장 }; const myInfoMenus: Menu[] = [ @@ -29,7 +36,7 @@ export default function UserSection() { ]; return ( -
+
diff --git a/src/components/header/skeleton/UserInfoSkeleton.module.css b/src/components/header/skeleton/UserInfoSkeleton.module.css index 4afea0f..7b0fcef 100644 --- a/src/components/header/skeleton/UserInfoSkeleton.module.css +++ b/src/components/header/skeleton/UserInfoSkeleton.module.css @@ -8,6 +8,34 @@ background-color: var(--gray-300); } +.userIcon::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 50%; + height: 100%; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0.6) 50%, + rgba(255, 255, 255, 0.2) 100% + ); + animation: shine 1.5s infinite; +} + +@keyframes shine { + 0% { + left: -100%; + } + 50% { + left: 100%; + } + 100% { + left: -100%; + } +} + @media screen and (min-width: 768px) { .userInfo { display: flex; diff --git a/src/components/landing/Header/Header.module.css b/src/components/landing/Header/Header.module.css index 47580e9..6df6ddc 100644 --- a/src/components/landing/Header/Header.module.css +++ b/src/components/landing/Header/Header.module.css @@ -8,13 +8,17 @@ z-index: 999; width: 100%; height: 60px; - padding: 18px 24px; - + padding: 16px 24px; background-color: inherit; } -.logo { - fill: white; +.logoSymbol { + width: 24px; + height: 27px; +} + +.logoTypo { + display: none; } .nav { @@ -34,6 +38,23 @@ height: 70px; } + .logoWrapper { + display: flex; + justify-content: center; + align-items: center; + } + + .logoSymbol { + width: 29px; + height: 33px; + } + + .logoTypo { + display: inline; + width: 80px; + height: 22px; + } + .navLink { font-size: 16px; line-height: 26px; diff --git a/src/components/landing/Header/Header.tsx b/src/components/landing/Header/Header.tsx index eff6a82..30046a1 100644 --- a/src/components/landing/Header/Header.tsx +++ b/src/components/landing/Header/Header.tsx @@ -1,15 +1,51 @@ +'use client'; + import Link from 'next/link'; -import CustomLogo from '@/components/CustomLogo'; +import { CustomSymbol, CustomTypo } from '@/components/CustomLogo'; +import { useEffect, useState } from 'react'; import styles from './Header.module.css'; export default function Header() { + const [scroll, setScroll] = useState(0); + + useEffect(() => { + const handleScroll = () => { + setScroll(window.scrollY); + }; + + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + const getGradientColor = () => { + const scrollPercent = Math.min(scroll / window.innerHeight, 1); + + const red = Math.floor(255 - (255 - 85) * scrollPercent); + const green = Math.floor(255 - (255 - 52) * scrollPercent); + const blue = Math.floor(255 - (255 - 218) * scrollPercent); + + const color = `rgb(${red}, ${green}, ${blue})`; + + return color; + }; + return (
- +
+ + +
+