diff --git a/package-lock.json b/package-lock.json index 9f2ce64..91ee26d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "es-toolkit": "^1.32.0", "lenis": "^1.1.20", "lottie-react": "^2.4.1", @@ -1897,6 +1898,11 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index d4b3b35..786d931 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "es-toolkit": "^1.32.0", "lenis": "^1.1.20", "lottie-react": "^2.4.1", diff --git a/src/apis/cards/index.ts b/src/apis/cards/index.ts new file mode 100644 index 0000000..2bbc558 --- /dev/null +++ b/src/apis/cards/index.ts @@ -0,0 +1,36 @@ +import axiosClientHelper from '@/utils/network/axiosClientHelper'; +import { Card, CardForm, cardSchema, CardsResponse, cardsResponseSchema, GetCardsParams } from './types'; + +const RESPONSE_INVALID_MESSAGE = '서버에서 받은 데이터가 예상과 다릅니다'; + +export const postCard = async (cardForm: CardForm) => { + const response = await axiosClientHelper.post('/cards', cardForm); + const result = cardSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const getCards = async (params: GetCardsParams) => { + const response = await axiosClientHelper.get('/cards', { params }); + const result = cardsResponseSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const putCard = async (cardId: number, cardForm: CardForm) => { + const response = await axiosClientHelper.put(`/cards/${cardId}`, cardForm); + const result = cardSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const getCard = async (cardId: number) => { + const response = await axiosClientHelper.get(`/cards/${cardId}`); + const result = cardSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const deleteCard = async (cardId: number) => { + await axiosClientHelper.delete(`/cards/${cardId}`); +}; diff --git a/src/apis/cards/types.ts b/src/apis/cards/types.ts new file mode 100644 index 0000000..893e9a9 --- /dev/null +++ b/src/apis/cards/types.ts @@ -0,0 +1,57 @@ +import isValidDate from '@/utils/isValidDate'; +import { z } from 'zod'; + +const IMAGE_URL = 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/taskify/task_image/'; + +const cardBaseSchema = z.object({ + dashboardId: z.number(), + columnId: z.number(), + title: z.string(), + description: z.string(), + dueDate: z.string().refine((date) => isValidDate(date), { + message: '유효하지 않은 날짜 형식입니다.', + }), + tags: z.array(z.string()), + imageUrl: z + .string() + .url() + .refine((val) => val.startsWith(IMAGE_URL), { + message: '올바르지 않은 이미지 경로입니다.', + }), +}); + +export const cardSchema = cardBaseSchema.extend({ + id: z.number(), + assignee: z.object({ + id: z.number(), + nickname: z.string(), + profileImageUrl: z.union([z.string(), z.null(), z.instanceof(URL)]), + }), + teamId: z.string(), + createdAt: z.union([z.string(), z.date()]), + updatedAt: z.union([z.string(), z.date()]), +}); + +export type Card = z.infer; + +export const cardFormSchema = cardBaseSchema.extend({ + assigneeUserId: z.number(), +}); + +export type CardForm = z.infer; + +export const cardsResponseSchema = z.object({ + cursorId: z.number().nullable(), + totalCount: z.number(), + cards: z.array(cardSchema), +}); + +export type CardsResponse = z.infer; + +export const getCardsParamsSchema = z.object({ + size: z.number().optional(), + cursorId: z.number().optional(), + columnId: z.number(), +}); + +export type GetCardsParams = z.infer; diff --git a/src/apis/columns/index.ts b/src/apis/columns/index.ts new file mode 100644 index 0000000..f1795db --- /dev/null +++ b/src/apis/columns/index.ts @@ -0,0 +1,47 @@ +import axiosClientHelper from '@/utils/network/axiosClientHelper'; +import { CardImageForm, CardImageResponse, cardImageResponseSchema, Column, ColumnForm, columnSchema, ColumnsResponse, columnsResponseSchema, GetColumnsParams } from './types'; + +const RESPONSE_INVALID_MESSAGE = '서버에서 받은 데이터가 예상과 다릅니다'; + +export const postColumn = async (dashboardId: number, columnForm: ColumnForm) => { + const response = await axiosClientHelper.post('/columns', { + ...columnForm, + dashboardId, + }); + const result = columnSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const getColumns = async (params: GetColumnsParams) => { + const response = await axiosClientHelper.get('/columns', { + params, + }); + + const result = columnsResponseSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const putColumn = async (columnId: number, columnForm: ColumnForm) => { + const response = await axiosClientHelper.put(`/columns/${columnId}`, columnForm); + const result = columnSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const deleteColumn = async (columnId: number) => { + await axiosClientHelper.delete(`/columns/${columnId}`); +}; + +export const postCardImage = async (columnId: number, cardImageForm: CardImageForm) => { + const response = await axiosClientHelper.post(`/columns/${columnId}/card-image`, cardImageForm, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const result = cardImageResponseSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; diff --git a/src/apis/columns/types.ts b/src/apis/columns/types.ts new file mode 100644 index 0000000..d44fc3d --- /dev/null +++ b/src/apis/columns/types.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +export const columnSchema = z.object({ + id: z.number(), + title: z.string(), + teamId: z.string(), + dashboardId: z.number(), + createdAt: z.union([z.string(), z.instanceof(URL)]), + updatedAt: z.union([z.string(), z.instanceof(URL)]), +}); + +export type Column = z.infer; + +export const columnsResponseSchema = z.object({ + result: z.string(), + data: z.array(columnSchema), +}); + +export type ColumnsResponse = z.infer; + +export const getColumnsParamsSchema = z.object({ + dashboardId: z.number(), +}); + +export type GetColumnsParams = z.infer; + +export const columnFormSchema = z.object({ + title: z.string(), +}); + +export type ColumnForm = z.infer; + +export const cardImageFormSchema = z.object({ + image: z.instanceof(File), +}); + +export type CardImageForm = z.infer; + +export const cardImageResponseSchema = z.object({ + imageUrl: z.union([z.string(), z.instanceof(URL)]), +}); + +export type CardImageResponse = z.infer; diff --git a/src/apis/comments/index.ts b/src/apis/comments/index.ts new file mode 100644 index 0000000..11f6057 --- /dev/null +++ b/src/apis/comments/index.ts @@ -0,0 +1,29 @@ +import axiosClientHelper from '@/utils/network/axiosClientHelper'; +import { Comment, CommentForm, commentSchema, CommentsResponse, commentsResponseSchema, GetCommentsParams, PutCommentForm } from './types'; + +const RESPONSE_INVALID_MESSAGE = '서버에서 받은 데이터가 예상과 다릅니다'; + +export const postComment = async (commentForm: CommentForm) => { + const response = await axiosClientHelper.post('/comments', commentForm); + const result = commentSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const getComments = async (params: GetCommentsParams) => { + const response = await axiosClientHelper.get('/comments', { params }); + const result = commentsResponseSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const putComment = async (commentId: number, putCommentForm: PutCommentForm) => { + const response = await axiosClientHelper.put(`/comments/${commentId}`, putCommentForm); + const result = commentSchema.safeParse(response.data); + if (!result.success) throw new Error(RESPONSE_INVALID_MESSAGE); + return result.data; +}; + +export const deleteComment = async (commentId: number) => { + await axiosClientHelper.delete(`/comments/${commentId}`); +}; diff --git a/src/apis/comments/types.ts b/src/apis/comments/types.ts new file mode 100644 index 0000000..af623b4 --- /dev/null +++ b/src/apis/comments/types.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +export const commentSchema = z.object({ + id: z.number(), + content: z.string(), + createdAt: z.union([z.string(), z.date()]), + updatedAt: z.union([z.string(), z.date()]), + cardId: z.number(), + author: z.object({ + id: z.number(), + nickname: z.string(), + profileImageUrl: z.union([z.null(), z.string(), z.instanceof(URL)]), + }), +}); + +export type Comment = z.infer; + +export const commentFormSchema = z.object({ + content: z.string(), + cardId: z.number(), + columnId: z.number(), + dashboardId: z.number(), +}); + +export type CommentForm = z.infer; + +export const getCommentsParamsSchema = z.object({ + cursorId: z.number().optional(), + size: z.number().optional(), + cardId: z.number(), +}); + +export type GetCommentsParams = z.infer; + +export const commentsResponseSchema = z.object({ + cursorId: z.number().nullable(), + comments: z.array(commentSchema), +}); + +export type CommentsResponse = z.infer; + +export const putCommentFormSchema = z.object({ + content: z.string(), +}); + +export type PutCommentForm = z.infer; diff --git a/src/utils/isValidDate.ts b/src/utils/isValidDate.ts new file mode 100644 index 0000000..5df336a --- /dev/null +++ b/src/utils/isValidDate.ts @@ -0,0 +1,13 @@ +import dayjs from 'dayjs'; + +const isValidDate = (date: string | Date) => { + const format = 'YYYY-MM-DD HH:mm'; + if (date instanceof Date) { + const formattedDate = dayjs(date).format(format); + return dayjs(formattedDate, format, true).isValid(); + } + + return dayjs(date, format, true).isValid(); +}; + +export default isValidDate;