diff --git a/apps/auth/app/constants/api.ts b/apps/auth/app/constants/api.ts index 6b845674..1c250768 100644 --- a/apps/auth/app/constants/api.ts +++ b/apps/auth/app/constants/api.ts @@ -1,8 +1,8 @@ -export const API_BASE_URL = 'https://api.clab.page'; +export const API_BASE_URL = 'https://api.clab.page/api'; export const END_POINTS = { - LOGIN: '/login', - TWO_FACTOR_LOGIN: '/login/authenticator', + LOGIN: '/v1/login', + TWO_FACTOR_LOGIN: '/v1/login/authenticator', } as const; export const SUCCESS_MESSAGE = { @@ -11,7 +11,7 @@ export const SUCCESS_MESSAGE = { export const ERROR_MESSAGE = { AUTH: '인증에 실패했습니다.', - SERVER: '오류가 발생했습니다. 잠시 후 다시 시도해주세요.', + SERVER: '로그인에 실패했습니다. 입력 정보를 다시 확인해주세요.', } as const; // OAuth 백엔드가 개발 전까지 임시로 사용합니다 diff --git a/apps/member/.env.example b/apps/member/.env.example index dae8ee51..4c5e525b 100644 --- a/apps/member/.env.example +++ b/apps/member/.env.example @@ -1,2 +1,3 @@ VITE_MODE=환경모드 +VITE_SERVER_BASE_URL=서버주소 VITE_API_BASE_URL=API주소 \ No newline at end of file diff --git a/apps/member/.env.production b/apps/member/.env.production index f9c51fd6..3f88990e 100644 --- a/apps/member/.env.production +++ b/apps/member/.env.production @@ -1,2 +1,3 @@ VITE_MODE=production -VITE_API_BASE_URL=https://api.clab.page \ No newline at end of file +VITE_SERVER_BASE_URL=https://api.clab.page/ +VITE_API_BASE_URL=https://api.clab.page/api/ \ No newline at end of file diff --git a/apps/member/member.config.json b/apps/member/member.config.json new file mode 100644 index 00000000..0490bdf7 --- /dev/null +++ b/apps/member/member.config.json @@ -0,0 +1,3 @@ +{ + "name": "C-Lab" +} diff --git a/apps/member/package.json b/apps/member/package.json index ceb97604..c4163fc9 100644 --- a/apps/member/package.json +++ b/apps/member/package.json @@ -13,13 +13,17 @@ "@gwansikk/server-chain": "^0.5.2", "@tanstack/react-query": "^5.18.1", "classnames": "^2.5.1", + "clsx": "^2.1.0", "dayjs": "^1.11.10", + "entities": "^4.5.0", "framer-motion": "^10.18.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", "react-icons": "^5.0.1", "react-router-dom": "^6.21.2", - "recoil": "^0.7.7" + "recoil": "^0.7.7", + "tailwind-merge": "^2.2.2" }, "devDependencies": { "@clab/design-system": "workspace:^", diff --git a/apps/member/src/api/activity.ts b/apps/member/src/api/activity.ts index 56d86108..41a54f48 100644 --- a/apps/member/src/api/activity.ts +++ b/apps/member/src/api/activity.ts @@ -7,6 +7,7 @@ import type { ActivityBoardType, ActivityGroupBoardParserType, ActivityGroupItem, + ActivityGroupMemberMyType, ActivityGroupStatusType, ActivityPhotoItem, ActivityRequestType, @@ -43,17 +44,19 @@ interface PatchActivityBoardArgs { files?: FormData; } -// 나의 활동 일정 조회 +/** + * 나의 활동 일정 조회 + */ export const getMyActivities = async ( - startDateTime: string, - endDateTime: string, + startDate: string, + endDate: string, page: number, size: number, ) => { const { data } = await server.get>({ url: createCommonPagination(END_POINT.MY_ACTIVITY, { - startDateTime, - endDateTime, + startDate, + endDate, page, size, }), @@ -61,8 +64,9 @@ export const getMyActivities = async ( return data; }; - -//활동 사진 조회 +/** + * 활동 사진 조회 + */ export const getActivityPhoto = async (page: number, size: number) => { const { data } = await server.get>({ url: createCommonPagination(END_POINT.MAIN_ACTIVITY_PHOTO, { page, size }), @@ -70,8 +74,9 @@ export const getActivityPhoto = async (page: number, size: number) => { return data; }; - -// 활동 상태별 조회 +/** + * 활동 상태별 조회 + */ export const getActivityGroupByStatus = async ( activityGroupStatus: ActivityGroupStatusType, page: number, @@ -87,8 +92,9 @@ export const getActivityGroupByStatus = async ( return data; }; - -// 활동 상세 조회 +/** + * 활동 상세 조회 + */ export const getActivityGroupDetail = async (id: number) => { const { data } = await server.get>( { @@ -102,8 +108,9 @@ export const getActivityGroupDetail = async (id: number) => { return data; }; - -// 활동 신청 +/** + * 활동 신청 + */ export const postActivityGroupMemberApply = async ({ activityGroupId, body, @@ -119,8 +126,9 @@ export const postActivityGroupMemberApply = async ({ return data; }; - -// 상태별 활동 멤버 조회 +/** + * 상태별 활동 멤버 조회 + */ export const getActivityGroupApplyByStatus = async ( activityGroupId: number, page: number, @@ -136,8 +144,22 @@ export const getActivityGroupApplyByStatus = async ( return data; }; +/** + * 나의 활동 목록 조회 + */ +export const getActivityGroupMemberMy = async (page: number, size: number) => { + const { data } = await server.get>({ + url: createCommonPagination(END_POINT.ACTIVITY_GROUP_MEMBER_MY, { + page, + size, + }), + }); -// 활동 신청 상태 + return data; +}; +/** + * 활동 신청 상태 + */ export const patchActivityGroupMemberApply = async ({ activityGroupId, memberId, @@ -150,8 +172,9 @@ export const patchActivityGroupMemberApply = async ({ return data; }; - -// 게시판 단일 조회 +/** + * 게시판 단일 조회 + */ export const getActivityBoard = async (activityGroupBoardId: number) => { const { data } = await server.get>({ url: createCommonPagination(END_POINT.ACTIVITY_GROUP_BOARDS, { @@ -161,8 +184,9 @@ export const getActivityBoard = async (activityGroupBoardId: number) => { return data; }; - -// 나의 과제 제출 게시판 조회 +/** + * 나의 과제 제출 게시판 조회 + */ export const getActivityBoardsMyAssignment = async (parentId: number) => { const { data } = await server.get>({ url: createCommonPagination(END_POINT.ACTIVITY_GROUP_BOARDS_MY_ASSIGNMENT, { @@ -172,8 +196,9 @@ export const getActivityBoardsMyAssignment = async (parentId: number) => { return data; }; - -// 활동 그룹 게시판 생성 +/** + * 활동 그룹 게시판 생성 + */ export const postActivityBoard = async ({ parentId, memberId, @@ -214,8 +239,9 @@ export const postActivityBoard = async ({ return data; }; - -// 활동 그룹 게시판 수정 +/** + * 활동 그룹 게시판 수정 + */ export const patchActivityBoard = async ({ activityGroupBoardId, groupId, diff --git a/apps/member/src/api/blog.ts b/apps/member/src/api/blog.ts index 0169139c..02403e1a 100644 --- a/apps/member/src/api/blog.ts +++ b/apps/member/src/api/blog.ts @@ -1,14 +1,25 @@ -import { PaginationType } from '@type/api'; +import { BaseResponse, PaginationType } from '@type/api'; import { server } from './server'; import { createCommonPagination } from '@utils/api'; import { END_POINT } from '@constants/api'; -import type { BlogPostItem } from '@type/blog'; - -// 블로그 게시글 조회 -export const getMyBlog = async (page: number, size: number) => { +import type { BlogPost } from '@type/blog'; +/** + * 블로그 포스트 조회 + */ +export const getBlog = async (page: number, size: number) => { const params = { page, size }; - const { data } = await server.get>({ - url: createCommonPagination(END_POINT.MY_BLOG, params), + const { data } = await server.get>({ + url: createCommonPagination(END_POINT.BLOG, params), + }); + + return data; +}; +/** + * 블로그 포스트 상세 조회 + */ +export const getBlogDetail = async (id: number) => { + const { data } = await server.get>({ + url: END_POINT.BLOG_DETAIL(id), }); return data; diff --git a/apps/member/src/api/board.ts b/apps/member/src/api/board.ts index 80edae84..844e07f6 100644 --- a/apps/member/src/api/board.ts +++ b/apps/member/src/api/board.ts @@ -1,21 +1,23 @@ import { BaseResponse, PaginationType } from '@type/api'; import { server } from './server'; import { END_POINT } from '@constants/api'; -import type { BoardItem } from '@type/board'; import { createCommonPagination } from '@utils/api'; +import type { BoardItem } from '@type/board'; import type { - CommunityCategoryKorType, + CommunityCategoryType, CommunityPostDetailItem, CommunityWriteItem, } from '@type/community'; import type { PostItem } from '@type/post'; -interface PatchBoardsArgs { - id: string; +interface PatchBoardsParams { + id: number; body: CommunityWriteItem; } -// 내가 작성한 커뮤니티 게시글 조회 +/** + * 내가 작성한 커뮤니티 게시글 조회 + */ export const getMyBoards = async (page: number, size: number) => { const params = { page, size }; const { data } = await server.get>({ @@ -24,32 +26,51 @@ export const getMyBoards = async (page: number, size: number) => { return data; }; +/** + * 커뮤니티 게시글 목록 조회 + */ +export const getBoards = async (page: number, size: number) => { + const { data } = await server.get>({ + url: createCommonPagination(END_POINT.BOARDS, { page, size }), + }); -// 커뮤니티 게시글 카테고리별 조회 + return data; +}; +/** + * 커뮤니티 게시글 카테고리별 조회 + */ export const getBoardsList = async ( - category: CommunityCategoryKorType, + category: CommunityCategoryType, page: number, size: number, ) => { - const params = { category, page, size }; const { data } = await server.get>({ - url: createCommonPagination(END_POINT.BOARDS_LIST, params), + url: createCommonPagination(END_POINT.BOARDS_LIST, { + category: category.toUpperCase(), + page, + size, + }), }); return data; }; - -// 커뮤니티 게시글 작성 +/** + * 커뮤니티 게시글 작성 + */ export const postBoardsWrite = async (body: CommunityWriteItem) => { const { data } = await server.post>({ url: END_POINT.BOARDS, - body, + body: { + ...body, + category: body.category.toUpperCase(), + }, }); return data; }; - -// 커뮤니티 게시글 상세 조회 +/** + * 커뮤니티 게시글 상세 조회 + */ export const getBoardsDetail = async (id: string) => { const { data } = await server.get>({ url: END_POINT.BOARDERS_ITEM(id), @@ -57,12 +78,16 @@ export const getBoardsDetail = async (id: string) => { return data; }; - -// 커뮤니티 게시글 수정 -export const patchBoards = async ({ id, body }: PatchBoardsArgs) => { +/** + * 커뮤니티 게시글 수정 + */ +export const patchBoards = async ({ id, body }: PatchBoardsParams) => { const { data } = await server.patch({ url: END_POINT.BOARDERS_ITEM(id), - body, + body: { + ...body, + category: body.category.toUpperCase(), + }, }); return data; diff --git a/apps/member/src/api/book.ts b/apps/member/src/api/book.ts index bacddf30..0b80556e 100644 --- a/apps/member/src/api/book.ts +++ b/apps/member/src/api/book.ts @@ -1,13 +1,26 @@ import { server } from './server'; import { createCommonPagination } from '@utils/api'; import { END_POINT } from '@constants/api'; -import type { BookItem, BookLoanRecordItem } from '@type/book'; -import type { BaseResponse, PaginationType } from '@type/api'; +import type { + BookItem, + BookLoanRecordConditionType, + BookLoanRecordItem, +} from '@type/book'; +import type { + BaseResponse, + PaginationPramsType, + PaginationType, +} from '@type/api'; interface PostBorrowBookArgs extends BookLoanRecordItem { memberId: string; } +interface GetBookLoanRecordConditionsPrams extends PaginationPramsType { + bookId?: number; + borrowerId?: string; + isReturned?: boolean; +} /** * 도서 목록 조회 */ @@ -19,7 +32,6 @@ export const getBooks = async (page: number, size: number) => { return data; }; - /** * 도서 상세 조회 */ @@ -30,7 +42,6 @@ export const getBookDetail = async (id: number) => { return data; }; - /** * 나의 대출내역 조회 */ @@ -42,7 +53,6 @@ export const getMyBooks = async (id: string, page: number, size: number) => { return data.items.filter((book) => book.borrowerId === id); }; - /** * 도서 대출 */ @@ -55,7 +65,6 @@ export const postBorrowBook = async (body: PostBorrowBookArgs) => { return { memberId: body.memberId, bookId: body.bookId, data }; }; - /** * 도서 반납 */ @@ -67,7 +76,6 @@ export const postReturnBook = async (body: BookLoanRecordItem) => { return { memberId: body.borrowerId, bookId: body.bookId, data }; }; - /** * 도서 연장 */ @@ -79,18 +87,26 @@ export const postExtendBook = async (body: BookLoanRecordItem) => { return { memberId: body.borrowerId, bookId: data }; }; - /** - * 도서 대출 내역 검색 + * 도서 대출 내역 조회 */ -export const getBookLoanByMemberId = async ( - borrowerId: string, +export const getBookLoanRecordConditions = async ({ + bookId, + borrowerId, + isReturned, page = 0, size = 20, -) => { - const params = { borrowerId, page, size }; - const { data } = await server.get>({ - url: createCommonPagination(END_POINT.BOOK_LOAN_SEARCH, params), +}: GetBookLoanRecordConditionsPrams) => { + const { data } = await server.get< + PaginationType + >({ + url: createCommonPagination(END_POINT.BOOK_LOAN_CONDITIONS, { + bookId, + borrowerId, + isReturned, + page, + size, + }), }); return data; diff --git a/apps/member/src/api/comment.ts b/apps/member/src/api/comment.ts index 5686162e..050ebdac 100644 --- a/apps/member/src/api/comment.ts +++ b/apps/member/src/api/comment.ts @@ -13,18 +13,22 @@ interface commentWriteArgs { boardId: string; body: CommentWriteItem; } - -// 나의 댓글 조회 +/** + * 나의 댓글 조회 + */ export const getMyComments = async (page: number, size: number) => { - const params = { page, size }; const { data } = await server.get>({ - url: createCommonPagination(END_POINT.MY_COMMENTS, params), + url: createCommonPagination(END_POINT.MY_COMMENTS, { + page, + size, + }), }); return data; }; - -// 댓글 목록 조회 +/** + * 댓글 목록 조회 + */ export const getCommentList = async ( id: string, page: number, @@ -37,20 +41,19 @@ export const getCommentList = async ( return data; }; - -// 댓글 작성 +/** + * 댓글 작성 + */ export const postCommentWrite = async ({ parentId, boardId, body, }: commentWriteArgs) => { - let url = createPath(END_POINT.COMMENTS(boardId)); - if (parentId) { - url += `?parentId=${parentId}`; - } - const { data } = await server.post({ - url, + url: createPath( + END_POINT.COMMENTS(boardId), + parentId && `?parentId=${parentId}`, + ), body, }); diff --git a/apps/member/src/api/member.ts b/apps/member/src/api/member.ts index a0b43807..0a1469fe 100644 --- a/apps/member/src/api/member.ts +++ b/apps/member/src/api/member.ts @@ -1,44 +1,43 @@ import { server } from './server'; import { END_POINT } from '@constants/api'; -import type { BaseResponse } from '@type/api'; -import type { ProfileData } from '@type/profile'; import { postUploadedFileProfileImage } from './uploadedFile'; +import type { BaseResponse } from '@type/api'; +import type { MemberProfileRequestType, MemberProfileType } from '@type/member'; interface PatchUserInfoArgs { id: string; - body: ProfileData; + body: MemberProfileRequestType; multipartFile: FormData | null; } -// 내 정보 +/** + * 내 프로필 조회 + */ export const getMyProfile = async () => { - const { data } = await server.get>({ + const { data } = await server.get>({ url: END_POINT.MY_PROFILE, }); return data; }; - -// 내 정보 수정 +/** + * 멤버 정보 수정 + */ export const patchUserInfo = async ({ id, body, multipartFile, }: PatchUserInfoArgs) => { - let userInfoData; if (multipartFile) { const data = await postUploadedFileProfileImage(multipartFile); - - userInfoData = { - ...body, - imageUrl: data.fileUrl, - }; - } else { - userInfoData = body; + body['imageUrl'] = data.fileUrl; } - const { data } = await server.patch>({ + const { data } = await server.patch< + MemberProfileRequestType, + BaseResponse + >({ url: END_POINT.MY_INFO_EDIT(id), - body: userInfoData, + body, }); return data; diff --git a/apps/member/src/api/membershipFee.ts b/apps/member/src/api/membershipFee.ts index 8a8ab0fa..aa8e03e4 100644 --- a/apps/member/src/api/membershipFee.ts +++ b/apps/member/src/api/membershipFee.ts @@ -2,44 +2,65 @@ import { server } from './server'; import { END_POINT } from '@constants/api'; import { createCommonPagination } from '@utils/api'; import { postUploadedFileMembershipFee } from './uploadedFile'; -import type { ArgsWithFiles, BaseResponse, PaginationType } from '@type/api'; -import type { MembershipFeeType } from '@type/membershipFee'; +import type { + BaseResponse, + PaginationType, + PaginationPramsType, + ArgsWithFiles, +} from '@type/api'; +import type { + MembershipFeeRequestType, + MembershipFeeType, +} from '@type/membershipFee'; -interface postMembershipFeeArgs extends ArgsWithFiles { - body: MembershipFeeType; +interface GetMembershipFeeParamsType extends PaginationPramsType { + memberId?: string; + memberName?: string; + category?: string; } +interface PostMembershipFeePramsType extends ArgsWithFiles { + body: MembershipFeeRequestType; +} /** - * 회비 신청 정보 조회 + * 회비 정보 조회 */ -export const getMembershipFee = async (page: number, size: number) => { - const params = { page, size }; +export const getMembershipFee = async ({ + memberId, + memberName, + category, + page, + size, +}: GetMembershipFeeParamsType) => { const { data } = await server.get>({ - url: createCommonPagination(END_POINT.MEMBERSHIP_FEE, params), + url: createCommonPagination(END_POINT.MEMBERSHIP_FEE, { + memberId, + memberName, + category, + page, + size, + }), }); + return data; }; - /** * 회비 신청 */ export const postMembershipFee = async ({ body, multipartFile, -}: postMembershipFeeArgs) => { +}: PostMembershipFeePramsType) => { if (multipartFile) { const data = await postUploadedFileMembershipFee({ storagePeriod: 365, multipartFile: multipartFile, }); - body['imageUrl'] = data[0].fileUrl; } - const { data } = await server.post>({ + return await server.post>({ url: END_POINT.MEMBERSHIP_FEE, body: body, }); - - return data; }; diff --git a/apps/member/src/api/schedule.ts b/apps/member/src/api/schedule.ts index ffa45722..ac153804 100644 --- a/apps/member/src/api/schedule.ts +++ b/apps/member/src/api/schedule.ts @@ -1,25 +1,52 @@ -import { BaseResponse, PaginationType } from '@type/api'; import { server } from './server'; import { END_POINT } from '@constants/api'; import { createCommonPagination } from '@utils/api'; -import type { ScheduleItem, ScheduleRegisterItem } from '@type/schedule'; +import type { + BaseResponse, + PaginationPramsType, + PaginationType, +} from '@type/api'; +import type { + ScheduleCollect, + ScheduleItem, + ScheduleRegisterItem, +} from '@type/schedule'; -// 일정 조회 -export const getSchedule = async ( - startDateTime: string, - endDateTime: string, - page: number, - size: number, -) => { - const params = { startDateTime, endDateTime, page, size }; +interface GetScheduleParams extends PaginationPramsType { + startDate: string; + endDate: string; + page: number; + size: number; +} +/** + * 일정 조회 + */ +export const getSchedule = async ({ + startDate, + endDate, + page, + size, +}: GetScheduleParams) => { + const params = { startDate, endDate, page, size }; const { data } = await server.get>({ url: createCommonPagination(END_POINT.MAIN_SCHEDULE, params), }); return data; }; +/** + * 일정 모아보기 + */ +export const getScheduleCollect = async () => { + const { data } = await server.get>({ + url: END_POINT.SCHEDULE_COLLECT, + }); -// 일정 추가 + return data; +}; +/** + * 일정 등록 + */ export const postSchedule = async (body: ScheduleRegisterItem) => { const { data } = await server.post< ScheduleRegisterItem, diff --git a/apps/member/src/assets/support/SupportIcons.tsx b/apps/member/src/assets/support/SupportIcons.tsx deleted file mode 100644 index a93ec886..00000000 --- a/apps/member/src/assets/support/SupportIcons.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import classNames from 'classnames'; -import { ReactNode } from 'react'; - -interface SupportIconsProps { - className?: string; - children: ReactNode; -} - -const SupportIcons = ({ className, children }: SupportIconsProps) => { - return
{children}
; -}; - -SupportIcons.check = () => ( - - - -); - -SupportIcons.checking = () => ( - - - -); - -SupportIcons.document = () => ( - - - -); - -SupportIcons.next = () => ( - - - -); - -export default SupportIcons; diff --git a/apps/member/src/assets/svg/yes24.svg b/apps/member/src/assets/svg/yes24.svg new file mode 100644 index 00000000..3a88f611 --- /dev/null +++ b/apps/member/src/assets/svg/yes24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/member/src/assets/webp/aladin.webp b/apps/member/src/assets/webp/aladin.webp new file mode 100644 index 00000000..b34d7a3d Binary files /dev/null and b/apps/member/src/assets/webp/aladin.webp differ diff --git a/apps/member/src/assets/webp/kyobobook.webp b/apps/member/src/assets/webp/kyobobook.webp new file mode 100644 index 00000000..62efb42e Binary files /dev/null and b/apps/member/src/assets/webp/kyobobook.webp differ diff --git a/apps/member/src/components/calendar/Calendar/Calendar.tsx b/apps/member/src/components/calendar/Calendar/Calendar.tsx deleted file mode 100644 index d29b51a5..00000000 --- a/apps/member/src/components/calendar/Calendar/Calendar.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import dayjs from 'dayjs'; -import classNames from 'classnames'; -import CalendarSchedule from '../CalendarSchedule/CalendarSchedule'; -import { now, transformEvents } from '@utils/date'; -import { useSchedule } from '@hooks/queries/useSchedule'; -import { DATE_FORMAT } from '@constants/state'; - -interface CalendarProps { - date: dayjs.Dayjs; -} - -const Calendar = ({ date }: CalendarProps) => { - const dateElements = []; - const year = date.year(); - const month = date.month() + 1; - const prevDaysInMonth = date.subtract(1, 'month').daysInMonth(); - const toDaysInMonth = date.daysInMonth(); - - const { data } = useSchedule({ - start: date.startOf('month').format(DATE_FORMAT.WITH_TIME), - end: date.endOf('month').format(DATE_FORMAT.WITH_TIME), - }); - - const events = transformEvents(data.items); - - //전 달 날짜 - for (let i = date.day(); i >= 0; i--) { - const day = date.subtract(1, 'month'); - dateElements.push( -
-

{prevDaysInMonth - i}

-
, - ); - } - - //이번 달 날짜 - for (let i = 1; i <= toDaysInMonth; i++) { - const day = dayjs(`${year}-${month}-${i}`); - const element = day.format('YYYY-MM-DD'); - const event = events[element]; - dateElements.push( -
-

- {i} -

- -
, - ); - } - - const nextDaysInMonth = 7 - (dateElements.length % 7); - - //다음 달 날짜 - for (let i = 1; i <= nextDaysInMonth; i++) { - const day = date.add(1, 'month'); - dateElements.push( -
-

{i}

-
, - ); - } - - return dateElements; -}; - -export default Calendar; diff --git a/apps/member/src/components/calendar/CalendarSchedule/CalendarSchedule.tsx b/apps/member/src/components/calendar/CalendarSchedule/CalendarSchedule.tsx index 8eab441a..50ea169a 100644 --- a/apps/member/src/components/calendar/CalendarSchedule/CalendarSchedule.tsx +++ b/apps/member/src/components/calendar/CalendarSchedule/CalendarSchedule.tsx @@ -1,36 +1,64 @@ import useModal from '@hooks/common/useModal'; import { formattedDate } from '@utils/date'; import type { ScheduleItem } from '@type/schedule'; +import { useCallback } from 'react'; +import { cn } from '@utils/string'; +import dayjs from 'dayjs'; + +interface CalendarScheduleProps extends ScheduleItem { + day: dayjs.Dayjs; +} const CalendarSchedule = ({ + day, title, detail, startDate, endDate, -}: ScheduleItem) => { +}: CalendarScheduleProps) => { const { openModal } = useModal(); + const isDateDiff = dayjs(startDate).diff(endDate, 'd'); - const onClickSchedule = (detail: string, start: string, end: string) => { - let date = `${formattedDate(start)} ~ ${formattedDate(end)}`; - - if (start === end) { + const handleScheduleClick = useCallback( + (detail: string, start: string, end: string) => { // 시작일과 종료일이 같은 경우, 종료일은 표시하지 않는다. - date = `${formattedDate(start)}`; - } + const date = + start === end + ? `${formattedDate(start)}` + : `${formattedDate(start)} ~ ${formattedDate(end)}`; - openModal({ - title: '📆 일정', - content: `내용: ${detail}\n일시: ${date}`, - }); - }; + openModal({ + title: '📆 일정', + content: `일시: ${date}\n내용: ${detail}`, + }); + }, + [openModal], + ); return ( -

onClickSchedule(detail, startDate, endDate)} + ); }; diff --git a/apps/member/src/components/calendar/CalendarSection/CalendarSection.tsx b/apps/member/src/components/calendar/CalendarSection/CalendarSection.tsx new file mode 100644 index 00000000..d9e44f97 --- /dev/null +++ b/apps/member/src/components/calendar/CalendarSection/CalendarSection.tsx @@ -0,0 +1,99 @@ +import { startTransition, useCallback, useState } from 'react'; +import ArrowButton from '@components/common/ArrowButton/ArrowButton'; +import Section from '@components/common/Section/Section'; +import { useSchedule } from '@hooks/queries'; +import { now, transformEvents } from '@utils/date'; +import { cn } from '@utils/string'; +import CalendarSchedule from '../CalendarSchedule/CalendarSchedule'; + +const today = now(); + +const CalendarSection = () => { + const [date, setDate] = useState(today); + const { data } = useSchedule({ + startDate: date.startOf('month').format('YYYY-MM-DD'), + endDate: date.endOf('month').format('YYYY-MM-DD'), + }); + + const handleDateClick = useCallback((action: 'prev' | 'next' | 'today') => { + startTransition(() => { + setDate((current) => { + switch (action) { + case 'prev': + return current.subtract(1, 'month'); + case 'next': + return current.add(1, 'month'); + case 'today': + return today; + } + }); + }); + }, []); + + const events = transformEvents(data.items); + const startDay = date.startOf('month').startOf('week'); // 현재 월의 첫 날짜의 주의 시작일 + const endDay = date.endOf('month').endOf('week'); // 현재 월의 마지막 날짜의 주의 마지막일 + + const days = []; + let day = startDay; + + while (day.isBefore(endDay)) { + const isToday = day.isSame(today, 'day'); + + days.push( + +

+

+ {day.format('D')} +

+ {events[day.format('YYYY-MM-DD')]?.map((event) => ( + + ))} +
+ , + ); + day = day.add(1, 'day'); + } + + const weeks = []; + for (let i = 0; i < days.length; i += 7) { + weeks.push({days.slice(i, i + 7)}); + } + + return ( +
+ + handleDateClick('prev')} /> + + handleDateClick('next')} /> + + + + + + + + + + + + + + + {weeks} +
+
+
+ ); +}; + +export default CalendarSection; diff --git a/apps/member/src/components/calendar/CalendarStatusSection/CalendarStatusSection.tsx b/apps/member/src/components/calendar/CalendarStatusSection/CalendarStatusSection.tsx new file mode 100644 index 00000000..5db6c04d --- /dev/null +++ b/apps/member/src/components/calendar/CalendarStatusSection/CalendarStatusSection.tsx @@ -0,0 +1,49 @@ +import Section from '@components/common/Section/Section'; +import StatusCard from '@components/common/StatusCard/StatusCard'; +import { useSchedule } from '@hooks/queries'; +import { calculateDDay, findClosestEvent, transformEvents } from '@utils/date'; +import { FcCalendar, FcLeave, FcOvertime, FcAlarmClock } from 'react-icons/fc'; +import { useScheduleCollect } from '@hooks/queries/useScheduleCollect'; + +const CalendarStatusSection = () => { + const { data: yearData } = useScheduleCollect(); + const { data: monthData } = useSchedule(); + + const closestEvent = findClosestEvent(transformEvents(monthData.items)); + const closestDDay = closestEvent?.startDate + ? `D-${calculateDDay(closestEvent.startDate)}` + : '이번 달에 남은 일정이 없어요'; + + return ( +
+ + + } + label={`${yearData.totalScheduleCount}회`} + description="이번 연도 동아리의 모든 일정 횟수에요." + /> + } + label={`${yearData.totalEventCount}회`} + description="이번 연도 총회, MT 등 중요도가 높은 행사 횟수에요." + /> + } + label={`${monthData.totalItems}회`} + description="이번 달 동아리 일정 횟수에요." + /> + } + label={closestDDay} + description="가장 가까운 일정까지 남은 일수에요." + /> + +
+ ); +}; + +export default CalendarStatusSection; diff --git a/apps/member/src/components/common/ArrowButton/ArrowButton.tsx b/apps/member/src/components/common/ArrowButton/ArrowButton.tsx new file mode 100644 index 00000000..e8315f03 --- /dev/null +++ b/apps/member/src/components/common/ArrowButton/ArrowButton.tsx @@ -0,0 +1,26 @@ +import { cn } from '@utils/string'; +import { ComponentPropsWithRef, forwardRef } from 'react'; +import { MdOutlineNavigateNext } from 'react-icons/md'; + +interface ArrowButtonProps extends ComponentPropsWithRef<'button'> { + direction?: 'prev' | 'next'; +} + +const ArrowButton = forwardRef( + ({ direction = 'prev', className, ...rest }, ref) => { + return ( + + ); + }, +); + +export default ArrowButton; diff --git a/apps/member/src/components/common/Comment/Comment.tsx b/apps/member/src/components/common/Comment/Comment.tsx index 39c43901..2eb36494 100644 --- a/apps/member/src/components/common/Comment/Comment.tsx +++ b/apps/member/src/components/common/Comment/Comment.tsx @@ -1,23 +1,31 @@ import classNames from 'classnames'; import Image from '../Image/Image'; import { formattedDate } from '@utils/date'; +import { getProfileRingStyle } from '@utils/style'; +import { createImageUrl } from '@utils/api'; interface CommentProps { - isReply?: boolean; - writer: string; - image: string; + writerId: string | null; + writerName: string; + writerRoleLevel: number | null; + writerImageUrl: string | null; + createdAt: string; children: React.ReactNode; onClickReport: () => void; onClickReply: () => void; + isReply?: boolean; } const Comment = ({ - isReply = false, - writer, - image, + writerId, + writerName, + writerRoleLevel, + writerImageUrl, + createdAt, children, onClickReport, onClickReply, + isReply, }: CommentProps) => { return (
프로필사진 -
-

{writer}

-

{children}

-
-

{formattedDate('2021-11-22')}

+
+

{`${writerName} ${writerId ? `(${writerId})` : ''}`}

+

{children}

+
+

{formattedDate(createdAt)}

{!isReply && }
diff --git a/apps/member/src/components/common/CommentInput/CommentInput.tsx b/apps/member/src/components/common/CommentInput/CommentInput.tsx index 36cb7f84..3ee42b66 100644 --- a/apps/member/src/components/common/CommentInput/CommentInput.tsx +++ b/apps/member/src/components/common/CommentInput/CommentInput.tsx @@ -1,16 +1,24 @@ import { useState } from 'react'; import Textarea from '../Textarea/Textarea'; -import { Button, Input } from '@clab/design-system'; +import { Button, Checkbox } from '@clab/design-system'; import { useCommentWriteMutation } from '@hooks/queries/useCommentWriteMutation'; +import { cn } from '@utils/string'; interface CommentInputProps { id: string; value: string; onChange: (e: React.ChangeEvent) => void; parentId?: number; + className?: string; } -const CommentInput = ({ id, value, onChange, parentId }: CommentInputProps) => { +const CommentInput = ({ + id, + value, + onChange, + parentId, + className, +}: CommentInputProps) => { const { commentWriteInfo } = useCommentWriteMutation(); const [anonymous, setAnonymous] = useState(false); @@ -38,25 +46,22 @@ const CommentInput = ({ id, value, onChange, parentId }: CommentInputProps) => { }; return ( -
+