diff --git a/src/apis/dashboards/index.ts b/src/apis/dashboards/index.ts index b56ed98..9e100e2 100644 --- a/src/apis/dashboards/index.ts +++ b/src/apis/dashboards/index.ts @@ -1,5 +1,16 @@ import axiosHelper from '@/utils/network/axiosHelper'; -import { BasePaginationParams, Dashboard, DashboardFormType, DashboardInvitation, DashboardInvitationResponse, DashboardsResponse, GetDashboardsParams, InviteDashboardType } from './types'; +import { + BasePaginationParams, + Dashboard, + DashboardFormType, + DashboardInvitation, + DashboardInvitationResponse, + dashboardSchema, + DashboardsResponse, + dashboardsResponseSchema, + GetDashboardsParams, + InviteDashboardType, +} from './types'; // dashboard 목록 조회 export const getDashboards = async ({ cursorId, page, size, navigationMethod }: GetDashboardsParams) => { @@ -11,25 +22,45 @@ export const getDashboards = async ({ cursorId, page, size, navigationMethod }: navigationMethod, }, }); - return response.data; + + const result = dashboardsResponseSchema.safeParse(response.data); + if (!result.success) { + throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); + } + return result.data; }; // dashboard 생성 export const createDashboard = async (data: DashboardFormType) => { const response = await axiosHelper.post('/dashboards', data); - return response.data; + + const result = dashboardSchema.safeParse(response.data); + if (!result.success) { + throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); + } + return result.data; }; // dashboard 상세 조회 export const getDashboardDetails = async (id: number) => { const response = await axiosHelper.get(`/dashboards/${id}`); - return response.data; + + const result = dashboardSchema.safeParse(response.data); + if (!result.success) { + throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); + } + return result.data; }; // dashboard 수정 export const updateDashboard = async (id: number, data: DashboardFormType) => { const response = await axiosHelper.put(`/dashboards/${id}`, data); - return response.data; + + const result = dashboardSchema.safeParse(response.data); + if (!result.success) { + throw new Error('서버에서 받은 데이터가 예상과 다릅니다.'); + } + return result.data; }; // dashboard 삭제 @@ -38,6 +69,7 @@ export const deleteDashboard = async (id: number) => { return response.data; }; +// TODO : UserSchema 추가이후, Invitation schema가 작성되면 응답 검증 로직 추가 필요 // dashboard 초대 불러오기 export const getDashboardInvitations = async (id: number, { page, size }: BasePaginationParams) => { const response = await axiosHelper.get(`/dashboards/${id}/invitations`, { @@ -49,6 +81,7 @@ export const getDashboardInvitations = async (id: number, { page, size }: BasePa return response.data; }; +// TODO : UserSchema 추가이후, Invitation schema가 작성되면 응답 검증 로직 추가 필요 // dashboard 초대 export const inviteDashboard = async (id: number, data: InviteDashboardType) => { const response = await axiosHelper.post(`/dashboards/${id}/invitations`, data); diff --git a/src/apis/dashboards/queries.ts b/src/apis/dashboards/queries.ts new file mode 100644 index 0000000..b8d0bf2 --- /dev/null +++ b/src/apis/dashboards/queries.ts @@ -0,0 +1,42 @@ +'use client'; + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { createDashboard, getDashboards } from '.'; +import { DashboardFormType } from './types'; + +export const useDashboardsQuery = (page: number, size: number) => { + return useQuery({ + queryKey: ['dashboards', page, size], + queryFn: () => + getDashboards({ + page, + size, + navigationMethod: 'pagination', + }), + }); +}; + +export const useDashboardMutation = () => { + const queryClient = useQueryClient(); + + const create = useMutation({ + mutationFn: (data: DashboardFormType) => { + return createDashboard(data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['dashboards'] }); + }, + }); + + // TODO : update query 작성 + const update = () => {}; + + // TODO : remove query 작성 + const remove = () => {}; + + return { + create: create.mutateAsync, + update, + remove, + }; +}; diff --git a/src/apis/dashboards/types.ts b/src/apis/dashboards/types.ts index 63143fb..2677d3f 100644 --- a/src/apis/dashboards/types.ts +++ b/src/apis/dashboards/types.ts @@ -1,44 +1,53 @@ import { z } from 'zod'; import { User } from '../users/types'; +import { DASHBOARD_FORM_ERROR_MESSAGE, DASHBOARD_FORM_VALID_LENGTH } from '@/constants/dashboard'; +import { DEFAULT_COLORS } from '@/constants/colors'; // base pagination params 타입 (필요시 공용으로 추출) -export type BasePaginationParams = { - page?: number; - size?: number; -}; +export const basePaginationParamsSchema = z.object({ + page: z.number().optional(), + size: z.number().optional(), +}); +export type BasePaginationParams = z.infer; // dashboard 항목 타입 -export type Dashboard = { - id: number; - title: string; - color: string; - createdAt: string; - updatedAt: string; - createdByMe: boolean; - userId: number; -}; +export const dashboardSchema = z.object({ + id: z.number(), + title: z.string(), + color: z.enum(DEFAULT_COLORS).catch(DEFAULT_COLORS[0]), + createdAt: z.string(), + updatedAt: z.string(), + createdByMe: z.boolean(), + userId: z.number(), +}); +export type Dashboard = z.infer; // dashboard 리스트 응답 타입 -export type DashboardsResponse = { - cursorId: number; - totalCount: number; - dashboards: Dashboard[]; -}; +export const dashboardsResponseSchema = z.object({ + cursorId: z.number().nullable(), + totalCount: z.number(), + dashboards: z.array(dashboardSchema), +}); +export type DashboardsResponse = z.infer; // dashboard get params 타입 -export type NavigationMethod = 'infiniteScroll' | 'pagination'; -export type GetDashboardsParams = BasePaginationParams & { - cursorId?: number; - navigationMethod: NavigationMethod; -}; +export const getDashboardsParamsSchema = basePaginationParamsSchema.extend({ + cursorId: z.number().optional(), + navigationMethod: z.enum(['infiniteScroll', 'pagination']), +}); +export type GetDashboardsParams = z.infer; // dashboard 작성 스키마 export const dashboardFormSchema = z.object({ - title: z.string(), + title: z + .string() + .min(DASHBOARD_FORM_VALID_LENGTH.TITLE.MIN, { message: DASHBOARD_FORM_ERROR_MESSAGE.TITLE.MIN }) + .max(DASHBOARD_FORM_VALID_LENGTH.TITLE.MAX, { message: DASHBOARD_FORM_ERROR_MESSAGE.TITLE.MAX }), color: z.string(), }); export type DashboardFormType = z.infer; +// TODO : UserSchema가 추가로 작성되면 zod schema로 변경 // invitation 타입 export type InvitationUser = Pick; export type InvitationDashboard = Pick; @@ -61,6 +70,6 @@ export type DashboardInvitationResponse = { // invitation 스키마 export const inviteDashboardFormSchema = z.object({ - email: z.string(), + email: z.string().email({ message: DASHBOARD_FORM_ERROR_MESSAGE.EMAIL.INVALID }), }); export type InviteDashboardType = z.infer; diff --git a/src/app/(dashboard)/dashboard/[id]/page.tsx b/src/app/(dashboard)/dashboard/[id]/page.tsx new file mode 100644 index 0000000..ac7992d --- /dev/null +++ b/src/app/(dashboard)/dashboard/[id]/page.tsx @@ -0,0 +1,4 @@ +export default async function DashboardDetailPage({ params }: { params: Promise<{ id: string }> }) { + const id = (await params).id; + return
아이디 {id} : 대시보드 상세페이지
; +} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 2d13c51..3cfdb1e 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from 'react'; import Header from '@/components/dashboard-header/Header'; -import Sidebar from '@/components/Sidebar/Sidebar'; +import Sidebar from '@/components/dashboard/Sidebar'; export default function layout({ children }: PropsWithChildren) { return ( diff --git a/src/app/(dashboard)/mydashboard/page.tsx b/src/app/(dashboard)/mydashboard/page.tsx index c37ab02..639d9b5 100644 --- a/src/app/(dashboard)/mydashboard/page.tsx +++ b/src/app/(dashboard)/mydashboard/page.tsx @@ -1,16 +1,15 @@ -'use client'; - +import MyDashboard from '@/components/dashboard/MyDashboard'; import InvitedDashboardCard from '@/components/invited-dashboard/InvitedDashboardCard'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; - -const queryClient = new QueryClient(); -export default function page() { +export default function MydashboardPage() { return ( -
- - - +
+
+
+ + +
+
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 343ea91..8c76329 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import localFont from 'next/font/local'; import { Montserrat } from 'next/font/google'; import { DialogContainer } from '@/components/ui/Modal/DialogContainer'; +import QueryClientProvider from '@/components/provider/QueryProvider'; import './globals.css'; const pretendard = localFont({ @@ -24,8 +25,10 @@ export default function RootLayout({ return ( - {children} - + + {children} + + ); diff --git a/src/components/Sidebar/Dot.tsx b/src/components/Sidebar/Dot.tsx deleted file mode 100644 index 86d8cb1..0000000 --- a/src/components/Sidebar/Dot.tsx +++ /dev/null @@ -1,32 +0,0 @@ -interface DotProps { - colorClass: 'green-30' | 'blue-20' | 'orange-20' | 'purple' | 'pink-20'; -} - -export default function Dot({ colorClass }: DotProps) { - let className = ''; - switch (colorClass) { - case 'green-30': - className = 'text-green-30'; - break; - case 'blue-20': - className = 'text-blue-20'; - break; - case 'orange-20': - className = 'text-orange-20'; - break; - case 'pink-20': - className = 'text-pink-20'; - break; - case 'purple': - className = 'text-purple'; - break; - default: - className = 'text-gray-50'; - } - - return ( - - - - ); -} diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx deleted file mode 100644 index 47e2b5e..0000000 --- a/src/components/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import Image from 'next/image'; - -import { UseChunkPagination } from '@/hooks/useChunkPagination'; -import PaginationControls from '../pagination/PaginationControls'; -import SidebarLogo from './SidebarLogo'; -import SidebarItemList from './SidebarItemList'; -import add_box from '@/assets/icons/add_box.svg'; - -// TODO : 실제 api 요청으로 변경 -import { mockDashboardData } from '@/apis/dashboards/mockData'; - -interface ItemList { - id: number; - title: string; - color: 'green-30' | 'blue-20' | 'orange-20' | 'purple' | 'pink-20'; - createdByMe: boolean; -} - -const dashboardsForSidebar: ItemList[] = mockDashboardData.dashboards.map((dash) => { - return { - id: dash.id, - title: dash.title, - color: dash.color as ItemList['color'], - createdByMe: dash.createdByMe, - }; -}); - -export default function Sidebar() { - const { currentGroups, totalPages, canGoPrev, canGoNext, handlePrev, handleNext } = UseChunkPagination({ - items: dashboardsForSidebar, - chunkSize: 5, - maxGroupsPerPage: 3, - }); - - return ( - - ); -} diff --git a/src/components/Sidebar/SidebarItemList.tsx b/src/components/Sidebar/SidebarItemList.tsx deleted file mode 100644 index a1b0117..0000000 --- a/src/components/Sidebar/SidebarItemList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import MyDashboardCard from '../mydashboard-card/MyDashboardCard'; - -interface ItemList { - id: number; - title: string; - color: 'green-30' | 'blue-20' | 'orange-20' | 'purple' | 'pink-20'; - createdByMe: boolean | undefined; -} - -interface SidebarItemListProps { - currentGroups: ItemList[][]; -} - -export default function SidebarItemList({ currentGroups }: SidebarItemListProps) { - return ( -
- {currentGroups.map((group, groupIndex) => ( -
- {group.map((item) => ( - - ))} -
- ))} -
- ); -} diff --git a/src/components/dashboard/CreateDashboard.tsx b/src/components/dashboard/CreateDashboard.tsx new file mode 100644 index 0000000..376683d --- /dev/null +++ b/src/components/dashboard/CreateDashboard.tsx @@ -0,0 +1,87 @@ +import { forwardRef } from 'react'; +import { useRouter } from 'next/navigation'; +import { isAxiosError } from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import useAlert from '@/hooks/useAlert'; +import { Modal, ModalContent, ModalFooter, ModalHandle, ModalHeader } from '@/components/ui/Modal/Modal'; +import Button from '@/components/ui/Button/Button'; +import ColorPicker from '@/components/ui/Chip/ColorPicker'; +import { useDashboardMutation } from '@/apis/dashboards/queries'; +import { dashboardFormSchema, DashboardFormType } from '@/apis/dashboards/types'; +import { DEFAULT_COLORS } from '@/constants/colors'; + +const CreateDashboard = forwardRef((props, ref) => { + const { + handleSubmit, + register, + control, + reset, + formState: { errors, isValid, isSubmitting }, + } = useForm({ + resolver: zodResolver(dashboardFormSchema), + mode: 'onBlur', + defaultValues: { + title: '', + color: DEFAULT_COLORS[0], + }, + }); + const { create } = useDashboardMutation(); + const alert = useAlert(); + const router = useRouter(); + + const handleReset = () => { + reset(); + if (ref && 'current' in ref) { + ref.current?.close(); + } + }; + + const onSubmit = async (formData: DashboardFormType) => { + try { + const { id } = await create(formData); + handleReset(); + router.push(`/dashboard/${id}`); + } catch (error) { + alert(isAxiosError(error) ? error.message : '문제가 발생했습니다.'); + } + }; + + return ( + + +
+ 새로운 대시보드 +
+ {/* TODO : 공용 Field 컴포넌트 개발후 교체 필요 */} + +
{errors.title?.message}
+ + { + return field.onChange(value)} />; + }} + /> +
+ + + + +
+
+
+ ); +}); + +CreateDashboard.displayName = 'CreateDashboard'; + +export default CreateDashboard; diff --git a/src/components/dashboard/MyDashboard.tsx b/src/components/dashboard/MyDashboard.tsx new file mode 100644 index 0000000..7dafd41 --- /dev/null +++ b/src/components/dashboard/MyDashboard.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useRef } from 'react'; +import { ModalHandle } from '@/components/ui/Modal/Modal'; +import CreateDashboard from './CreateDashboard'; +import MyDashboardList from './MyDashboardList'; + +export default function MyDashboard() { + const addDashboardModalRef = useRef(null); + + return ( + <> + addDashboardModalRef.current?.open()} /> + + + ); +} diff --git a/src/components/mydashboard-card/MyDashboardCard.tsx b/src/components/dashboard/MyDashboardCard.tsx similarity index 85% rename from src/components/mydashboard-card/MyDashboardCard.tsx rename to src/components/dashboard/MyDashboardCard.tsx index 49d705f..54bc9b2 100644 --- a/src/components/mydashboard-card/MyDashboardCard.tsx +++ b/src/components/dashboard/MyDashboardCard.tsx @@ -1,12 +1,13 @@ import { cn } from '@/utils/helper'; -import Dot from '../Sidebar/Dot'; +import Dot from '@/components/ui/Dot/Dot'; import Image from 'next/image'; import crown from '@/assets/icons/crown.svg'; import right_arrow from '@/assets/icons/right_arrow.svg'; +import { DEFAULT_COLOR } from '@/constants/colors'; interface MyDashboardCardProps { title: string; - color: 'green-30' | 'blue-20' | 'orange-20' | 'purple' | 'pink-20'; + color: DEFAULT_COLOR; createdByMe: boolean | undefined; variant?: 'sidebar' | 'card'; } @@ -15,7 +16,7 @@ export default function MyDashboardCard({ title, color, createdByMe, variant }: const containerClass = cn( 'flex rounded-md ', variant === 'sidebar' && 'justify-center gap-2.5 p-3 md:justify-start md:hover:bg-violet-10', - variant === 'card' && 'items-center justify-between border border-gray-30 hover:shadow-md px-5 py-[22px] w-[260px] md:w-[247px] lg:w-[332px]', + variant === 'card' && 'items-center justify-between border border-gray-30 hover:shadow-md p-5 bg-white h-[58px] md:h-[70px]', ); const textContainerClass = cn(variant === 'sidebar' ? 'hidden md:flex gap-1.5' : 'flex gap-1.5'); @@ -24,7 +25,7 @@ export default function MyDashboardCard({ title, color, createdByMe, variant }: return (
- +
{title} {createdByMe && crown} diff --git a/src/components/dashboard/MyDashboardList.tsx b/src/components/dashboard/MyDashboardList.tsx new file mode 100644 index 0000000..0663ae2 --- /dev/null +++ b/src/components/dashboard/MyDashboardList.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import PaginationControls from '@/components/pagination/PaginationControls'; +import MyDashboardCard from '@/components/dashboard/MyDashboardCard'; +import plusIcon from '@/assets/icons/plus.svg'; +import { useDashboardsQuery } from '@/apis/dashboards/queries'; + +interface MyDashboardListProps { + onAdd: () => void; +} + +const ITEMS_PER_PAGE = 5; + +export default function MyDashboardList({ onAdd }: MyDashboardListProps) { + const [page, setPage] = useState(1); + const { data, isLoading } = useDashboardsQuery(page, ITEMS_PER_PAGE); + + const totalCount = data?.totalCount || 0; + const totalPage = Math.ceil(totalCount / ITEMS_PER_PAGE); + const hasNext = page < totalPage; + const hasPrev = page > 1; + + const handlePrev = () => { + setPage((prev) => prev - 1); + }; + const handleNext = () => { + setPage((prev) => prev + 1); + }; + + return ( +
+
    +
  • + +
  • + + {isLoading && [...Array(5)].map((item, index) => )} + + {data?.dashboards.map((item) => ( +
  • + + + +
  • + ))} +
+ + {data?.totalCount && ( +
+ + {totalPage} 페이지중 {page} + + +
+ )} +
+ ); +} + +export function SkeletionItem() { + return ( +
  • +
    + +
    +
  • + ); +} diff --git a/src/components/dashboard/Sidebar.tsx b/src/components/dashboard/Sidebar.tsx new file mode 100644 index 0000000..2269a32 --- /dev/null +++ b/src/components/dashboard/Sidebar.tsx @@ -0,0 +1,15 @@ +'use client'; + +import SidebarDashboardList from '@/components/dashboard/SidebarDashboardList'; +import SidebarLogo from '@/components/dashboard/SidebarLogo'; +import SidebarHeader from '@/components/dashboard/SidebarHeader'; + +export default function Sidebar() { + return ( + + ); +} diff --git a/src/components/dashboard/SidebarDashboardList.tsx b/src/components/dashboard/SidebarDashboardList.tsx new file mode 100644 index 0000000..32586e1 --- /dev/null +++ b/src/components/dashboard/SidebarDashboardList.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import PaginationControls from '@/components/pagination/PaginationControls'; +import MyDashboardCard from '@/components/dashboard/MyDashboardCard'; +import { useDashboardsQuery } from '@/apis/dashboards/queries'; + +const ITEMS_PER_PAGE = 15; + +export default function SidebarDashboardList() { + const [page, setPage] = useState(1); + const { data, isLoading } = useDashboardsQuery(page, ITEMS_PER_PAGE); + + const totalCount = data?.totalCount || 0; + const totalPage = Math.ceil(totalCount / ITEMS_PER_PAGE); + const hasNext = page < totalPage; + const hasPrev = page > 1; + + const handlePrev = () => { + setPage((prev) => prev - 1); + }; + const handleNext = () => { + setPage((prev) => prev + 1); + }; + + return ( +
    +
    +
      + {isLoading && [...Array(8)].map((item, index) => )} + + {data?.dashboards.map((item) => ( +
    • + + + +
    • + ))} +
    +
    + + +
    + ); +} + +export function SkeletionItem() { + return ( +
  • +
    + +
    +
  • + ); +} diff --git a/src/components/dashboard/SidebarHeader.tsx b/src/components/dashboard/SidebarHeader.tsx new file mode 100644 index 0000000..9854b1b --- /dev/null +++ b/src/components/dashboard/SidebarHeader.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { useRef } from 'react'; +import Image from 'next/image'; +import CreateDashboard from '@/components/dashboard/CreateDashboard'; +import { ModalHandle } from '@/components/ui/Modal/Modal'; +import add_box from '@/assets/icons/add_box.svg'; + +export default function SidebarHeader() { + const addDashboardModalRef = useRef(null); + + return ( + <> +
    +

    Dash Boards

    + +
    + + + + ); +} diff --git a/src/components/Sidebar/SidebarLogo.tsx b/src/components/dashboard/SidebarLogo.tsx similarity index 74% rename from src/components/Sidebar/SidebarLogo.tsx rename to src/components/dashboard/SidebarLogo.tsx index 1b1e4b4..788c038 100644 --- a/src/components/Sidebar/SidebarLogo.tsx +++ b/src/components/dashboard/SidebarLogo.tsx @@ -1,13 +1,13 @@ import Image from 'next/image'; import logo from '@/assets/images/sidebar_logo.png'; import logo_ci from '@/assets/images/logo_ci.png'; +import Link from 'next/link'; export default function SidebarLogo() { return ( - <> + logo - logo - + ); } diff --git a/src/components/pagination/PaginationControls.tsx b/src/components/pagination/PaginationControls.tsx index 9deffa9..bd7fdc7 100644 --- a/src/components/pagination/PaginationControls.tsx +++ b/src/components/pagination/PaginationControls.tsx @@ -38,11 +38,11 @@ export default function PaginationControls({ canGoPrev, canGoNext, handlePrev, h return (
    - -
    diff --git a/src/components/provider/QueryProvider.tsx b/src/components/provider/QueryProvider.tsx new file mode 100644 index 0000000..c150378 --- /dev/null +++ b/src/components/provider/QueryProvider.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { QueryClientProvider as Provider, QueryClient } from '@tanstack/react-query'; +import { PropsWithChildren } from 'react'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1분 + }, + }, +}); + +export default function QueryClientProvider({ children }: PropsWithChildren) { + return {children}; +} diff --git a/src/components/ui/Chip/ColorChip.tsx b/src/components/ui/Chip/ColorChip.tsx index f813ceb..77c863f 100644 --- a/src/components/ui/Chip/ColorChip.tsx +++ b/src/components/ui/Chip/ColorChip.tsx @@ -1,9 +1,9 @@ -import { HexColor } from '@/constants/colors'; +import { DEFAULT_COLOR } from '@/constants/colors'; import { cn } from '@/utils/helper'; import { HTMLAttributes } from 'react'; interface ColorChipProps extends HTMLAttributes { - color: HexColor; + color: DEFAULT_COLOR; } export default function ColorChip({ color, className, ...props }: ColorChipProps) { diff --git a/src/components/ui/Dot/Dot.tsx b/src/components/ui/Dot/Dot.tsx new file mode 100644 index 0000000..389e9da --- /dev/null +++ b/src/components/ui/Dot/Dot.tsx @@ -0,0 +1,13 @@ +import { DEFAULT_COLOR } from '@/constants/colors'; + +interface DotProps { + color: DEFAULT_COLOR; +} + +export default function Dot({ color }: DotProps) { + return ( + + + + ); +} diff --git a/src/constants/colors.ts b/src/constants/colors.ts index ce2d01a..55856af 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -1,3 +1,4 @@ export type HexColor = `#${string}`; -export const DEFAULT_COLORS: HexColor[] = ['#7AC555', '#760DDE', '#FFA500', '#76A5EA', '#E876EA']; +export const DEFAULT_COLORS = ['#7AC555', '#760DDE', '#FFA500', '#76A5EA', '#E876EA'] as const satisfies HexColor[]; +export type DEFAULT_COLOR = (typeof DEFAULT_COLORS)[number]; diff --git a/src/constants/dashboard.ts b/src/constants/dashboard.ts new file mode 100644 index 0000000..e304b01 --- /dev/null +++ b/src/constants/dashboard.ts @@ -0,0 +1,16 @@ +export const DASHBOARD_FORM_VALID_LENGTH = { + TITLE: { + MIN: 2, + MAX: 10, + }, +} as const; + +export const DASHBOARD_FORM_ERROR_MESSAGE = { + TITLE: { + MIN: '2자 이상 입력해 주세요', + MAX: '10자 이하로 작성해 주세요', + }, + EMAIL: { + INVALID: '이메일 형식으로 작성해 주세요', + }, +} as const; diff --git a/src/utils/helper.ts b/src/utils/helper.ts index b4de34b..a4d9ef3 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -5,7 +5,7 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function getColorByString(value: string, colorArray: string[]) { +export function getColorByString(value: string, colorArray: readonly string[]) { const charCode = value.toLowerCase().charCodeAt(0); const index = charCode % colorArray.length; return colorArray[index];