From 9c0620dd347f30cb13d128ce38a4d784eb73b37d Mon Sep 17 00:00:00 2001 From: Jong Date: Sun, 25 Aug 2024 16:31:45 +0900 Subject: [PATCH] feat: Home MVP v.2 (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 진행중인 질문 영역 * feat: 홈 이전 질문 * feat: 멤버 프로필 * feat: 홈 지면 SSR 적용 * chore: eslint fix * feat: 이전 질문 UI * feat: 홈 지면 마크업 * refactor: 리뷰 반영 * feat: api 연동 * refactor: home 공통 컴포넌트 components 밑으로 이동 * chore: gitignore 수정 * feat: 홈 지면 api 연동 * chore: tsconfig * feat: dialog 마크업 * chore: api 쿠키 셋 제거 * feat: api 연동 구체화 * feat: 알림 기능 구현 * feat: 질문 답변 지면 (#75) * feat: 답변 오프닝 UI * feat: 답변 클로징 UI * fix: type error fix * fix: 디자인 수정건 반영 * chore: temp * feat: 질문 답변 페이지 * chore: 릴레이 질문 CTA 버튼 노출 조건 수정 * fix: type error * feat: 질문 답변 영역 * feat: 홈지면 라우팅 연결 * chore: console 제거 * feat: 질문 답변 api 연동 * chore: eslint * fix: 버튼 Link 위계 변경 * fix: fix common name * chore: 환경 변수 변경 * fix: 릴레이 질문 도착 모달 버튼 수정 * feat: 질문 답변 기능 * feat: root 접근시 auth 로 Redirect * chore: tsconfig * feat: 홈지면 prefetch * feat: 질문 답변 지면 prefetch * feat: 질문 결과 라우팅 수정 * feat: 질문 답변후 invalidate * fix: 답변완료 비율 최대 100으로 한정 * feat: StaleTime 0 설정 * feat: 카톡 공유하기 * fix: 폴링 invalidate 제거 * feat: 답변완료시 답변페이지로 이동버튼 제거 * fix: meetings 스키마 변경 * fix: 질문 등록 버튼 노출 의존성 변경 * feat: home 네비게이터 아이콘 추가 * feat: 홈 내비게이션 탭 구현 * feat: 모임원 목록 검색 기능 * feat: 카카오톡 초대 모달 * feat: 마이 탭 화면 * feat: 알림탭 UI * feat: 모임 이름 선택 UI 추가 * feat: 홈, 알림, 질문, 마이 탭 UI * feat: 바텀싯 * chore: type error fix * feat: 서로 꼭찌르기 여부 조건 추가 * refactor: home Atoms namespace 정의 * fix: 서로 인사하기 이슈 수정 * fix: bottomsheet 하단 padding 추가 * feat: 모임원 목록 초대링크 공유 기능 * feat: 멀티 meeting ID * feat: 미팅 변경 대응 * feat: 알림 탭 * fix: 알림, 플로팅 버튼 버그 픽스 * refactor: 알림 탭 네이밍 변경 * fix: 질문 종료 공유 링크 수정 * fix: 질문 시작 meetingId params 추가 * fix: 유저 프로필 링크 변경 * fix: relative path about start question * fix: fix link question result * chore: lodash -> lodash-es * feat: 마이프로필 --- .../answer/[questionId]/comment/layout.tsx | 0 .../answer/[questionId]/comment/page.tsx | 0 .../answer/[questionId]/layout.tsx | 0 .../answer/[questionId]/page.tsx | 0 .../answer/closing/layout.tsx | 0 .../{ => [meetingId]}/answer/closing/page.tsx | 0 .../answer/opening/layout.tsx | 0 .../{ => [meetingId]}/answer/opening/page.tsx | 0 apps/web/app/home/me/modify/page.tsx | 8 ++ apps/web/app/home/me/page.tsx | 5 + apps/web/app/home/notification/page.tsx | 5 + apps/web/app/home/question/page.tsx | 5 + package.json | 2 + .../src/components/Icon/assets/AddMeeting.tsx | 21 ++++ .../src/components/Icon/assets/AddUser.tsx | 29 +++++ .../src/components/Icon/assets/BellIcon.tsx | 43 +++++++ .../src/components/Icon/assets/CheckIcon.tsx | 16 +++ .../src/components/Icon/assets/CloseIcon.tsx | 14 +++ .../components/Icon/assets/ConnectStar.tsx | 15 +++ .../components/Icon/assets/HandShaving.tsx | 14 +++ .../src/components/Icon/assets/HomeIcon.tsx | 47 ++++++++ .../components/Icon/assets/QuestionIcon.tsx | 54 +++++++++ .../src/components/Icon/assets/SearchIcon.tsx | 21 ++++ .../src/components/Icon/assets/UpAndDown.tsx | 17 +++ .../src/components/Icon/assets/UserIcon.tsx | 40 +++++++ .../core/sds/src/components/Icon/constants.ts | 30 ++++- .../apis/mutates/useUpdateQuestionsActive.ts | 2 +- .../common/apis/queries/useGetAnswersMe.ts | 5 +- .../common/apis/queries/useGetMember.ts | 2 - .../common/apis/queries/useGetMemberMe.ts | 5 +- .../about-me/features/containers/styles.ts | 2 +- .../containers/AnswerClosingContainer.tsx | 4 +- .../services/useAnswerClosingService.ts | 23 +++- .../services/useAnswerQuestionService.ts | 13 +-- .../comment/services/useCommentService.tsx | 27 +++-- .../components/StartButton.tsx | 5 +- .../ProgressingQuestionContainer.tsx | 6 +- .../useProgressingQuestionService.tsx | 12 +- .../answer/screens/AnswerOpeningScreen.tsx | 21 ++-- .../answer/screens/AnswerQuestionScreen.tsx | 24 ++-- .../common/components/ActionBar/ActionBar.tsx | 17 +-- .../src/common/utils/bodyScrollLock.ts | 46 ++++++++ .../src/common/utils/generateInviteLink.ts | 11 ++ .../src/common/utils/getCurrentMeeting.ts | 27 +++++ .../src/common/utils/getKeywordRegex.ts | 11 ++ .../mutations/useHandWavingIgnoreMutation.ts | 25 ++++ .../useHandWavingResponseMutation.ts | 25 ++++ .../apis/mutations/useUpdateLastMeeting.ts | 34 ++++++ .../home/common/apis/queries/useGetEvents.ts | 61 ++++++++++ .../common/apis/queries/useGetInviteCode.ts | 44 +++++++ .../queries/useGetPreviousQuestionList.ts | 110 +----------------- .../common/apis/schema/InviteCode.schema.ts | 5 + .../home/common/apis/schema/Meeting.schema.ts | 8 +- .../common/apis/schema/Notification.schema.ts | 21 +++- .../useGetProgressingQuestionQuery.type.ts | 1 + .../src/home/common/atoms/home.atom.ts | 18 ++- .../BottomSheet/BottomSheet.styles.ts | 38 ++++++ .../components/BottomSheet/BottomSheet.tsx | 89 ++++++++++++++ .../common/components/Layout/HomeLayout.tsx | 4 +- .../Layout/PreviousQuestionLayout.tsx | 2 + .../Navigation/HomeNavigationProvider.tsx | 3 + .../Navigation/HomeNavigationTab.tsx | 64 ++++++++++ .../home/common/hooks/useSetCurrentMeeting.ts | 62 ++++++++++ .../components/ProgressingQuestionModal.tsx | 38 ++++-- .../components/StartRelayQuestionButton.tsx | 1 + .../containers/FloatingButtonContainer.tsx | 16 ++- .../services/useFloatingButtonService.ts | 65 ++++++----- .../GatherMemberProfile.tsx | 58 +++++---- .../GatherMemberProfileList.tsx | 23 ++-- .../GatherMemberSearchInput.tsx | 85 ++++++++++++++ .../GatherMemberProfileListContainer.tsx | 47 ++++++-- .../useGatherMemberProfileListService.tsx | 77 +++++++++++- .../containers/HomeNavigationContainer.tsx | 38 ++++++ .../components/MeetingChoiceBottomSheet.tsx | 100 ++++++++++++++++ .../containers/MeetingIntroContainer.tsx | 35 ++++++ .../services/useMeetingIntroService.ts | 27 +++++ .../my-profile/components/AboutMe.tsx | 22 ++++ .../my-profile/components/AnswerQuestions.tsx | 90 ++++++++++++++ .../my-profile/components/MyProfile.tsx | 31 +++++ .../containers/MyprofileContainer.tsx | 69 +++++++++++ .../services/useMyprofileService.ts | 46 ++++++++ .../ArrivedQuestionNotification.tsx | 20 ++-- .../components/NotificationItem.tsx | 59 ++++++++++ .../components/NotificationList.tsx | 25 ++++ .../SelectedTargetMemberNotification.tsx | 16 ++- .../containers/AlarmListContainer.tsx | 78 +++++++++++++ .../containers/NotificationContainer.tsx | 4 +- .../services/useAlarmListService.ts | 33 ++++++ .../services/useNotificationService.ts | 29 ++--- .../HomePreviousQuestionItem.tsx | 18 +-- .../HomePreviousQuestionItemPlaceholder.tsx | 31 +++++ .../PreviousQuestion/PreviousQuestionItem.tsx | 4 +- .../PreviousQuestionListContainer.tsx | 6 +- .../TopPreviousQuestionListContainer.tsx | 37 +++++- .../usePreviousQuestionListService.tsx | 10 +- .../useTopPreviousQuestionListService.tsx | 13 ++- .../components/GatherName/GatherName.tsx | 37 +++--- .../QuestionInfo/ActiveQuestion.tsx | 5 +- .../ProgressingQuestionContainer.tsx | 36 +++++- .../useProgressingQuestionService.tsx | 86 ++++++++------ packages/web-domains/src/home/index.ts | 3 + .../src/home/screens/AlarmScreen.tsx | 38 ++++++ .../src/home/screens/HomeMeScreen.tsx | 44 +++++++ .../src/home/screens/HomeScreen.tsx | 33 ++---- .../home/screens/PreviousQuestionScreen.tsx | 2 +- .../src/home/screens/QuestionScreen.tsx | 51 ++++++++ pnpm-lock.yaml | 50 ++++++-- 107 files changed, 2458 insertions(+), 441 deletions(-) rename apps/web/app/{ => [meetingId]}/answer/[questionId]/comment/layout.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/[questionId]/comment/page.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/[questionId]/layout.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/[questionId]/page.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/closing/layout.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/closing/page.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/opening/layout.tsx (100%) rename apps/web/app/{ => [meetingId]}/answer/opening/page.tsx (100%) create mode 100644 apps/web/app/home/me/modify/page.tsx create mode 100644 apps/web/app/home/me/page.tsx create mode 100644 apps/web/app/home/notification/page.tsx create mode 100644 apps/web/app/home/question/page.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/AddMeeting.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/AddUser.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/BellIcon.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/CheckIcon.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/CloseIcon.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/ConnectStar.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/HandShaving.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/HomeIcon.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/QuestionIcon.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/SearchIcon.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/UpAndDown.tsx create mode 100644 packages/core/sds/src/components/Icon/assets/UserIcon.tsx create mode 100644 packages/web-domains/src/common/utils/bodyScrollLock.ts create mode 100644 packages/web-domains/src/common/utils/generateInviteLink.ts create mode 100644 packages/web-domains/src/common/utils/getCurrentMeeting.ts create mode 100644 packages/web-domains/src/common/utils/getKeywordRegex.ts create mode 100644 packages/web-domains/src/home/common/apis/mutations/useHandWavingIgnoreMutation.ts create mode 100644 packages/web-domains/src/home/common/apis/mutations/useHandWavingResponseMutation.ts create mode 100644 packages/web-domains/src/home/common/apis/mutations/useUpdateLastMeeting.ts create mode 100644 packages/web-domains/src/home/common/apis/queries/useGetEvents.ts create mode 100644 packages/web-domains/src/home/common/apis/queries/useGetInviteCode.ts create mode 100644 packages/web-domains/src/home/common/apis/schema/InviteCode.schema.ts create mode 100644 packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.styles.ts create mode 100644 packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.tsx create mode 100644 packages/web-domains/src/home/common/components/Navigation/HomeNavigationProvider.tsx create mode 100644 packages/web-domains/src/home/common/components/Navigation/HomeNavigationTab.tsx create mode 100644 packages/web-domains/src/home/common/hooks/useSetCurrentMeeting.ts create mode 100644 packages/web-domains/src/home/features/gather-member/components/GatherMemberSearch/GatherMemberSearchInput.tsx create mode 100644 packages/web-domains/src/home/features/home-navigation/containers/HomeNavigationContainer.tsx create mode 100644 packages/web-domains/src/home/features/meeting-choice/components/MeetingChoiceBottomSheet.tsx create mode 100644 packages/web-domains/src/home/features/meeting-intro/containers/MeetingIntroContainer.tsx create mode 100644 packages/web-domains/src/home/features/meeting-intro/services/useMeetingIntroService.ts create mode 100644 packages/web-domains/src/home/features/my-profile/components/AboutMe.tsx create mode 100644 packages/web-domains/src/home/features/my-profile/components/AnswerQuestions.tsx create mode 100644 packages/web-domains/src/home/features/my-profile/components/MyProfile.tsx create mode 100644 packages/web-domains/src/home/features/my-profile/containers/MyprofileContainer.tsx create mode 100644 packages/web-domains/src/home/features/my-profile/services/useMyprofileService.ts create mode 100644 packages/web-domains/src/home/features/notification/components/NotificationItem.tsx create mode 100644 packages/web-domains/src/home/features/notification/components/NotificationList.tsx create mode 100644 packages/web-domains/src/home/features/notification/containers/AlarmListContainer.tsx create mode 100644 packages/web-domains/src/home/features/notification/services/useAlarmListService.ts create mode 100644 packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItemPlaceholder.tsx create mode 100644 packages/web-domains/src/home/screens/AlarmScreen.tsx create mode 100644 packages/web-domains/src/home/screens/HomeMeScreen.tsx create mode 100644 packages/web-domains/src/home/screens/QuestionScreen.tsx diff --git a/apps/web/app/answer/[questionId]/comment/layout.tsx b/apps/web/app/[meetingId]/answer/[questionId]/comment/layout.tsx similarity index 100% rename from apps/web/app/answer/[questionId]/comment/layout.tsx rename to apps/web/app/[meetingId]/answer/[questionId]/comment/layout.tsx diff --git a/apps/web/app/answer/[questionId]/comment/page.tsx b/apps/web/app/[meetingId]/answer/[questionId]/comment/page.tsx similarity index 100% rename from apps/web/app/answer/[questionId]/comment/page.tsx rename to apps/web/app/[meetingId]/answer/[questionId]/comment/page.tsx diff --git a/apps/web/app/answer/[questionId]/layout.tsx b/apps/web/app/[meetingId]/answer/[questionId]/layout.tsx similarity index 100% rename from apps/web/app/answer/[questionId]/layout.tsx rename to apps/web/app/[meetingId]/answer/[questionId]/layout.tsx diff --git a/apps/web/app/answer/[questionId]/page.tsx b/apps/web/app/[meetingId]/answer/[questionId]/page.tsx similarity index 100% rename from apps/web/app/answer/[questionId]/page.tsx rename to apps/web/app/[meetingId]/answer/[questionId]/page.tsx diff --git a/apps/web/app/answer/closing/layout.tsx b/apps/web/app/[meetingId]/answer/closing/layout.tsx similarity index 100% rename from apps/web/app/answer/closing/layout.tsx rename to apps/web/app/[meetingId]/answer/closing/layout.tsx diff --git a/apps/web/app/answer/closing/page.tsx b/apps/web/app/[meetingId]/answer/closing/page.tsx similarity index 100% rename from apps/web/app/answer/closing/page.tsx rename to apps/web/app/[meetingId]/answer/closing/page.tsx diff --git a/apps/web/app/answer/opening/layout.tsx b/apps/web/app/[meetingId]/answer/opening/layout.tsx similarity index 100% rename from apps/web/app/answer/opening/layout.tsx rename to apps/web/app/[meetingId]/answer/opening/layout.tsx diff --git a/apps/web/app/answer/opening/page.tsx b/apps/web/app/[meetingId]/answer/opening/page.tsx similarity index 100% rename from apps/web/app/answer/opening/page.tsx rename to apps/web/app/[meetingId]/answer/opening/page.tsx diff --git a/apps/web/app/home/me/modify/page.tsx b/apps/web/app/home/me/modify/page.tsx new file mode 100644 index 00000000..18294edb --- /dev/null +++ b/apps/web/app/home/me/modify/page.tsx @@ -0,0 +1,8 @@ +import HomeMeScreen from '../page'; + +export const dynamic = 'force-dynamic'; + +const HomeMeModifyPage = () => { + return ; +}; +export default HomeMeModifyPage; diff --git a/apps/web/app/home/me/page.tsx b/apps/web/app/home/me/page.tsx new file mode 100644 index 00000000..dc353497 --- /dev/null +++ b/apps/web/app/home/me/page.tsx @@ -0,0 +1,5 @@ +import { HomeMeScreen } from '@/home'; + +export const dynamic = 'force-dynamic'; + +export default HomeMeScreen; diff --git a/apps/web/app/home/notification/page.tsx b/apps/web/app/home/notification/page.tsx new file mode 100644 index 00000000..ea79d88a --- /dev/null +++ b/apps/web/app/home/notification/page.tsx @@ -0,0 +1,5 @@ +import { AlarmScreen } from '@/home'; + +export const dynamic = 'force-dynamic'; + +export default AlarmScreen; diff --git a/apps/web/app/home/question/page.tsx b/apps/web/app/home/question/page.tsx new file mode 100644 index 00000000..5d192f13 --- /dev/null +++ b/apps/web/app/home/question/page.tsx @@ -0,0 +1,5 @@ +import { QuestionScreen } from '@sambad/web-domains/home'; + +export const dynamic = 'force-dynamic'; + +export default QuestionScreen; diff --git a/package.json b/package.json index 9bb76768..b8089407 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "@tanstack/react-query": "^5.50.1", "dayjs": "^1.11.12", "jotai": "^2.9.1", + "lodash-es": "^4.17.21", "next": "14.2.4", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@turbo/gen": "^1.12.4", + "@types/lodash-es": "^4.17.12", "cspell": "^8.10.0", "husky": "^9.0.11", "prettier": "^3.2.5", diff --git a/packages/core/sds/src/components/Icon/assets/AddMeeting.tsx b/packages/core/sds/src/components/Icon/assets/AddMeeting.tsx new file mode 100644 index 00000000..cb9cf46b --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/AddMeeting.tsx @@ -0,0 +1,21 @@ +import { IconAssetProps } from '../types'; + +export const AddMeeting = (props: IconAssetProps) => { + const { size } = props; + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/AddUser.tsx b/packages/core/sds/src/components/Icon/assets/AddUser.tsx new file mode 100644 index 00000000..5c00cb6e --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/AddUser.tsx @@ -0,0 +1,29 @@ +import { IconAssetProps } from '../types'; + +export const AddUser = (props: IconAssetProps) => { + const { size } = props; + + return ( + + + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/BellIcon.tsx b/packages/core/sds/src/components/Icon/assets/BellIcon.tsx new file mode 100644 index 00000000..606056f2 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/BellIcon.tsx @@ -0,0 +1,43 @@ +import { IconAssetProps } from '../types'; + +export const BellIcon = (props: IconAssetProps) => { + const { color = 'white', size = 24 } = props; + + if (color === 'black') { + return ( + + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/CheckIcon.tsx b/packages/core/sds/src/components/Icon/assets/CheckIcon.tsx new file mode 100644 index 00000000..96441c7d --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/CheckIcon.tsx @@ -0,0 +1,16 @@ +import { IconAssetProps } from '../types'; + +export const CheckIconMeet = (props: IconAssetProps) => { + const { size } = props; + + return ( + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/CloseIcon.tsx b/packages/core/sds/src/components/Icon/assets/CloseIcon.tsx new file mode 100644 index 00000000..abccf845 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/CloseIcon.tsx @@ -0,0 +1,14 @@ +import { IconAssetProps } from '../types'; + +export const CloseIcon = (props: IconAssetProps) => { + const { size } = props; + + return ( + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/ConnectStar.tsx b/packages/core/sds/src/components/Icon/assets/ConnectStar.tsx new file mode 100644 index 00000000..e1bcaf15 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/ConnectStar.tsx @@ -0,0 +1,15 @@ +import { IconAssetProps } from '../types'; + +export const ConnectStar = (props: IconAssetProps) => { + const { size = 20 } = props; + + return ( + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/HandShaving.tsx b/packages/core/sds/src/components/Icon/assets/HandShaving.tsx new file mode 100644 index 00000000..7c969987 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/HandShaving.tsx @@ -0,0 +1,14 @@ +import { IconAssetProps } from '../types'; + +export const HandShaving = (props: IconAssetProps) => { + const { size } = props; + + return ( + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/HomeIcon.tsx b/packages/core/sds/src/components/Icon/assets/HomeIcon.tsx new file mode 100644 index 00000000..468a1efc --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/HomeIcon.tsx @@ -0,0 +1,47 @@ +import { IconAssetProps } from '../types'; + +export const HomeIcon = (props: IconAssetProps) => { + const { color = 'white', size = 24 } = props; + + if (color === 'black') { + return ( + + + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/QuestionIcon.tsx b/packages/core/sds/src/components/Icon/assets/QuestionIcon.tsx new file mode 100644 index 00000000..e60c80a0 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/QuestionIcon.tsx @@ -0,0 +1,54 @@ +import { IconAssetProps } from '../types'; + +export const QuestionIcon = (props: IconAssetProps) => { + const { color = 'white', size = 24 } = props; + + if (color === 'black') { + return ( + + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/SearchIcon.tsx b/packages/core/sds/src/components/Icon/assets/SearchIcon.tsx new file mode 100644 index 00000000..9f18054b --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/SearchIcon.tsx @@ -0,0 +1,21 @@ +import { IconAssetProps } from '../types'; + +export const SearchIcon = (props: IconAssetProps) => { + const { size = '16' } = props; + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/UpAndDown.tsx b/packages/core/sds/src/components/Icon/assets/UpAndDown.tsx new file mode 100644 index 00000000..2ea76eb9 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/UpAndDown.tsx @@ -0,0 +1,17 @@ +import { IconAssetProps } from '../types'; + +export const UpAndDown = (props: IconAssetProps) => { + const { size } = props; + return ( + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/assets/UserIcon.tsx b/packages/core/sds/src/components/Icon/assets/UserIcon.tsx new file mode 100644 index 00000000..793bacf9 --- /dev/null +++ b/packages/core/sds/src/components/Icon/assets/UserIcon.tsx @@ -0,0 +1,40 @@ +import { IconAssetProps } from '../types'; + +export const UserIcon = (props: IconAssetProps) => { + const { color = 'white', size = 24 } = props; + + if (color === 'black') { + return ( + + + + + ); + } + + return ( + + + + + + + + + + + + ); +}; diff --git a/packages/core/sds/src/components/Icon/constants.ts b/packages/core/sds/src/components/Icon/constants.ts index bad039c8..a7f22e93 100644 --- a/packages/core/sds/src/components/Icon/constants.ts +++ b/packages/core/sds/src/components/Icon/constants.ts @@ -1,18 +1,30 @@ +import { AddMeeting } from './assets/AddMeeting'; +import { AddUser } from './assets/AddUser'; import { AngleDownIcon } from './assets/AngleDown'; import { AngleLeftIcon } from './assets/AngleLeft'; import { AngleRightIcon } from './assets/AngleRight'; import { AngleSmallDownIcon } from './assets/AngleSmallDown'; import { AngleSmallUpIcon } from './assets/AngleSmallUp'; import { AngleUpIcon } from './assets/AngleUp'; +import { BellIcon } from './assets/BellIcon'; import { CaretDownIcon } from './assets/CaretDown'; import { CheckIcon } from './assets/Check'; +import { CheckIconMeet } from './assets/CheckIcon'; +import { CloseIcon } from './assets/CloseIcon'; import { CommentsIcon } from './assets/CommentsIcon'; +import { ConnectStar } from './assets/ConnectStar'; import { CrownIcon } from './assets/Crown'; +import { HandShaving } from './assets/HandShaving'; +import { HomeIcon } from './assets/HomeIcon'; import { Landscape } from './assets/Landscape'; +import { QuestionIcon } from './assets/QuestionIcon'; import { SadUserIcon } from './assets/SadUserIcon'; +import { SearchIcon } from './assets/SearchIcon'; import { ShareIcon } from './assets/Share'; import { Stats } from './assets/Stats'; +import { UpAndDown } from './assets/UpAndDown'; import { Upload } from './assets/Upload'; +import { UserIcon } from './assets/UserIcon'; import { XIcon } from './assets/XIcon'; export const iconMap = { @@ -23,12 +35,24 @@ export const iconMap = { 'angle-small-up': AngleSmallUpIcon, 'angle-up': AngleUpIcon, 'caret-down': CaretDownIcon, - 'sad-user': SadUserIcon, - landscape: Landscape, - crown: CrownIcon, 'share-icon': ShareIcon, 'comments-icon': CommentsIcon, 'x-icon': XIcon, + 'home-icon': HomeIcon, + 'question-icon': QuestionIcon, + 'bell-icon': BellIcon, + 'user-icon': UserIcon, + 'connect-star': ConnectStar, + 'search-icon': SearchIcon, + 'sad-user': SadUserIcon, + 'add-user': AddUser, + 'up-and-down': UpAndDown, + 'close-icon': CloseIcon, + 'hand-shaving': HandShaving, + 'check-icon': CheckIconMeet, + 'add-meeting': AddMeeting, + landscape: Landscape, + crown: CrownIcon, stats: Stats, upload: Upload, check: CheckIcon, diff --git a/packages/web-domains/src/about-me/common/apis/mutates/useUpdateQuestionsActive.ts b/packages/web-domains/src/about-me/common/apis/mutates/useUpdateQuestionsActive.ts index c28aecac..119485e9 100644 --- a/packages/web-domains/src/about-me/common/apis/mutates/useUpdateQuestionsActive.ts +++ b/packages/web-domains/src/about-me/common/apis/mutates/useUpdateQuestionsActive.ts @@ -27,7 +27,7 @@ export const useUpdateQuestionsActive = () => { mutationFn, onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ANSWERS_ME_QUERY_KEY] }); - router.push('/about/me'); + router.push('/home/me'); }, }); }; diff --git a/packages/web-domains/src/about-me/common/apis/queries/useGetAnswersMe.ts b/packages/web-domains/src/about-me/common/apis/queries/useGetAnswersMe.ts index ae7bc65a..63ab60e3 100644 --- a/packages/web-domains/src/about-me/common/apis/queries/useGetAnswersMe.ts +++ b/packages/web-domains/src/about-me/common/apis/queries/useGetAnswersMe.ts @@ -1,4 +1,5 @@ -import { QueryClient, UseQueryOptions, useSuspenseQuery } from '@tanstack/react-query'; +import { UseQueryOptionsExcludedQueryKey } from '@sambad/types-utils/tanstack'; +import { QueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { Http } from '@/common/apis/base.api'; @@ -9,7 +10,7 @@ interface Params { } interface QueryProps extends Params { - options?: UseQueryOptions; + options?: UseQueryOptionsExcludedQueryKey; } export const ANSWERS_ME_QUERY_KEY = 'ANSWERS_ME_QUERY_KEY'; diff --git a/packages/web-domains/src/about-me/common/apis/queries/useGetMember.ts b/packages/web-domains/src/about-me/common/apis/queries/useGetMember.ts index 73bd9fb7..9e51262e 100644 --- a/packages/web-domains/src/about-me/common/apis/queries/useGetMember.ts +++ b/packages/web-domains/src/about-me/common/apis/queries/useGetMember.ts @@ -21,8 +21,6 @@ const queryFn = ({ meetingId, meetingMemberId }: Params) => export const useGetMember = (props: QueryProps) => { const { options, ...params } = props; - console.log(!!params.meetingMemberId); - return useQuery({ queryKey: [MEMBER_QUERY_KEY, params], queryFn: () => queryFn(params), diff --git a/packages/web-domains/src/about-me/common/apis/queries/useGetMemberMe.ts b/packages/web-domains/src/about-me/common/apis/queries/useGetMemberMe.ts index b14c0a95..e93d6adf 100644 --- a/packages/web-domains/src/about-me/common/apis/queries/useGetMemberMe.ts +++ b/packages/web-domains/src/about-me/common/apis/queries/useGetMemberMe.ts @@ -1,4 +1,5 @@ -import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { UseQueryOptionsExcludedQueryKey } from '@sambad/types-utils/tanstack'; +import { QueryClient, useQuery } from '@tanstack/react-query'; import { Http } from '@/common/apis/base.api'; @@ -9,7 +10,7 @@ interface Params { } interface QueryProps extends Params { - options?: UseQueryOptions; + options?: UseQueryOptionsExcludedQueryKey; } export const MEMBER_ME_QUERY_KEY = 'MEMBER_ME_QUERY_KEY'; diff --git a/packages/web-domains/src/about-me/features/containers/styles.ts b/packages/web-domains/src/about-me/features/containers/styles.ts index 77a8b815..71ec6606 100644 --- a/packages/web-domains/src/about-me/features/containers/styles.ts +++ b/packages/web-domains/src/about-me/features/containers/styles.ts @@ -16,7 +16,7 @@ export const aboutMeSectionCss = css({ [`& ${subTitleAttribute.querySelector}`]: { paddingBottom: size['6xs'], - '&:not(:first-child)': { + '&:not(:first-of-type)': { paddingTop: size['sm'], }, }, diff --git a/packages/web-domains/src/answer/features/answer-closing/containers/AnswerClosingContainer.tsx b/packages/web-domains/src/answer/features/answer-closing/containers/AnswerClosingContainer.tsx index 768b7179..e730827a 100644 --- a/packages/web-domains/src/answer/features/answer-closing/containers/AnswerClosingContainer.tsx +++ b/packages/web-domains/src/answer/features/answer-closing/containers/AnswerClosingContainer.tsx @@ -8,7 +8,7 @@ import { ClosingMessage } from '../components/ClosingMessage'; import { useAnswerClosingService } from '../services/useAnswerClosingService'; export const AnswerClosingContainer = () => { - const { answerGlobalTime, isOpen, basePath, close } = useAnswerClosingService(); + const { meetingId, answerGlobalTime, isOpen, basePath, close } = useAnswerClosingService(); return ( <> @@ -27,7 +27,7 @@ export const AnswerClosingContainer = () => { onClose={close} topTitle="모임원들에게 릴레이 질문을" bottomTitle="공유해보세요!" - shareLink={`${basePath}/answer/opening`} + shareLink={`${basePath}/${meetingId}/answer/opening`} /> diff --git a/packages/web-domains/src/answer/features/answer-closing/services/useAnswerClosingService.ts b/packages/web-domains/src/answer/features/answer-closing/services/useAnswerClosingService.ts index b12e0d44..85501547 100644 --- a/packages/web-domains/src/answer/features/answer-closing/services/useAnswerClosingService.ts +++ b/packages/web-domains/src/answer/features/answer-closing/services/useAnswerClosingService.ts @@ -1,15 +1,34 @@ -import { useAtomValue } from 'jotai'; +import dayjs from 'dayjs'; +import { useAtom } from 'jotai'; +import { useParams } from 'next/navigation'; +import { useGetProgressingQuestion } from '@/answer/common/apis/queries/useGetProgressingQuestion'; import { answerAtoms } from '@/answer/common/atoms/answer.atom'; import { getWebDomain } from '@/common'; import { useDialogContext } from '@/common/contexts/DialogProvider'; export const useAnswerClosingService = () => { + const { meetingId } = useParams<{ meetingId: string }>(); + const [answerGlobalTime, setAnswerGlobalTime] = useAtom(answerAtoms.answerGlobalTime); + const { close, isOpen } = useDialogContext(); - const answerGlobalTime = useAtomValue(answerAtoms.answerGlobalTime); const basePath = getWebDomain(); + useGetProgressingQuestion({ + params: { meetingId: parseInt(meetingId) }, + options: { + select: (data) => { + if (data?.startTime) { + setAnswerGlobalTime(dayjs(data.startTime).valueOf()); + } + return data; + }, + enabled: !!meetingId, + }, + }); + return { + meetingId, answerGlobalTime: answerGlobalTime ?? 0, isOpen, close, diff --git a/packages/web-domains/src/answer/features/answer-question/services/useAnswerQuestionService.ts b/packages/web-domains/src/answer/features/answer-question/services/useAnswerQuestionService.ts index e3102d03..38de7c3b 100644 --- a/packages/web-domains/src/answer/features/answer-question/services/useAnswerQuestionService.ts +++ b/packages/web-domains/src/answer/features/answer-question/services/useAnswerQuestionService.ts @@ -3,27 +3,21 @@ import { useParams, useRouter } from 'next/navigation'; import { useGetQuestion } from '@/answer/common/apis/queries/useGetQuestion'; import { answerAtoms } from '@/answer/common/atoms/answer.atom'; -import { useGetMeetingInfo } from '@/home/common/apis/queries/useGetMeetingName'; export const useAnswerQuestionService = () => { - const { questionId } = useParams<{ questionId: string }>(); + const { meetingId, questionId } = useParams<{ meetingId: string; questionId: string }>(); const [answerList, setAnswerList] = useAtom(answerAtoms.answerList); const { push } = useRouter(); - const { data: meetingInfo } = useGetMeetingInfo({ - options: { gcTime: Infinity }, - }); - - const meetingId = meetingInfo?.meetings[0]?.meetingId; const { data: question } = useGetQuestion({ - params: { meetingId: meetingId! }, + params: { meetingId: parseInt(meetingId) }, options: { enabled: !!meetingId, }, }); const moveToCommentPage = async () => { - push(`/answer/${questionId}/comment`); + push(`/${meetingId}/answer/${questionId}/comment`); }; const handleAnswerList = (answerIdList: number[]) => { @@ -31,6 +25,7 @@ export const useAnswerQuestionService = () => { }; return { + meetingId, isNotAnswerd: !answerList.length, question: question?.content, questionType: question?.questionType, diff --git a/packages/web-domains/src/answer/features/comment/services/useCommentService.tsx b/packages/web-domains/src/answer/features/comment/services/useCommentService.tsx index 1a28ca01..3dc3be47 100644 --- a/packages/web-domains/src/answer/features/comment/services/useCommentService.tsx +++ b/packages/web-domains/src/answer/features/comment/services/useCommentService.tsx @@ -4,12 +4,12 @@ import { useAtomValue } from 'jotai'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { ANSWERS_ME_QUERY_KEY } from '@/about-me/common/apis/queries/useGetAnswersMe'; import { useAnswerQuestionMutation } from '@/answer/common/apis/mutations/useAnswerQuestionMutation'; import { useCommentMutation } from '@/answer/common/apis/mutations/useCommentMutation'; import { PROGRESSING_QUESTION_QUERY_KEY } from '@/answer/common/apis/queries/useGetProgressingQuestion'; import { answerAtoms } from '@/answer/common/atoms/answer.atom'; import { GATHER_MEMBER_QUERY_KEY } from '@/home/common/apis/queries/useGetGatherMemberList'; -import { useGetMeetingInfo } from '@/home/common/apis/queries/useGetMeetingName'; import { NOTIFICATION_QUERY_KEY } from '@/home/common/apis/queries/useGetNotification'; import { TOP_PREVIOUS_QUESTION_QUERY_KEY } from '@/home/common/apis/queries/useGetTopPreviousQuestionList'; @@ -18,25 +18,19 @@ export const useCommentService = () => { const { push } = useRouter(); const [comment, setComment] = useState(''); const answerList = useAtomValue(answerAtoms.answerList); - const { questionId } = useParams<{ questionId: string }>(); + const { meetingId, questionId } = useParams<{ meetingId: string; questionId: string }>(); const { mutateAsync: sendCommentMutate } = useCommentMutation({}); const { mutateAsync: sendAnswerMutate } = useAnswerQuestionMutation({}); - const { data: meetingInfo } = useGetMeetingInfo({ - options: { gcTime: Infinity }, - }); - const handleSubmit = async () => { - const meetingId = meetingInfo?.meetings[0]?.meetingId; - if (!meetingId) { return; } try { - await sendAnswerMutate({ meetingId: meetingId.toString(), meetingQuestionId: questionId, answerIds: answerList }); + await sendAnswerMutate({ meetingId: meetingId, meetingQuestionId: questionId, answerIds: answerList }); if (comment.length) { - await sendCommentMutate({ content: comment, meetingId, meetingQuestionId: questionId }); + await sendCommentMutate({ content: comment, meetingId: parseInt(meetingId), meetingQuestionId: questionId }); } const questionInvalidate = queryClient.invalidateQueries({ @@ -53,10 +47,19 @@ export const useCommentService = () => { const notificationInvalidate = queryClient.invalidateQueries({ queryKey: [NOTIFICATION_QUERY_KEY], }); + const myAnswersInvalidate = queryClient.invalidateQueries({ + queryKey: [ANSWERS_ME_QUERY_KEY, meetingId], + }); - Promise.all([questionInvalidate, topPreviousQuestionInvalidate, gatherMemberInvalidate, notificationInvalidate]); + Promise.all([ + questionInvalidate, + topPreviousQuestionInvalidate, + gatherMemberInvalidate, + notificationInvalidate, + myAnswersInvalidate, + ]); - push('/answer/closing'); + push(`/${meetingId}/answer/closing`); } catch (error) { if (isAxiosError(error)) { console.log(error); diff --git a/packages/web-domains/src/answer/features/floating-button/components/StartButton.tsx b/packages/web-domains/src/answer/features/floating-button/components/StartButton.tsx index f860695e..3e67e139 100644 --- a/packages/web-domains/src/answer/features/floating-button/components/StartButton.tsx +++ b/packages/web-domains/src/answer/features/floating-button/components/StartButton.tsx @@ -6,9 +6,10 @@ import Link from 'next/link'; interface StartButtonProps { questionId: number; + meetingId?: number; } -export const StartButton = ({ questionId }: StartButtonProps) => { +export const StartButton = ({ questionId, meetingId }: StartButtonProps) => { return (
{ padding: '0 20px', }} > - + +
+
+ {!disableBack && ( + + )} {title} - + {rightDecor}
); diff --git a/packages/web-domains/src/common/utils/bodyScrollLock.ts b/packages/web-domains/src/common/utils/bodyScrollLock.ts new file mode 100644 index 00000000..2dbb0299 --- /dev/null +++ b/packages/web-domains/src/common/utils/bodyScrollLock.ts @@ -0,0 +1,46 @@ +const isBrowser = typeof window !== 'undefined'; + +export class BodyScrollLock { + container: HTMLElement | null = null; + + constructor(container: HTMLElement | null) { + this.container = container; + } + + scrollLock = (e: TouchEvent) => { + if (!isBrowser) { + return; + } + + let scrollTarget = e.target; + const containers = this.container; + while (scrollTarget instanceof HTMLElement && scrollTarget !== containers) { + const style = window.getComputedStyle(scrollTarget); + if ( + (scrollTarget.scrollHeight > scrollTarget.clientHeight || + scrollTarget.scrollWidth > scrollTarget.clientWidth) && + /auto|scroll/.test(style.overflow) + ) { + break; + } + scrollTarget = scrollTarget.parentNode as HTMLElement; + } + if (scrollTarget !== containers) { + e.preventDefault(); + return; + } + + if (scrollTarget instanceof HTMLElement && scrollTarget.scrollHeight === scrollTarget.clientHeight) { + e.preventDefault(); + } + }; + + disableScroll = () => { + document.body.style.overflow = 'hidden'; + document.body.addEventListener('touchmove', this.scrollLock, { passive: false }); + }; + enableScroll = () => { + document.body.style.removeProperty('overflow'); + document.body.removeEventListener('touchmove', this.scrollLock); + }; +} diff --git a/packages/web-domains/src/common/utils/generateInviteLink.ts b/packages/web-domains/src/common/utils/generateInviteLink.ts new file mode 100644 index 00000000..c71b0f55 --- /dev/null +++ b/packages/web-domains/src/common/utils/generateInviteLink.ts @@ -0,0 +1,11 @@ +import { getWebDomain } from './getWebDomain'; + +export const generateInviteLink = (inviteCode?: string | null) => { + if (!inviteCode) { + return null; + } + + const domain = getWebDomain(); + + return `${domain}/meeting/participate/closing?inviteCode=${inviteCode}`; +}; diff --git a/packages/web-domains/src/common/utils/getCurrentMeeting.ts b/packages/web-domains/src/common/utils/getCurrentMeeting.ts new file mode 100644 index 00000000..3ee41b68 --- /dev/null +++ b/packages/web-domains/src/common/utils/getCurrentMeeting.ts @@ -0,0 +1,27 @@ +import { MeetingIdListResponseType } from '@/home/common/apis/schema/Meeting.schema'; + +/** + * + * @param meetingData + * @returns + * 데이터 없을 때 null 반환 + * lastMeetingId가 있고 데이터에서 찾았을 때, + * 존재하면 마지막 방문 미팅 반환 + * 없다면 처음 인덱스 미팅 반환 -> 왜 처음 인덱스를 반환하는가? : 미팅이 삭제될 경우를 대비함 + * lastMeetingId가 없다면 + * 처음 인덱스 미팅 반환 + */ +export const getCurrentMeeting = (meetingData: MeetingIdListResponseType) => { + if (!meetingData) { + return null; + } + + const firstIndexMeeting = meetingData.meetings[0]; + + if (meetingData.lastMeetingId) { + const lastVisitedMeeting = meetingData.meetings.find((meeting) => meeting.meetingId === meetingData.lastMeetingId); + return lastVisitedMeeting ?? firstIndexMeeting ?? null; + } + + return firstIndexMeeting ?? null; +}; diff --git a/packages/web-domains/src/common/utils/getKeywordRegex.ts b/packages/web-domains/src/common/utils/getKeywordRegex.ts new file mode 100644 index 00000000..522ea84e --- /dev/null +++ b/packages/web-domains/src/common/utils/getKeywordRegex.ts @@ -0,0 +1,11 @@ +export const getKeywordRegex = (keyword: string) => { + try { + const escapedDash = keyword.replace(/\\/g, '\\'); + const escapedKeyword = escapedDash.replace(/[^ㄱ-ㅎ|ㅏ-ㅣ|가-햫|a-z|A-Z|0-9|\s]/g, '\\$&'); + const regex = new RegExp(`(${escapedKeyword})`, 'gi'); + return regex; + } catch (error) { + console.log(error); + throw error; + } +}; diff --git a/packages/web-domains/src/home/common/apis/mutations/useHandWavingIgnoreMutation.ts b/packages/web-domains/src/home/common/apis/mutations/useHandWavingIgnoreMutation.ts new file mode 100644 index 00000000..239aeb78 --- /dev/null +++ b/packages/web-domains/src/home/common/apis/mutations/useHandWavingIgnoreMutation.ts @@ -0,0 +1,25 @@ +import { UseMutationOptions, useMutation } from '@tanstack/react-query'; + +import { Http } from '@/common/apis/base.api'; + +type Params = { meetingId: number; handWavingId: number }; + +interface Args { + options?: UseMutationOptions<{} | undefined, unknown, Params>; +} + +export const useHandWavingIgonoreMutation = ({ options }: Args = {}) => { + return useMutation({ + mutationFn: async (params: Params) => { + const data = await ignoreHandwaving(params); + return data; + }, + ...options, + }); +}; + +export async function ignoreHandwaving(params: Params): Promise<{}> { + const { handWavingId, meetingId } = params; + const data = await Http.PATCH(`/v1/meetings/${meetingId}/hand-wavings/${handWavingId}/ignore`, {}); + return data; +} diff --git a/packages/web-domains/src/home/common/apis/mutations/useHandWavingResponseMutation.ts b/packages/web-domains/src/home/common/apis/mutations/useHandWavingResponseMutation.ts new file mode 100644 index 00000000..40d39833 --- /dev/null +++ b/packages/web-domains/src/home/common/apis/mutations/useHandWavingResponseMutation.ts @@ -0,0 +1,25 @@ +import { UseMutationOptions, useMutation } from '@tanstack/react-query'; + +import { Http } from '@/common/apis/base.api'; + +type Params = { meetingId: number; handWavingId: number }; + +interface Args { + options?: UseMutationOptions<{} | undefined, unknown, Params>; +} + +export const useHandWavingResponseMutation = ({ options }: Args = {}) => { + return useMutation({ + mutationFn: async (params: Params) => { + const data = await handWavingResponse(params); + return data; + }, + ...options, + }); +}; + +export async function handWavingResponse(params: Params): Promise<{}> { + const { handWavingId, meetingId } = params; + const data = await Http.PATCH(`/v1/meetings/${meetingId}/hand-wavings/${handWavingId}/accept`, {}); + return data; +} diff --git a/packages/web-domains/src/home/common/apis/mutations/useUpdateLastMeeting.ts b/packages/web-domains/src/home/common/apis/mutations/useUpdateLastMeeting.ts new file mode 100644 index 00000000..95fdeb2e --- /dev/null +++ b/packages/web-domains/src/home/common/apis/mutations/useUpdateLastMeeting.ts @@ -0,0 +1,34 @@ +import { UseMutationOptions, useMutation } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; + +import { Http } from '@/common/apis/base.api'; + +type Params = { meetingId: number }; + +interface Args { + options?: UseMutationOptions<{} | undefined, unknown, Params>; +} + +export const useUpdateLastMeeting = ({ options }: Args = {}) => { + return useMutation({ + mutationFn: async (params: Params) => { + try { + const data = await updateLastMeeting(params); + return data; + } catch (error) { + if (isAxiosError(error)) { + console.error(error); + } + } + }, + ...options, + }); +}; + +export async function updateLastMeeting(params: Params): Promise<{}> { + const { meetingId } = params; + const data = await Http.PATCH(`/v1/users/last-meeting`, { + meetingId, + }); + return data; +} diff --git a/packages/web-domains/src/home/common/apis/queries/useGetEvents.ts b/packages/web-domains/src/home/common/apis/queries/useGetEvents.ts new file mode 100644 index 00000000..0bdafd6e --- /dev/null +++ b/packages/web-domains/src/home/common/apis/queries/useGetEvents.ts @@ -0,0 +1,61 @@ +import { UseQueryOptionsExcludedQueryKey } from '@sambad/types-utils/tanstack'; +import { QueryClient, useQuery } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies'; + +import { Http } from '../../../../common/apis/base.api'; +import { AlarmEventListResponseType } from '../schema/Notification.schema'; + +type Params = { meetingId: number }; +interface Args { + params: Params; + options?: UseQueryOptionsExcludedQueryKey; +} + +export const EVENTS_QUERY_KEY = 'EVENTS_QUERY_KEY'; + +export const useGetEvents = ({ params, options }: Args) => { + return useQuery({ + queryKey: [EVENTS_QUERY_KEY, params.meetingId], + queryFn: async () => { + try { + const data = await getEvents(params); + return data; + } catch (error) { + if (isAxiosError(error)) { + console.log(error); + } + } + }, + ...options, + }); +}; + +export const getEventsPrefetch = (params: Params, queryClient: QueryClient, cookie?: ReadonlyRequestCookies) => { + const prefetch = queryClient.prefetchQuery({ + queryKey: [EVENTS_QUERY_KEY, params.meetingId], + queryFn: async () => { + try { + return await getEvents(params, cookie); + } catch (error) { + if (isAxiosError(error)) { + console.log(error); + } + } + }, + }); + + return prefetch; +}; + +export async function getEvents(params: Params, cookie?: ReadonlyRequestCookies): Promise { + const { meetingId } = params; + + const data = await Http.GET(`/v1/events/meetings/${meetingId}`, { + headers: { + Cookie: cookie?.toString(), + }, + }); + + return data; +} diff --git a/packages/web-domains/src/home/common/apis/queries/useGetInviteCode.ts b/packages/web-domains/src/home/common/apis/queries/useGetInviteCode.ts new file mode 100644 index 00000000..8605ebec --- /dev/null +++ b/packages/web-domains/src/home/common/apis/queries/useGetInviteCode.ts @@ -0,0 +1,44 @@ +import { UseQueryOptionsExcludedQueryKey } from '@sambad/types-utils/tanstack'; +import { useQuery } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies'; + +import { Http } from '../../../../common/apis/base.api'; +import { InviteCodeResponseType } from '../schema/InviteCode.schema'; + +type Params = { meetingId: number }; +interface Args { + params: Params; + options?: UseQueryOptionsExcludedQueryKey; +} + +export const INVITE_CODE_QUERY_KEY = 'INVITE_CODE_QUERY_KEY'; + +export const useGetInviteCode = ({ params, options }: Args) => { + return useQuery({ + queryKey: [INVITE_CODE_QUERY_KEY, params.meetingId], + queryFn: async () => { + try { + const data = await getInviteCode(params); + return data; + } catch (error) { + if (isAxiosError(error)) { + console.log(error); + } + } + }, + ...options, + }); +}; + +export async function getInviteCode(params: Params, cookie?: ReadonlyRequestCookies): Promise { + const { meetingId } = params; + + const data = await Http.GET(`/v1/meetings/${meetingId}/code`, { + headers: { + Cookie: cookie?.toString(), + }, + }); + + return data; +} diff --git a/packages/web-domains/src/home/common/apis/queries/useGetPreviousQuestionList.ts b/packages/web-domains/src/home/common/apis/queries/useGetPreviousQuestionList.ts index 53dc00fb..a7e2b8ac 100644 --- a/packages/web-domains/src/home/common/apis/queries/useGetPreviousQuestionList.ts +++ b/packages/web-domains/src/home/common/apis/queries/useGetPreviousQuestionList.ts @@ -29,6 +29,7 @@ export const useGetPreviousQuestionList = ; + meetings: Array; + lastMeetingId: number; } | undefined; diff --git a/packages/web-domains/src/home/common/apis/schema/Notification.schema.ts b/packages/web-domains/src/home/common/apis/schema/Notification.schema.ts index a77fde89..a0b266f8 100644 --- a/packages/web-domains/src/home/common/apis/schema/Notification.schema.ts +++ b/packages/web-domains/src/home/common/apis/schema/Notification.schema.ts @@ -1,10 +1,29 @@ export type NotificationType = { eventId: number; - eventType: 'QUESTION_REGISTERED' | 'TARGET_MEMBER'; + eventType: NotificationEventType; }; +export type NotificationEventType = 'QUESTION_REGISTERED' | 'TARGET_MEMBER' | 'HAND_WAVING_REQUESTED'; + export type NotificationResponseType = | { contents: Array; } | undefined; + +export type AlarmEventType = { + eventId: number; + type: NotificationEventType; + messages: string[]; + status: 'ACTIVE' | 'INACTIVE'; + additionalData: { + handWavingId: number; + }; + createdAt: number; +}; + +export type AlarmEventListType = { + contents: Array; +}; + +export type AlarmEventListResponseType = AlarmEventListType | undefined; diff --git a/packages/web-domains/src/home/common/apis/schema/useGetProgressingQuestionQuery.type.ts b/packages/web-domains/src/home/common/apis/schema/useGetProgressingQuestionQuery.type.ts index 8e8566ce..2d02a044 100644 --- a/packages/web-domains/src/home/common/apis/schema/useGetProgressingQuestionQuery.type.ts +++ b/packages/web-domains/src/home/common/apis/schema/useGetProgressingQuestionQuery.type.ts @@ -18,4 +18,5 @@ export type MemberType = { name: string; profileImageFileUrl?: string; role: 'OWNER' | 'ADMIN' | 'MEMBER'; + isHandWaved: boolean; }; diff --git a/packages/web-domains/src/home/common/atoms/home.atom.ts b/packages/web-domains/src/home/common/atoms/home.atom.ts index 95fad6ef..0b712d9f 100644 --- a/packages/web-domains/src/home/common/atoms/home.atom.ts +++ b/packages/web-domains/src/home/common/atoms/home.atom.ts @@ -1,9 +1,19 @@ import { atom } from 'jotai'; -export const isProgessingQuestionAtom = atom(false); +const isProgessingQuestionAtom = atom(false); -export const isSelectedTargetAtom = atom(false); +const isSelectedTargetAtom = atom(false); -export const homeGlobalTimeAtom = atom(null); +const homeGlobalTimeAtom = atom(null); -export const isNextTargetAtom = atom(false); +const isNextTargetAtom = atom(false); + +const currentMeeting = atom<{ meetingId: number; name: string } | null>(null); + +export const HomeAtoms = { + isNextTargetAtom, + homeGlobalTimeAtom, + isSelectedTargetAtom, + isProgessingQuestionAtom, + currentMeeting, +}; diff --git a/packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.styles.ts b/packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.styles.ts new file mode 100644 index 00000000..daaf351e --- /dev/null +++ b/packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.styles.ts @@ -0,0 +1,38 @@ +import { css } from '@emotion/react'; +import { colors } from '@sds/theme'; + +export const BottomSheetContainerCss = css({ + position: 'fixed', + width: '100%', + height: '100%', + top: 0, + left: 0, + zIndex: 999, +}); + +export const DimmedCss = css({ + position: 'absolute', + width: '100%', + height: '100%', + top: 0, + left: 0, + transition: 'opacity 0.2s ease', + background: `${colors.black}`, + opacity: '0.4', +}); + +export const ContentWrapperCss = css({ + position: 'absolute', + bottom: 0, + left: '50%', + zIndex: 100, + width: '100%', + maxWidth: '600px', + boxShadow: '0px -4px 8px rgba(0, 0, 0, 0.1)', + borderRadius: '16px 16px 0 0', + transition: 'transform 0.2s ease', + height: 'calc(100% - 54px)', + backgroundColor: `${colors.grey200}`, + padding: '8px 0', + overflow: 'auto', +}); diff --git a/packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.tsx b/packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.tsx new file mode 100644 index 00000000..7d05669a --- /dev/null +++ b/packages/web-domains/src/home/common/components/BottomSheet/BottomSheet.tsx @@ -0,0 +1,89 @@ +import { Icon, Txt } from '@sds/components'; +import { colors } from '@sds/theme'; +import { CSSProperties, PropsWithChildren, useEffect, useState } from 'react'; + +import { BodyScrollLock } from '@/common/utils/bodyScrollLock'; + +import { BottomSheetContainerCss, ContentWrapperCss, DimmedCss } from './BottomSheet.styles'; + +interface BottomSheetProps { + title?: string; + isOpen: boolean; + onClose?: () => void; + bottomSheetStyles?: CSSProperties; +} + +export const BottomSheet = ({ + title, + isOpen, + onClose, + children, + bottomSheetStyles, +}: PropsWithChildren) => { + const [isContainerVisible, setIsContainerVisible] = useState(false); + const [isContentVisible, setIsContentVisible] = useState(false); + const [contentRef, setContentRef] = useState(null); + + useEffect(() => { + if (contentRef) { + const scrollControl = new BodyScrollLock(contentRef); + if (isOpen) { + scrollControl.disableScroll(); + setIsContainerVisible(true); + setIsContentVisible(true); + } else { + scrollControl.enableScroll(); + setIsContainerVisible(false); + setIsContentVisible(false); + } + return () => { + scrollControl.enableScroll(); + }; + } + }, [isOpen, contentRef]); + + const handleCloseContent = () => { + setIsContentVisible(false); + }; + + return ( +
+
+
{ + if (!isContentVisible) { + onClose?.(); + } + }} + > + {title && ( +
+ + {title} + + + +
+ )} +
+ {children} +
+
+
+ ); +}; diff --git a/packages/web-domains/src/home/common/components/Layout/HomeLayout.tsx b/packages/web-domains/src/home/common/components/Layout/HomeLayout.tsx index aaeb44ff..be2f34fb 100644 --- a/packages/web-domains/src/home/common/components/Layout/HomeLayout.tsx +++ b/packages/web-domains/src/home/common/components/Layout/HomeLayout.tsx @@ -1,5 +1,7 @@ +'use client'; + import { PropsWithChildren } from 'react'; export const HomeLayout = ({ children }: PropsWithChildren) => { - return
{children}
; + return
{children}
; }; diff --git a/packages/web-domains/src/home/common/components/Layout/PreviousQuestionLayout.tsx b/packages/web-domains/src/home/common/components/Layout/PreviousQuestionLayout.tsx index 9b146a8b..1153471a 100644 --- a/packages/web-domains/src/home/common/components/Layout/PreviousQuestionLayout.tsx +++ b/packages/web-domains/src/home/common/components/Layout/PreviousQuestionLayout.tsx @@ -1,3 +1,5 @@ +'use client'; + import { PropsWithChildren } from 'react'; export const PreviousQuestionLayout = ({ children }: PropsWithChildren) => { diff --git a/packages/web-domains/src/home/common/components/Navigation/HomeNavigationProvider.tsx b/packages/web-domains/src/home/common/components/Navigation/HomeNavigationProvider.tsx new file mode 100644 index 00000000..279dfa4c --- /dev/null +++ b/packages/web-domains/src/home/common/components/Navigation/HomeNavigationProvider.tsx @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const HomeNavigationContext = createContext({}); diff --git a/packages/web-domains/src/home/common/components/Navigation/HomeNavigationTab.tsx b/packages/web-domains/src/home/common/components/Navigation/HomeNavigationTab.tsx new file mode 100644 index 00000000..673c536b --- /dev/null +++ b/packages/web-domains/src/home/common/components/Navigation/HomeNavigationTab.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { Icon, Txt } from '@sds/components'; +import { colors } from '@sds/theme'; +import Link from 'next/link'; +import { PropsWithChildren } from 'react'; + +type TabItemType = { path: string; title: string; Icon: ReturnType }; + +interface TabProps { + tabList?: Array; +} + +const Tab = ({ tabList, children }: PropsWithChildren) => { + return ( +
    + {tabList?.map(({ path, Icon, title }) => )} + {children} +
+ ); +}; + +interface TabItemProps extends TabItemType {} + +const TabItem = ({ path, title, Icon }: TabItemProps) => { + return ( +
  • + + {Icon} + + {title} + + +
  • + ); +}; + +export const HomeNavigation = { + Tab, + TabItem, +}; diff --git a/packages/web-domains/src/home/common/hooks/useSetCurrentMeeting.ts b/packages/web-domains/src/home/common/hooks/useSetCurrentMeeting.ts new file mode 100644 index 00000000..762a6ec6 --- /dev/null +++ b/packages/web-domains/src/home/common/hooks/useSetCurrentMeeting.ts @@ -0,0 +1,62 @@ +import { isAxiosError } from 'axios'; +import { useAtom, useSetAtom } from 'jotai'; +import { useEffect } from 'react'; + +import { getCurrentMeeting } from '@/common/utils/getCurrentMeeting'; + +import { useUpdateLastMeeting } from '../apis/mutations/useUpdateLastMeeting'; +import { useGetMeetingInfo } from '../apis/queries/useGetMeetingName'; +import { MeetingInfoType } from '../apis/schema/Meeting.schema'; +import { HomeAtoms } from '../atoms/home.atom'; + +export const useSetCurrentMeeting = () => { + const [currentMeeting, setCurrentMeeting] = useAtom(HomeAtoms.currentMeeting); + const setIsProgressingQuestion = useSetAtom(HomeAtoms.isProgessingQuestionAtom); + const setHomeGlobalTime = useSetAtom(HomeAtoms.homeGlobalTimeAtom); + const setSelectedTarget = useSetAtom(HomeAtoms.isSelectedTargetAtom); + const setIsNextTarget = useSetAtom(HomeAtoms.isNextTargetAtom); + + const { data: meetingInfo } = useGetMeetingInfo({}); + + const { mutateAsync } = useUpdateLastMeeting(); + + const handleChangeCurrentMeeting = async (meeting: MeetingInfoType) => { + if (meeting.meetingId === currentMeeting?.meetingId) { + return; + } + setIsNextTarget(false); + setSelectedTarget(false); + setHomeGlobalTime(null); + setIsProgressingQuestion(false); + try { + setCurrentMeeting(meeting); + mutateAsync({ meetingId: meeting.meetingId }); + } catch (error) { + if (isAxiosError(error)) { + console.log(error); + } + } + }; + + useEffect(() => { + if (!currentMeeting) { + const lastVisitedMeeting = getCurrentMeeting(meetingInfo); + + try { + if (lastVisitedMeeting) { + mutateAsync({ meetingId: lastVisitedMeeting.meetingId }); + } + } catch (error) { + console.log(error); + } + setCurrentMeeting(lastVisitedMeeting); + } + }, [meetingInfo]); + + return { + meetingInfo, + meetingId: currentMeeting ? currentMeeting?.meetingId : getCurrentMeeting(meetingInfo)?.meetingId, + gatherName: currentMeeting ? currentMeeting?.name : getCurrentMeeting(meetingInfo)?.name, + handleChangeCurrentMeeting, + }; +}; diff --git a/packages/web-domains/src/home/features/floating-button/components/ProgressingQuestionModal.tsx b/packages/web-domains/src/home/features/floating-button/components/ProgressingQuestionModal.tsx index f88990b4..ff9692fd 100644 --- a/packages/web-domains/src/home/features/floating-button/components/ProgressingQuestionModal.tsx +++ b/packages/web-domains/src/home/features/floating-button/components/ProgressingQuestionModal.tsx @@ -1,14 +1,16 @@ import { Button, Txt } from '@sambad/sds/components'; -import { colors } from '@sambad/sds/theme'; -import dayjs from 'dayjs'; +import { borderRadiusVariants, colors } from '@sambad/sds/theme'; import dynamic from 'next/dynamic'; import Countdown from 'react-countdown'; +import { getRemainTime } from '@/home/common/utils/getRemainTime.ts'; + import { Modal, ModalProps } from '../../../../common/components/Modal/Modal'; -import { useDialogContext } from '../../../../common/contexts/DialogProvider'; interface ProgressingQuestionModalProps extends ModalProps { - countdownTimer: number | string | Date; + isOpen: boolean; + onClose?: () => void; + time: number | string | Date; } const CountdownRender = dynamic( @@ -16,16 +18,30 @@ const CountdownRender = dynamic( import('./ProgressingQuestionModalCountdownRender.tsx').then((mod) => mod.ProgressingQuestionModalCountdownRender), { ssr: true, + loading: () => ( +
    + ), }, ); -export const ProgressingQuestionModal = ({ countdownTimer, ...rest }: ProgressingQuestionModalProps) => { - const { isOpen, close } = useDialogContext(); - - const timer = countdownTimer ? countdownTimer : dayjs().valueOf(); +export const ProgressingQuestionModal = ({ isOpen, time, onClose, ...rest }: ProgressingQuestionModalProps) => { + const countdownTimer = getRemainTime(time); return ( - +
    @@ -41,12 +57,12 @@ export const ProgressingQuestionModal = ({ countdownTimer, ...rest }: Progressin 릴레이 질문을 생성할 수 있어요 ( )} /> - +
    + + ); }; diff --git a/packages/web-domains/src/home/features/gather-member/services/useGatherMemberProfileListService.tsx b/packages/web-domains/src/home/features/gather-member/services/useGatherMemberProfileListService.tsx index 4b906aca..435235f2 100644 --- a/packages/web-domains/src/home/features/gather-member/services/useGatherMemberProfileListService.tsx +++ b/packages/web-domains/src/home/features/gather-member/services/useGatherMemberProfileListService.tsx @@ -1,13 +1,28 @@ -import { useGetMeetingInfo } from '@/home/common/apis/queries/useGetMeetingName'; +import { useAtomValue } from 'jotai'; +import { debounce } from 'lodash-es'; +import { useEffect, useState } from 'react'; + +import { getWebDomain } from '@/common'; +import { generateInviteLink } from '@/common/utils/generateInviteLink'; +import { getKeywordRegex } from '@/common/utils/getKeywordRegex'; +import { useGetInviteCode } from '@/home/common/apis/queries/useGetInviteCode'; +import { useGetMyInfo } from '@/home/common/apis/queries/useGetMyInfo'; +import { MemberType } from '@/home/common/apis/schema/useGetProgressingQuestionQuery.type'; +import { HomeAtoms } from '@/home/common/atoms/home.atom'; import { useGetGatherMemberList } from '../../../common/apis/queries/useGetGatherMemberList'; export const useGatherMemberProfileListService = () => { - const { data: meetingInfo } = useGetMeetingInfo({ - options: { gcTime: Infinity }, - }); + const currentMeeting = useAtomValue(HomeAtoms.currentMeeting); + const [isOpen, setIsOpen] = useState(false); + const [searchInput, setSearchInput] = useState(''); + const [gatherMemberList, setGatherMemberList] = useState([]); + const meetingId = currentMeeting?.meetingId; - const meetingId = meetingInfo?.meetings[0]?.meetingId; + const { data: myInfo } = useGetMyInfo({ + params: { meetingId: meetingId! }, + options: { enabled: !!meetingId }, + }); const { data } = useGetGatherMemberList({ params: { meetingId: meetingId! }, @@ -16,7 +31,57 @@ export const useGatherMemberProfileListService = () => { }, }); + const { data: inviteCode } = useGetInviteCode({ + params: { meetingId: meetingId! }, + options: { enabled: !!meetingId }, + }); + + const handleChangeSearchInput = (value: string) => { + setSearchInput(value); + }; + + const handleChangeGatherMemberList = (keyword: string) => { + const trimKeyword = keyword.trim(); + const regex = getKeywordRegex(trimKeyword); + + if (data?.contents) { + const filteredMemberList = data.contents.filter(({ name }) => regex.test(name)); + setGatherMemberList(filteredMemberList); + } + }; + + const inviteModalOpen = () => { + setIsOpen(true); + }; + + const inviteModalClose = () => { + setIsOpen(false); + }; + + useEffect(() => { + if (data?.contents && myInfo) { + setGatherMemberList(data?.contents); + } + }, [data, myInfo]); + + useEffect(() => { + let filter; + filter = debounce(handleChangeGatherMemberList, 300); + filter(searchInput); + + return () => { + filter = null; + }; + }, [searchInput]); + return { - gatherMemberList: data?.contents, + meetingId, + isOpen, + inviteLink: generateInviteLink(inviteCode?.code) ?? `${getWebDomain()}`, + searchInput, + gatherMemberList, + handleChangeSearchInput, + inviteModalOpen, + inviteModalClose, }; }; diff --git a/packages/web-domains/src/home/features/home-navigation/containers/HomeNavigationContainer.tsx b/packages/web-domains/src/home/features/home-navigation/containers/HomeNavigationContainer.tsx new file mode 100644 index 00000000..bda7314f --- /dev/null +++ b/packages/web-domains/src/home/features/home-navigation/containers/HomeNavigationContainer.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { Icon } from '@sds/components'; +import { usePathname } from 'next/navigation'; + +import { HomeNavigation } from '@/home/common/components/Navigation/HomeNavigationTab'; + +export const HomeNavigationConatiner = () => { + return ( + + } /> + } + /> + } + /> + } + /> + + ); +}; + +const isActive = (path: string) => { + const pathname = usePathname(); + + if (pathname === path) { + return 'black'; + } + return 'white'; +}; diff --git a/packages/web-domains/src/home/features/meeting-choice/components/MeetingChoiceBottomSheet.tsx b/packages/web-domains/src/home/features/meeting-choice/components/MeetingChoiceBottomSheet.tsx new file mode 100644 index 00000000..596ffd2a --- /dev/null +++ b/packages/web-domains/src/home/features/meeting-choice/components/MeetingChoiceBottomSheet.tsx @@ -0,0 +1,100 @@ +import { Txt, Icon } from '@sds/components'; +import { borderRadiusVariants, colors } from '@sds/theme'; +import Link from 'next/link'; + +import { MeetingInfoType } from '@/home/common/apis/schema/Meeting.schema'; +import { BottomSheet } from '@/home/common/components/BottomSheet/BottomSheet'; + +interface MeetingChoiceBottomSheetProps { + currentMeetingId?: number; + meetingList: MeetingInfoType[]; + isOpen: boolean; + onClose: () => void; + onChange: (meeting: MeetingInfoType) => void; +} + +export const MeetingChoiceBottomSheet = ({ + currentMeetingId, + meetingList, + isOpen, + onClose, + onChange, +}: MeetingChoiceBottomSheetProps) => { + const selectMeeting = (meeting: MeetingInfoType) => { + onChange(meeting); + onClose(); + }; + + const isCurrent = (meeting: MeetingInfoType) => currentMeetingId === meeting.meetingId; + + return ( + +
    + + 전체 모임(체험판) + +
    +
      + {meetingList.map((meeting) => { + const currentFlag = isCurrent(meeting); + + return ( +
    • selectMeeting(meeting)} + key={meeting.meetingId} + css={{ + padding: '12px 20px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + cursor: 'pointer', + }} + > + + {meeting.name} + + {currentFlag && } +
    • + ); + })} +
    + + + + 모임 추가하기 + + +
    + ); +}; diff --git a/packages/web-domains/src/home/features/meeting-intro/containers/MeetingIntroContainer.tsx b/packages/web-domains/src/home/features/meeting-intro/containers/MeetingIntroContainer.tsx new file mode 100644 index 00000000..9c83bfc6 --- /dev/null +++ b/packages/web-domains/src/home/features/meeting-intro/containers/MeetingIntroContainer.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { colors } from '@sds/theme'; + +import { HomeNavigationConatiner } from '../../home-navigation/containers/HomeNavigationContainer'; +import { MeetingChoiceBottomSheet } from '../../meeting-choice/components/MeetingChoiceBottomSheet'; +import { GatherName } from '../../progressing-question/components/GatherName/GatherName'; +import { useMeetingIntroService } from '../services/useMeetingIntroService'; + +export const MeetingIntroContainer = () => { + const { + meetingInfo, + meetingId, + gatherName, + isOpen, + handleCloseBottomSheet, + handleOpenBottmSheet, + handleChangeCurrentMeeting, + } = useMeetingIntroService(); + + return ( +
    + + + + +
    + ); +}; diff --git a/packages/web-domains/src/home/features/meeting-intro/services/useMeetingIntroService.ts b/packages/web-domains/src/home/features/meeting-intro/services/useMeetingIntroService.ts new file mode 100644 index 00000000..95e84e86 --- /dev/null +++ b/packages/web-domains/src/home/features/meeting-intro/services/useMeetingIntroService.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import { useSetCurrentMeeting } from '@/home/common/hooks/useSetCurrentMeeting'; + +export const useMeetingIntroService = () => { + const { meetingId, meetingInfo, gatherName, handleChangeCurrentMeeting } = useSetCurrentMeeting(); + + const [isOpen, setIsOpen] = useState(false); + + const handleOpenBottmSheet = () => { + setIsOpen(true); + }; + + const handleCloseBottomSheet = () => { + setIsOpen(false); + }; + + return { + isOpen, + meetingInfo, + meetingId, + gatherName, + handleOpenBottmSheet, + handleCloseBottomSheet, + handleChangeCurrentMeeting, + }; +}; diff --git a/packages/web-domains/src/home/features/my-profile/components/AboutMe.tsx b/packages/web-domains/src/home/features/my-profile/components/AboutMe.tsx new file mode 100644 index 00000000..6c15ec24 --- /dev/null +++ b/packages/web-domains/src/home/features/my-profile/components/AboutMe.tsx @@ -0,0 +1,22 @@ +import { size } from '@sds/theme/size'; + +import { MeetingMemberResponse } from '@/about-me/common/apis/schema/MeetingMemberResponse'; +import { HobbyList, IntroduceBox } from '@/about-me/features/components'; +import { EmptyView } from '@/common/components'; + +interface AboutMeProps { + info?: MeetingMemberResponse; +} + +export const AboutMe = ({ info }: AboutMeProps) => { + const hasNoInfo = !info?.hobbies.length && !info?.introduction; + + if (hasNoInfo) return ; + + return ( +
    + + +
    + ); +}; diff --git a/packages/web-domains/src/home/features/my-profile/components/AnswerQuestions.tsx b/packages/web-domains/src/home/features/my-profile/components/AnswerQuestions.tsx new file mode 100644 index 00000000..0efc9eb1 --- /dev/null +++ b/packages/web-domains/src/home/features/my-profile/components/AnswerQuestions.tsx @@ -0,0 +1,90 @@ +import { If } from '@sambad/react-utils'; +import { Accordion, Checkbox, Txt } from '@sds/components'; +import { colors, size } from '@sds/theme'; +import { useRef, useImperativeHandle, forwardRef } from 'react'; + +import { useUpdateQuestionsActive } from '@/about-me/common/apis/mutates/useUpdateQuestionsActive'; +import { MyMeetingAnswerListResponse } from '@/about-me/common/apis/schema/MyMeetingAnswerListReponse'; +import { checkboxAttribute } from '@/about-me/features/containers/constants'; +import { answerContentCss, checkboxAndTriggerCss } from '@/about-me/features/containers/styles'; +import { EmptyView } from '@/common/components'; + +interface AnswerQuestionsProps { + isModifyPage: boolean; + answers?: MyMeetingAnswerListResponse; +} + +interface Ref { + onMutate: (meetingId: number) => void; +} + +export const AnswerQuestions = forwardRef(({ isModifyPage, answers }, ref) => { + const answersLength = answers?.contents?.length; + + const { mutate } = useUpdateQuestionsActive(); + + const checkboxRefs = useRef<(HTMLInputElement | null)[]>([]); + + if (answersLength === 0) { + return ; + } + + const handleModify = (meetingId: number) => { + const checkedIds = checkboxRefs.current + .filter((checkbox) => checkbox?.checked) + .map((checkbox) => Number(checkbox?.id)); + + mutate({ meetingId, activeMeetingQuestionIds: checkedIds }); + }; + + // NOTE: ScreenContainer에서 호출하기 위해 ref에 추가 + useImperativeHandle(ref, () => ({ + onMutate: (meetingId) => handleModify(meetingId), + })); + + if (answers?.contents?.length === 0) { + return ; + } + + return ( +
    + + + 프로필에 표시할 질문 선택하기 + + + + {answers?.contents?.map((answer, index) => ( + +
    + + { + checkboxRefs.current[index] = el; + }} + defaultChecked={!answer.isHidden} + {...checkboxAttribute.attribute} + /> + + + + #{index + 1} + + {answer.title} + +
    + +
    + {answer.content.join(', ')} + {answer.commentContent != null && {answer.commentContent}} +
    +
    +
    + ))} +
    +
    + ); +}); + +AnswerQuestions.displayName = 'AnswerQuestions'; diff --git a/packages/web-domains/src/home/features/my-profile/components/MyProfile.tsx b/packages/web-domains/src/home/features/my-profile/components/MyProfile.tsx new file mode 100644 index 00000000..4e645788 --- /dev/null +++ b/packages/web-domains/src/home/features/my-profile/components/MyProfile.tsx @@ -0,0 +1,31 @@ +import { TextButton } from '@sds/components'; +import { size } from '@sds/theme/size'; +import Link from 'next/link'; + +import { MeetingMemberResponse } from '@/about-me/common/apis/schema/MeetingMemberResponse'; +import { Profile } from '@/about-me/features/components'; +import { profileRootCss } from '@/about-me/features/containers/styles'; + +interface MyProfileProps { + profileInfo?: MeetingMemberResponse; + meetingId?: number; +} + +export const MyProfile = ({ profileInfo, meetingId }: MyProfileProps) => { + return ( +
    + + + 기본 정보 수정하러 가기 + +
    + ); +}; diff --git a/packages/web-domains/src/home/features/my-profile/containers/MyprofileContainer.tsx b/packages/web-domains/src/home/features/my-profile/containers/MyprofileContainer.tsx new file mode 100644 index 00000000..59af3e36 --- /dev/null +++ b/packages/web-domains/src/home/features/my-profile/containers/MyprofileContainer.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { colors, size } from '@sambad/sds/theme'; +import { SegmentedControl, TextButton, Txt } from '@sds/components'; + +import { + aboutMeSectionCss, + screenRootCss, + segmentedCss, + segmentedSectionCss, +} from '@/about-me/features/containers/styles'; +import { ActionBar } from '@/common/components/ActionBar/ActionBar'; + +import { AboutMe } from '../components/AboutMe'; +import { AnswerQuestions } from '../components/AnswerQuestions'; +import { MyProfile } from '../components/MyProfile'; +import { useMyprofileService } from '../services/useMyprofileService'; + +export const MyprofileContainer = () => { + const { segmentedRef, data, answers, handleTab, tab, meetingId, isModifyPage, handleModify, handleMoveToModifyPage } = + useMyprofileService(); + + return ( +
    + + {/* NOTE: sds에서 Txt 컴포넌트가 css로 오버라이딩이 되지 않는 무제 해결 후 inherit 제거 예정 */} + + {isModifyPage ? '수정완료' : '수정하기'} + + + } + /> +
    + + +
    + + 자기소개 + 릴레이 질문 + +
    +
    + {tab === 'about-me' && } + {tab === 'answered-questions' && ( + + )} +
    +
    +
    +
    +
    + ); +}; + +const layoutStyle = { + backgroundColor: colors.grey200, +}; + +const sectionStyle = { + marginTop: size['5xs'], +}; diff --git a/packages/web-domains/src/home/features/my-profile/services/useMyprofileService.ts b/packages/web-domains/src/home/features/my-profile/services/useMyprofileService.ts new file mode 100644 index 00000000..3ad73312 --- /dev/null +++ b/packages/web-domains/src/home/features/my-profile/services/useMyprofileService.ts @@ -0,0 +1,46 @@ +import { useRouter } from 'next/navigation'; +import { useRef, useState } from 'react'; + +import { useGetAnswersMe } from '@/about-me/common/apis/queries/useGetAnswersMe'; +import { useGetMemberMe } from '@/about-me/common/apis/queries/useGetMemberMe'; +import { useGetIsModifyPage } from '@/about-me/features/hooks/useGetIsModifyPage'; +import { useSetCurrentMeeting } from '@/home/common/hooks/useSetCurrentMeeting'; + +type TabType = 'about-me' | 'answered-questions'; + +export const useMyprofileService = () => { + const segmentedRef = useRef<{ onMutate: (meetingId?: number) => void }>(null); + const { meetingId } = useSetCurrentMeeting(); + const isModifyPage = useGetIsModifyPage(); + const router = useRouter(); + + const [tab, setTab] = useState(isModifyPage ? 'answered-questions' : 'about-me'); + + const handleTab = (value: string) => { + setTab(value as TabType); + }; + + const data = useGetMemberMe({ meetingId: meetingId!, options: { staleTime: 0 } }); + + const answers = useGetAnswersMe({ meetingId: meetingId!, options: { staleTime: 0 } }); + + const handleMoveToModifyPage = () => { + router.push(`/home/me/modify`); + }; + + const handleModify = () => { + segmentedRef.current?.onMutate(meetingId); + }; + + return { + segmentedRef, + data: data.data, + answers: answers.data, + tab, + handleTab, + meetingId, + isModifyPage, + handleModify, + handleMoveToModifyPage, + }; +}; diff --git a/packages/web-domains/src/home/features/notification/components/ArrivedQuestionNotification.tsx b/packages/web-domains/src/home/features/notification/components/ArrivedQuestionNotification.tsx index 7e27c289..6337e7f3 100644 --- a/packages/web-domains/src/home/features/notification/components/ArrivedQuestionNotification.tsx +++ b/packages/web-domains/src/home/features/notification/components/ArrivedQuestionNotification.tsx @@ -3,17 +3,23 @@ import { colors } from '@sambad/sds/theme'; import Link from 'next/link'; import { Modal, ModalProps } from '../../../../common/components/Modal/Modal'; -import { useDialogContext } from '../../../../common/contexts/DialogProvider'; interface ArrivedQuestionNotificationProps extends ModalProps { + meetingId?: number; + isOpen: boolean; + onClose: () => void; onClickAnswerLater?: () => void; } -export const ArrivedQuestionNotification = ({ onClickAnswerLater, ...rest }: ArrivedQuestionNotificationProps) => { - const { isOpen, close } = useDialogContext(); - +export const ArrivedQuestionNotification = ({ + meetingId, + isOpen, + onClose, + onClickAnswerLater, + ...rest +}: ArrivedQuestionNotificationProps) => { return ( - +
    @@ -22,7 +28,7 @@ export const ArrivedQuestionNotification = ({ onClickAnswerLater, ...rest }: Arr 릴레이 질문에 바로 답변해볼까요? - + + +
    + } + /> + ); + } + + case 'QUESTION_REGISTERED': + case 'TARGET_MEMBER': + return ; + } + }; + + return ( +
    +
    + + 알림 + +
    + renderNotification(notfication, meetingId)} + /> +
    + ); +}; diff --git a/packages/web-domains/src/home/features/notification/containers/NotificationContainer.tsx b/packages/web-domains/src/home/features/notification/containers/NotificationContainer.tsx index 16f944bc..aa2dbbb4 100644 --- a/packages/web-domains/src/home/features/notification/containers/NotificationContainer.tsx +++ b/packages/web-domains/src/home/features/notification/containers/NotificationContainer.tsx @@ -4,13 +4,14 @@ import { ArrivedQuestionNotification } from '../components/ArrivedQuestionNotifi import { SelectedTargetMemberNotification } from '../components/SelectedTargetMemberNotification'; import { useNotificationService } from '../services/useNotificationService'; export const NotificationContainer = () => { - const { notfication, handleClose, isOpen, isNotAnswerd, isNotRegistered, handleClickActionLater } = + const { meetingId, notfication, handleClose, isOpen, isNotAnswerd, isNotRegistered, handleClickActionLater } = useNotificationService(); return ( <> {notfication?.eventType === 'QUESTION_REGISTERED' && isNotAnswerd && ( { )} {notfication?.eventType === 'TARGET_MEMBER' && isNotRegistered && ( { + const { meetingId } = useSetCurrentMeeting(); + + const { data: myInfo } = useGetMyInfo({ + params: { meetingId: meetingId! }, + options: { enabled: !!meetingId }, + }); + + const { data } = useGetEvents({ + params: { meetingId: meetingId! }, + options: { + enabled: !!meetingId, + }, + }); + + const { mutate: ignoreHandWaving } = useHandWavingIgonoreMutation(); + + const { mutate: handWavingResponse } = useHandWavingResponseMutation(); + + return { + meetingId, + notficationList: data?.contents ?? [], + myMemberId: myInfo?.meetingMemberId, + ignoreHandWaving, + handWavingResponse, + }; +}; diff --git a/packages/web-domains/src/home/features/notification/services/useNotificationService.ts b/packages/web-domains/src/home/features/notification/services/useNotificationService.ts index 0848b6b8..d779f502 100644 --- a/packages/web-domains/src/home/features/notification/services/useNotificationService.ts +++ b/packages/web-domains/src/home/features/notification/services/useNotificationService.ts @@ -1,41 +1,33 @@ import { useQueryClient } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; +import { useAtomValue } from 'jotai'; import { useEffect } from 'react'; import { PROGRESSING_QUESTION_QUERY_KEY } from '@/answer/common/apis/queries/useGetProgressingQuestion'; import { useDialogContext } from '@/common/contexts/DialogProvider'; import { useInActiveEventMutation } from '@/home/common/apis/mutations/useInActiveEventMutation'; -import { useGetMeetingInfo } from '@/home/common/apis/queries/useGetMeetingName'; import { useGetNotification } from '@/home/common/apis/queries/useGetNotification'; import { ProgressingQuestionType } from '@/home/common/apis/schema/useGetProgressingQuestionQuery.type'; +import { HomeAtoms } from '@/home/common/atoms/home.atom'; export const useNotificationService = () => { const queryClient = useQueryClient(); + const currentMeeting = useAtomValue(HomeAtoms.currentMeeting); const { isOpen, close, open } = useDialogContext(); - const { data: meetingInfo } = useGetMeetingInfo({ - options: { gcTime: Infinity }, - }); - const meetingId = meetingInfo?.meetings[0]?.meetingId; + const meetingId = currentMeeting?.meetingId; const progressingQuestionData: ProgressingQuestionType | undefined = queryClient.getQueryData([ PROGRESSING_QUESTION_QUERY_KEY, meetingId, ]); - const { data: notfication, isRefetching } = useGetNotification({ + const { data: notfication } = useGetNotification({ params: { meetingId: meetingId! }, options: { enabled: !!meetingId, refetchInterval: 1000 * 30, - select: (data) => { - if (!data?.contents.length) { - close(); - } - - return data; - }, }, }); @@ -58,17 +50,20 @@ export const useNotificationService = () => { }; useEffect(() => { - if (notfication?.contents[0]) { + if (notfication?.contents?.[0]) { open(); + } else { + close(); } - }, [notfication]); + }, [notfication, currentMeeting]); return { - notfication: notfication?.contents[0], + meetingId, + notfication: notfication?.contents?.[0], isOpen, handleClose, handleClickActionLater, - isRefetching, + isNotAnswerd: !progressingQuestionData?.isAnswered, isNotRegistered: !progressingQuestionData?.isQuestionRegistered, }; diff --git a/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItem.tsx b/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItem.tsx index 28e7e889..7c7a3d20 100644 --- a/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItem.tsx +++ b/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItem.tsx @@ -10,7 +10,7 @@ interface HomePreviousQuestionItemProps { question: TopPreviousQuestionType; } -export const HomePreviousQuestionItem = ({ question }: HomePreviousQuestionItemProps) => { +export const HomePreviousQuestionItem = ({ meetingId, question }: HomePreviousQuestionItemProps) => { const { content, engagementRate, title, meetingQuestionId } = question; return ( @@ -25,16 +25,20 @@ export const HomePreviousQuestionItem = ({ question }: HomePreviousQuestionItemP }} >
    - - {content} - - 이/가 가장 많았어요. + {content ? ( + + {content} + + 이/가 가장 많았어요. + - + ) : ( + 아무도 답변하지 않았어요... + )} {title} diff --git a/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItemPlaceholder.tsx b/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItemPlaceholder.tsx new file mode 100644 index 00000000..dffef744 --- /dev/null +++ b/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/HomePreviousQuestionItemPlaceholder.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from '@sambad/sds/components'; +import { colors } from '@sambad/sds/theme'; + +import { AngleRightIcon } from '../../../../../../../core/sds/src/components/Icon/assets/AngleRight'; + +export const HomePreviousQuestionItemPlaceholder = () => { + return ( +
  • +
    +
    + +
    +
    + +
    +
    +
  • + ); +}; diff --git a/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/PreviousQuestionItem.tsx b/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/PreviousQuestionItem.tsx index 5ba51205..47aa33ae 100644 --- a/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/PreviousQuestionItem.tsx +++ b/packages/web-domains/src/home/features/previous-question/components/PreviousQuestion/PreviousQuestionItem.tsx @@ -11,7 +11,7 @@ interface PreviousQuestionItemProps { question: PreviousQuestionType; } -export const PreviousQuestionItem = ({ question }: PreviousQuestionItemProps) => { +export const PreviousQuestionItem = ({ meetingId, question }: PreviousQuestionItemProps) => { const { title, questionNumber, targetMember, questionImageFileUrl, startDate, meetingQuestionId } = question; const questionTime = dayjs(startDate).format('YYYY-MM-DD'); @@ -25,7 +25,7 @@ export const PreviousQuestionItem = ({ question }: PreviousQuestionItemProps) => listStyle: 'none', }} > - +
    { - const { previousQuestionList, targetRef } = usePreviousQuestionListService(); + const { previousQuestionList, targetRef, meetingId } = usePreviousQuestionListService(); if (!previousQuestionList || !previousQuestionList.length) { return ; @@ -19,7 +19,9 @@ export const PreviousQuestionListContainer = () => { } + renderItem={(item) => ( + + )} ref={targetRef as MutableRefObject} /> diff --git a/packages/web-domains/src/home/features/previous-question/containers/TopPreviousQuestionListContainer.tsx b/packages/web-domains/src/home/features/previous-question/containers/TopPreviousQuestionListContainer.tsx index b428a269..c5151d14 100644 --- a/packages/web-domains/src/home/features/previous-question/containers/TopPreviousQuestionListContainer.tsx +++ b/packages/web-domains/src/home/features/previous-question/containers/TopPreviousQuestionListContainer.tsx @@ -1,21 +1,54 @@ 'use client'; import { Txt } from '@sambad/sds/components'; -import { colors } from '@sambad/sds/theme'; +import { borderRadiusVariants, colors } from '@sambad/sds/theme'; import Link from 'next/link'; import { AngleRightIcon } from '../../../../../../core/sds/src/components/Icon/assets/AngleRight'; import { HomePreviousQuestionItem } from '../components/PreviousQuestion/HomePreviousQuestionItem'; +import { HomePreviousQuestionItemPlaceholder } from '../components/PreviousQuestion/HomePreviousQuestionItemPlaceholder'; import { HomePreviousQuestionList } from '../components/PreviousQuestion/HomePreviousQuestionList'; import { useTopPreviousQuestionListService } from '../services/useTopPreviousQuestionListService'; export const TopPreviousQuestionListContainer = () => { - const { previousQuestionList, meetingId } = useTopPreviousQuestionListService(); + const { previousQuestionList, meetingId, isLoading } = useTopPreviousQuestionListService(); if (!previousQuestionList || !previousQuestionList.contents.length) { return null; } + if (isLoading) { + return ( +
    + + 이전 질문 + + + 전체보기 + + + + + + +
      + + +
    +
    + ); + } + return (
    { + const currentMeeting = useAtomValue(HomeAtoms.currentMeeting); const [previousQuestionList, setPreviousQuestionList] = useState>([]); const [hasNextPage, setHasNextPage] = useState(true); const [page, setPage] = useState(0); - const { data: meetingInfo } = useGetMeetingInfo({ - options: { gcTime: Infinity }, - }); - - const meetingId = meetingInfo?.meetings[0]?.meetingId; + const meetingId = currentMeeting?.meetingId; const { data, isFetching } = useGetPreviousQuestionList({ params: { meetingId: meetingId!, page }, diff --git a/packages/web-domains/src/home/features/previous-question/services/useTopPreviousQuestionListService.tsx b/packages/web-domains/src/home/features/previous-question/services/useTopPreviousQuestionListService.tsx index 95d69182..483f64b3 100644 --- a/packages/web-domains/src/home/features/previous-question/services/useTopPreviousQuestionListService.tsx +++ b/packages/web-domains/src/home/features/previous-question/services/useTopPreviousQuestionListService.tsx @@ -1,14 +1,14 @@ -import { useGetMeetingInfo } from '@/home/common/apis/queries/useGetMeetingName'; +import { useAtomValue } from 'jotai'; + import { useGetTopPreviousQuestionList } from '@/home/common/apis/queries/useGetTopPreviousQuestionList'; +import { HomeAtoms } from '@/home/common/atoms/home.atom'; export const useTopPreviousQuestionListService = () => { - const { data: meetingInfo } = useGetMeetingInfo({ - options: { gcTime: Infinity }, - }); + const currentMeeting = useAtomValue(HomeAtoms.currentMeeting); - const meetingId = meetingInfo?.meetings[0]?.meetingId; + const meetingId = currentMeeting?.meetingId; - const { data: previousQuestionList } = useGetTopPreviousQuestionList({ + const { data: previousQuestionList, isLoading } = useGetTopPreviousQuestionList({ params: { meetingId: meetingId! }, options: { select: (data) => { @@ -19,6 +19,7 @@ export const useTopPreviousQuestionListService = () => { }); return { + isLoading, previousQuestionList, meetingId, }; diff --git a/packages/web-domains/src/home/features/progressing-question/components/GatherName/GatherName.tsx b/packages/web-domains/src/home/features/progressing-question/components/GatherName/GatherName.tsx index fc577b67..71df5e8f 100644 --- a/packages/web-domains/src/home/features/progressing-question/components/GatherName/GatherName.tsx +++ b/packages/web-domains/src/home/features/progressing-question/components/GatherName/GatherName.tsx @@ -1,31 +1,38 @@ -import { Txt } from '@sambad/sds/components'; +import { Icon, Skeleton, Txt } from '@sambad/sds/components'; import { colors } from '@sds/theme'; -import Link from 'next/link'; import { HTMLAttributes } from 'react'; -import { Avatar } from '../../../../common/components/Avatar/Avatar'; - interface GatherNameProps extends HTMLAttributes { gatherName?: string; - profileImage?: string; + subTitle: string; + onClick?: () => void; } -export const GatherName = ({ gatherName, profileImage, ...rest }: GatherNameProps) => { +export const GatherName = ({ gatherName, subTitle, onClick, ...rest }: GatherNameProps) => { return (
    - - {gatherName} - + - 모임원들과 더 가까워져 볼까요? + {subTitle}
    -
    - - - -
    ); }; diff --git a/packages/web-domains/src/home/features/progressing-question/components/QuestionInfo/ActiveQuestion.tsx b/packages/web-domains/src/home/features/progressing-question/components/QuestionInfo/ActiveQuestion.tsx index 6d46cdd4..de27727f 100644 --- a/packages/web-domains/src/home/features/progressing-question/components/QuestionInfo/ActiveQuestion.tsx +++ b/packages/web-domains/src/home/features/progressing-question/components/QuestionInfo/ActiveQuestion.tsx @@ -9,10 +9,11 @@ import { ProgressingQuestionType } from '../../../../common/apis/schema/useGetPr import { Avatar } from '../../../../common/components/Avatar/Avatar'; interface ActiveQuestionProps { + meetingId?: number; question: ProgressingQuestionType; } -export const ActiveQuestion = ({ question }: ActiveQuestionProps) => { +export const ActiveQuestion = ({ meetingId, question }: ActiveQuestionProps) => { const { responseCount, targetMember, @@ -95,7 +96,7 @@ export const ActiveQuestion = ({ question }: ActiveQuestionProps) => { />
    {!isAnswered && ( - +