├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── docs.md │ ├── feat.md │ ├── fix.md │ ├── refactor.md │ └── style.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── chromatic.yml │ └── deploy.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.ts └── preview.tsx ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── README.md ├── build-storybook.log ├── docker-compose.yml ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── assets │ └── images │ │ ├── BUDDY.png │ │ ├── PIPI.png │ │ ├── TOBBY.png │ │ ├── TRUE.png │ │ ├── apple_login_large_wide.png │ │ ├── character-buddy.png │ │ ├── character-pipi.png │ │ ├── character-toby.png │ │ ├── character-true.png │ │ ├── congrats-clap.png │ │ ├── default-og-image.png │ │ ├── default-profile-image-buddy.png │ │ ├── default-profile-image-pipi.png │ │ ├── default-profile-image-tobby.png │ │ ├── default-profile-image-true.png │ │ ├── default_planet_logo.png │ │ ├── error-ufo.png │ │ ├── explain-edit-id-card.png │ │ ├── explain-id-card.png │ │ ├── id_card_creation.png │ │ ├── invitation-og-image.png │ │ ├── invitation.png │ │ ├── kakao_login_large_wide.png │ │ ├── memo.png │ │ ├── onboarding-planet.png │ │ ├── onboarding-question-food.png │ │ ├── onboarding-question-snack.png │ │ ├── onboarding-question-ticket.png │ │ ├── onboarding-question-ufo.png │ │ ├── planet-cover-default-image.png │ │ ├── planet-create-result-bg.png │ │ ├── planet-with-shadow.png │ │ ├── planet.png │ │ ├── rocket.png │ │ ├── splash.png │ │ ├── talk.png │ │ └── ufo.png ├── mockServiceWorker.js ├── next.svg └── vercel.svg ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── api │ ├── config │ │ ├── api.types.ts │ │ ├── customError.ts │ │ ├── interceptor.client.ts │ │ ├── interceptor.server.ts │ │ ├── interceptor.ts │ │ ├── privateApi.server.ts │ │ ├── privateApi.ts │ │ ├── publicApi.ts │ │ └── requestUrl.ts │ ├── domain │ │ ├── auth.api.client.ts │ │ ├── auth.api.server.ts │ │ ├── comment │ │ │ ├── comment.api.server.ts │ │ │ ├── comment.api.ts │ │ │ └── comment.helper.ts │ │ ├── community.api.server.ts │ │ ├── community.api.ts │ │ ├── idCard.api.server.ts │ │ ├── idCard.api.ts │ │ ├── image.api.ts │ │ ├── notification.api.server.ts │ │ ├── notification.api.ts │ │ ├── nudge.api.client.ts │ │ ├── user.api.server.ts │ │ └── user.api.ts │ └── fetch │ │ ├── privateFetch.ts │ │ └── publicFetch.ts ├── app │ ├── Provider.tsx │ ├── admin │ │ └── planet │ │ │ ├── [id] │ │ │ ├── components │ │ │ │ └── AdminPlanetDetailPage │ │ │ │ │ ├── AdminPlanetDetailPage.tsx │ │ │ │ │ └── index.ts │ │ │ ├── edit │ │ │ │ ├── components │ │ │ │ │ └── PlanetAdminEdit │ │ │ │ │ │ ├── PlanetAdminEdit.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ └── create │ │ │ ├── page.tsx │ │ │ └── result │ │ │ └── page.tsx │ ├── apple-icon.png │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ ├── callback │ │ │ └── kakao │ │ │ │ └── page.tsx │ │ └── signin │ │ │ ├── page.tsx │ │ │ └── signInProviders.server.tsx │ ├── error.tsx │ ├── favicon.ico │ ├── icon.png │ ├── invitation │ │ └── [code] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── layout.tsx │ ├── my-page │ │ ├── [communityId] │ │ │ ├── components │ │ │ │ └── MyPageIdCard │ │ │ │ │ ├── MyPageIdCard.client.tsx │ │ │ │ │ └── index.ts │ │ │ ├── edit │ │ │ │ ├── components │ │ │ │ │ └── MyPageEditIdCard │ │ │ │ │ │ ├── MyPageEditIdCard.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── config │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── empty │ │ │ └── page.tsx │ ├── not-found.tsx │ ├── notification │ │ ├── InjectQueryDataNotification.client.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── onboarding │ │ └── page.tsx │ ├── page.tsx │ └── planet │ │ ├── [communityId] │ │ ├── components │ │ │ ├── CommunityDetail │ │ │ │ ├── CommunityDetail.tsx │ │ │ │ └── index.ts │ │ │ ├── CommunityIdCards │ │ │ │ ├── CommunityIdCards.client.tsx │ │ │ │ └── index.ts │ │ │ └── IdCardCreatorButton │ │ │ │ ├── IdCardCreatorButton.client.tsx │ │ │ │ └── index.ts │ │ ├── id-card │ │ │ ├── [idCardId] │ │ │ │ ├── components │ │ │ │ │ ├── CommentCount │ │ │ │ │ │ ├── CommentCount.client.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── CommentList │ │ │ │ │ │ ├── CommentList.client.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── IdCardDetail │ │ │ │ │ │ ├── IdCardDetail.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── Nudge │ │ │ │ │ │ ├── Button │ │ │ │ │ │ └── NudgeButton.tsx │ │ │ │ │ │ ├── Message │ │ │ │ │ │ └── NudgeMessages.tsx │ │ │ │ │ │ ├── Nudge.client.tsx │ │ │ │ │ │ └── Nudge.stories.tsx │ │ │ │ └── page.tsx │ │ │ └── create │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ │ └── page.tsx ├── components │ ├── BottomNavigation │ │ ├── BottomNavigation.stories.tsx │ │ ├── BottomNavigation.tsx │ │ └── index.ts │ ├── BottomSheet │ │ ├── BottomSheet.stories.tsx │ │ ├── BottomSheet.tsx │ │ ├── BottomSheetContent.tsx │ │ ├── BottomSheetFooter.tsx │ │ ├── BottomSheetFooterButton.tsx │ │ ├── BottomSheetHeader.tsx │ │ ├── BottomSheetWrapper.tsx │ │ ├── index.ts │ │ └── useBottomSheet.ts │ ├── Button │ │ ├── Button.stories.tsx │ │ ├── Button.tsx │ │ ├── TextButton.stories.tsx │ │ ├── TextButton.tsx │ │ └── index.ts │ ├── Chip │ │ ├── Chip.stories.tsx │ │ ├── Chip.tsx │ │ └── index.ts │ ├── ConfirmPopup │ │ ├── ConfirmCreateIdCard │ │ │ ├── ConfirmCreateIdCard.client.tsx │ │ │ ├── ConfirmCreateIdCard.stories.tsx │ │ │ └── index.ts │ │ ├── ConfirmDeleteKeyword │ │ │ ├── ConfirmDeleteKeyword.client.tsx │ │ │ ├── ConfirmDeleteKeyword.stories.tsx │ │ │ └── index.ts │ │ ├── ConfirmUnSave │ │ │ ├── ConfirmUnSave.client.tsx │ │ │ ├── ConfirmUnSave.stories.tsx │ │ │ └── index.ts │ │ ├── CopyInvitation │ │ │ ├── CopyInvitation.client.tsx │ │ │ ├── CopyInvitation.stories.tsx │ │ │ └── index.ts │ │ ├── SimpleConfirmPopup │ │ │ ├── SimpleConfirmPopup.client.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── useConfirmPopup.ts │ ├── Divider │ │ ├── Divider.tsx │ │ └── index.ts │ ├── ErrorBoundary │ │ ├── RetryErrorBoundary.client.tsx │ │ └── index.ts │ ├── HydrationProvider │ │ ├── HydrationProvider.tsx │ │ └── index.ts │ ├── Icon │ │ ├── ArrowLeftIcon.tsx │ │ ├── ArrowVerticalIcon.tsx │ │ ├── CameraIcon.tsx │ │ ├── CancelCircleIcon.tsx │ │ ├── CancelIcon.tsx │ │ ├── CelebrationIcon.tsx │ │ ├── ChatBubbleIcon.tsx │ │ ├── CheckCircleFillIcon.tsx │ │ ├── ChevronLeftIcon.tsx │ │ ├── ChevronRightIcon.tsx │ │ ├── DashIcon.tsx │ │ ├── EyeIcon.tsx │ │ ├── GearIcon.tsx │ │ ├── HeartExchangeIcon.tsx │ │ ├── HeartFillIcon.tsx │ │ ├── HeartIcon.tsx │ │ ├── HomeIcon.tsx │ │ ├── Icon.stories.tsx │ │ ├── KakaoIcon.tsx │ │ ├── NotificationIcon.tsx │ │ ├── NudgeCloseIcon.tsx │ │ ├── NudgeIcon.tsx │ │ ├── NudgeMessageIcon.tsx │ │ ├── PersonIcon.tsx │ │ ├── PlusIcon.tsx │ │ ├── QuestionCircleIcon.tsx │ │ ├── RiceIcon.tsx │ │ ├── SendIcon.tsx │ │ ├── ThreeDotsVerticalIcon.tsx │ │ └── index.tsx │ ├── KeywordInput │ │ ├── KeywordInput.stories.tsx │ │ ├── KeywordInput.tsx │ │ ├── index.ts │ │ ├── keywordInput.type.ts │ │ ├── useInputAutoSize.ts │ │ └── useKeywordInput.hooks.ts │ ├── Menu │ │ ├── Menu.stories.tsx │ │ ├── MenuElement.tsx │ │ ├── MenuHeader.tsx │ │ ├── MenuWrapper.tsx │ │ └── index.ts │ ├── NudgeIconSelector │ │ ├── NudgeIconSelector.tsx │ │ └── index.tsx │ ├── Popup │ │ ├── Popup.stories.tsx │ │ ├── Popup.tsx │ │ ├── index.ts │ │ └── usePopup.ts │ ├── Portal │ │ ├── AnimatedPortal.tsx │ │ ├── Portal.tsx │ │ └── index.ts │ ├── ProfileImageEdit │ │ ├── ProfileImageEdit.client.tsx │ │ └── index.ts │ ├── ProgressBar │ │ ├── ProgressBar.stories.tsx │ │ ├── ProgressBar.tsx │ │ └── index.ts │ ├── SpeechBubble │ │ ├── SpeechBubble.stories.tsx │ │ ├── SpeechBubbleDetail.tsx │ │ ├── SpeechBubbleThumbnail.tsx │ │ └── index.ts │ ├── Svg │ │ ├── Svg.tsx │ │ └── index.tsx │ ├── Swiper │ │ ├── Swiper.stories.tsx │ │ ├── Swiper.tsx │ │ ├── SwiperSlide.tsx │ │ └── index.tsx │ ├── Tag │ │ ├── Tag.stories.tsx │ │ ├── Tag.tsx │ │ └── index.ts │ ├── Template │ │ ├── Template.stories.tsx │ │ ├── TemplateButton.tsx │ │ ├── TemplateContent.tsx │ │ ├── TemplateDescription.tsx │ │ ├── TemplateTitle.tsx │ │ ├── TemplateWrapper.tsx │ │ └── index.ts │ ├── TextArea │ │ ├── TextArea.stories.tsx │ │ ├── TextAreaBorder.tsx │ │ ├── TextAreaContent.tsx │ │ ├── TextAreaHeader.tsx │ │ ├── TextAreaImage.tsx │ │ ├── TextAreaLabel.tsx │ │ ├── TextAreaWrapper.tsx │ │ ├── index.tsx │ │ ├── useAutoHeightTextArea.tsx │ │ └── useTextArea.ts │ ├── TextInput │ │ ├── TextInput.stories.tsx │ │ ├── TextInputBorder.tsx │ │ ├── TextInputContent.tsx │ │ ├── TextInputLabel.tsx │ │ ├── TextInputWrapper.tsx │ │ ├── index.tsx │ │ └── useTextInput.ts │ ├── ToastMessage │ │ ├── ToastMessage.stories.tsx │ │ ├── ToastMessage.tsx │ │ ├── ToastMessageProvider.tsx │ │ └── index.ts │ └── TopNavigation │ │ ├── TopNavigation.stories.tsx │ │ ├── TopNavigationBackButton.tsx │ │ ├── TopNavigationLeft.tsx │ │ ├── TopNavigationProgressBar.tsx │ │ ├── TopNavigationRight.tsx │ │ ├── TopNavigationTitle.tsx │ │ ├── TopNavigationWrapper.tsx │ │ └── index.tsx ├── constant │ ├── planet.ts │ └── recommendKeyword.ts ├── hooks │ ├── useIsMounted.hooks.ts │ └── usePlanetNavigate.ts ├── lib │ └── tanstackQuery │ │ └── getQueryClient.tsx ├── middleware.ts ├── mocks │ ├── browser.ts │ ├── comment │ │ ├── comment.mock.ts │ │ └── comment.mockHandler.ts │ ├── community │ │ ├── community.mock.ts │ │ └── community.mockHandler.ts │ ├── handlers.ts │ ├── idCard │ │ ├── idCard.mock.ts │ │ └── idCard.mockHandler.ts │ ├── image │ │ ├── image.mock.ts │ │ └── image.mockHandler.ts │ ├── index.ts │ ├── mock.util.ts │ ├── notification │ │ ├── notification.mock.ts │ │ └── notification.mockHandler.ts │ ├── nudge.util.ts │ ├── nudge │ │ ├── nudge.mock.ts │ │ └── nudge.mockHandler.ts │ ├── server.ts │ └── user │ │ ├── user.mock.ts │ │ └── user.mockHandler.ts ├── modules │ ├── CommentInput │ │ ├── ActiveCommentInput.client.tsx │ │ ├── CommentInput.client.tsx │ │ ├── CommentInput.stories.tsx │ │ ├── DisabledCommentInput.client.tsx │ │ ├── ReplyIndicator.client.tsx │ │ └── index.ts │ ├── CommentList │ │ ├── Comment │ │ │ ├── Comment.client.tsx │ │ │ ├── Comment.stories.tsx │ │ │ └── index.ts │ │ ├── CommentCommon │ │ │ ├── CommentOptions.client.tsx │ │ │ ├── Content.client.tsx │ │ │ ├── DeleteButton.client.tsx │ │ │ ├── Empty.client.tsx │ │ │ ├── Header.client.tsx │ │ │ ├── LikeCount.client.tsx │ │ │ ├── LikeIcon.client.tsx │ │ │ ├── ReplyHideButton.client.tsx │ │ │ ├── ReplyShowButton.client.tsx │ │ │ ├── ReplySubmitButton.client.tsx │ │ │ ├── ReportButton.client.tsx │ │ │ ├── UserProfile.client.tsx │ │ │ └── index.ts │ │ ├── CommentReplyList │ │ │ ├── CommentReply.client.tsx │ │ │ ├── CommentReplyList.client.tsx │ │ │ └── index.ts │ │ └── useLike.ts │ ├── CommunityAdmin │ │ ├── CommunityAdmin.stories.tsx │ │ ├── CommunityAdmin.tsx │ │ ├── CommunityAdmin.type.ts │ │ ├── CommunityAdminCreate.client.tsx │ │ ├── CommunityAdminEdit.client.tsx │ │ └── CommunityAdminEditForm.client.tsx │ ├── CommunityProfile │ │ ├── CommunityBgImage.client.tsx │ │ ├── CommunityLogoImage.tsx │ │ ├── CommunityProfile.stories.tsx │ │ ├── CommunityProfile.tsx │ │ └── index.ts │ ├── CreateIdCardButton │ │ ├── CreateIdCardButton.client.tsx │ │ ├── CreateIdCardButton.stories.tsx │ │ └── index.ts │ ├── IdCard │ │ ├── IdCard.client.tsx │ │ ├── IdCard.stories.tsx │ │ └── index.ts │ ├── IdCardCreation │ │ ├── Form │ │ │ ├── IdCardCreationForm.tsx │ │ │ └── index.ts │ │ ├── IdCardCreation.type.ts │ │ ├── IdCardCreationSteps.stories.tsx │ │ ├── IdCardCreationSteps.tsx │ │ ├── Step │ │ │ ├── BoardingStep.client.tsx │ │ │ ├── CompleteStep.client.tsx │ │ │ ├── KeywordContentImage.client.tsx │ │ │ ├── KeywordContentStep.client.tsx │ │ │ ├── KeywordStep.client.tsx │ │ │ ├── LoadingStep.client.tsx │ │ │ ├── ProfileStep.client.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── IdCardDetail │ │ ├── Intro.tsx │ │ ├── KeywordContentCard.tsx │ │ └── index.ts │ ├── IdCardEditButton │ │ ├── IdCardEditButton.client.tsx │ │ ├── IdCardEditButton.stories.tsx │ │ └── index.ts │ ├── IdCardEditor │ │ ├── Form │ │ │ ├── IdCardEditorForm.tsx │ │ │ └── index.ts │ │ ├── IdCardEditor.constant.ts │ │ ├── IdCardEditor.tsx │ │ ├── IdCardEditor.type.ts │ │ ├── IdCardEditorSteps.stories.tsx │ │ ├── Step │ │ │ ├── EditKeywordContentStep.client.tsx │ │ │ ├── EditKeywordStep.client.tsx │ │ │ ├── EditProfileInfoStep.client.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── InvitationButtons │ │ ├── InvitationButtons.client.tsx │ │ ├── InvitationButtons.stories.tsx │ │ └── index.tsx │ ├── KeywordContentEditCard │ │ ├── KeywordContentEditCard.client.tsx │ │ └── index.tsx │ ├── LoginStep │ │ ├── AppleLoginButton.client.tsx │ │ ├── LoginStep.client.tsx │ │ ├── LoginStep.stories.tsx │ │ ├── index.ts │ │ ├── kakaoLoginButton.client.tsx │ │ └── style.css │ ├── Notification │ │ ├── NewNotificationBadge.client.tsx │ │ ├── Notification.stories.tsx │ │ ├── NotificationItem.tsx │ │ ├── NotificationList.tsx │ │ ├── NotificationNoData.tsx │ │ ├── NotificationTab.client.tsx │ │ └── NotificationTabItem.client.tsx │ ├── NudgeItem │ │ ├── NudgeItem.stories.tsx │ │ ├── NudgeItem.tsx │ │ └── index.ts │ ├── NudgeList │ │ ├── NudgeList.client.tsx │ │ └── index.ts │ ├── Onboarding │ │ ├── CharacterCreation.type.ts │ │ ├── CharacterCreationSteps.client.tsx │ │ ├── CharacterCreationSteps.stories.tsx │ │ ├── Form │ │ │ ├── CharacterCreationForm.client.tsx │ │ │ ├── CharacterCreationForm.helper.ts │ │ │ └── index.ts │ │ ├── Question │ │ │ ├── CharacterQuestion.stories.tsx │ │ │ ├── CharacterQuestion.tsx │ │ │ └── CharacterQuestion.type.ts │ │ ├── Step │ │ │ ├── CharacterBoardingStep.tsx │ │ │ ├── CharacterCompleteStep.client.tsx │ │ │ ├── CharacterCompleteStep.stories.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── PlanetCreationButton │ │ ├── PlanetCreationButton.client.tsx │ │ ├── PlanetCreationButton.stories.tsx │ │ └── index.ts │ ├── PlanetEnterButton │ │ ├── PlanetEnterButton.client.tsx │ │ ├── PlanetEnterButton.stories.tsx │ │ └── index.ts │ ├── PlanetMenu │ │ ├── PlanetMenu.client.tsx │ │ ├── PlanetMenu.stories.tsx │ │ └── index.ts │ ├── PlanetSelector │ │ ├── CommunityList.client.tsx │ │ ├── PlanetSelector.client.tsx │ │ ├── PlanetSelector.stories.tsx │ │ └── index.ts │ └── UserMenu │ │ ├── UserMenu.client.tsx │ │ ├── UserMenu.stories.tsx │ │ └── index.ts ├── stores │ ├── comment.store.ts │ ├── community.store.ts │ └── toastMessage.store.ts ├── styles │ └── globals.css ├── types │ ├── api │ │ ├── index.ts │ │ └── response.type.ts │ ├── auth │ │ ├── index.ts │ │ ├── model.type.ts │ │ └── response.type.ts │ ├── comment │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ ├── community │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ ├── errorCodes.ts │ ├── idCard │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ ├── image │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ ├── notification │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ ├── nudge │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ ├── user │ │ ├── index.ts │ │ ├── model.type.ts │ │ ├── request.type.ts │ │ └── response.type.ts │ └── util.d.ts └── utils │ ├── auth │ ├── error.ts │ ├── getUserId.client.ts │ ├── getUserId.server.ts │ ├── loginProviders.ts │ ├── tokenHandlers.ts │ ├── tokenValidator.client.ts │ └── tokenValidator.server.ts │ ├── cookie.util.ts │ ├── fetch.ts │ ├── route │ └── route.ts │ ├── tailwind.util.ts │ ├── time.util.ts │ ├── util.common.ts │ ├── validate.ts │ └── variable.ts ├── tailwind.config.js └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @13th-7team-fe-review 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: docs 3 | about: 문서 변경 4 | title: '' 5 | labels: 'docs' 6 | --- 7 | 8 | ## 설명 9 | 10 | ## 할 일 11 | 12 | - [ ] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: feat 3 | about: 새로운 기능 추가 4 | title: '' 5 | labels: 'feat' 6 | --- 7 | 8 | ## 설명 9 | 10 | ## 할 일 11 | 12 | - [ ] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/fix.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: fix 3 | about: 트러블 슈팅 및 디버깅 4 | title: '' 5 | labels: 'fix' 6 | --- 7 | 8 | ## 설명 9 | 10 | ## 할 일 11 | 12 | - [ ] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: refactor 3 | about: 리팩터링 포인트 및 방법 4 | title: '' 5 | labels: 'refactor' 6 | --- 7 | 8 | ## 설명 9 | 10 | ## 할 일 11 | 12 | - [ ] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: style 3 | about: CSS 및 UI 디자인 변경 4 | title: '' 5 | labels: 'style' 6 | --- 7 | 8 | ## 설명 9 | 10 | ## 할 일 11 | 12 | - [ ] 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### ⛳️ Task 2 | 3 | ### ✍️ Note 4 | 5 | ### ⚡️ Test 6 | 7 | ### 📸 Screenshot 8 | 9 | | AS-IS | TO-BE | 10 | | ----- | ----- | 11 | | | | 12 | 13 | ### 📎 Reference 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | .env.development 32 | .env.production 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # editor 42 | .idea -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm build -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .next 3 | package.json 4 | package-lock.json 5 | public 6 | /**/*.png 7 | **/*.ico 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "arrowParens": "avoid", 11 | "endOfLine": "lf", 12 | "plugins": ["prettier-plugin-tailwindcss"] 13 | } 14 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-links', 7 | '@storybook/addon-essentials', 8 | '@storybook/addon-interactions', 9 | { 10 | name: '@storybook/addon-styling', 11 | options: { 12 | postCss: true, 13 | }, 14 | }, 15 | '@storybook/addon-viewport', 16 | 'storybook-addon-cookie', 17 | ], 18 | framework: { 19 | name: '@storybook/nextjs', 20 | options: {}, 21 | }, 22 | docs: { 23 | autodocs: 'tag', 24 | }, 25 | staticDirs: ['../public'], // msw 세팅을 위해 필요합니다. 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.quickSuggestions": { 3 | "strings": true 4 | }, 5 | "cSpell.words": ["Dtos", "kakao", "PIPI", "signin", "TOBBY"], 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | "editor.formatOnSave": true 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 위에서 도커 허브 node 이미지를 기반으로 로컬로 다운로드 및 캐싱 되었기 때문에 이미지를 가져올 수 있다. 2 | FROM node:18-alpine AS base 3 | 4 | # Install dependencies only when needed 5 | FROM base AS deps 6 | 7 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 8 | RUN apk add --no-cache libc6-compat 9 | 10 | WORKDIR /app 11 | 12 | COPY package.json pnpm-lock.yaml ./ 13 | 14 | RUN npm install -g pnpm && pnpm install 15 | 16 | FROM base AS builder 17 | WORKDIR /app 18 | COPY --from=deps /app/node_modules ./node_modules 19 | COPY . . 20 | 21 | #COPY .env.development .env.production 22 | RUN yarn build 23 | 24 | FROM base AS runner 25 | WORKDIR /app 26 | 27 | ENV NODE_ENV production 28 | 29 | RUN addgroup --system --gid 1001 nodejs 30 | RUN adduser --system --uid 1001 nextjs 31 | 32 | COPY --from=builder /app/public ./public 33 | COPY --from=builder /app/package.json ./package.json 34 | 35 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 36 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 37 | 38 | USER nextjs 39 | 40 | EXPOSE 3000 41 | ENV PORT 3000 42 | 43 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: redis 6 | container_name: redis 7 | hostname: redis 8 | ports: 9 | - "6379:6379" 10 | 11 | api-server: 12 | image: simmigyeong/depromeet 13 | container_name: api-server 14 | build: 15 | context: './Api' 16 | dockerfile: Dockerfile 17 | expose: 18 | - 8080 19 | ports: 20 | - 8080:8080 21 | depends_on: 22 | - redis 23 | 24 | front: 25 | image: simmigyeong/depromeet-fe 26 | container_name: front 27 | build: 28 | context: '../' 29 | dockerfile: Dockerfile 30 | args: 31 | ENV_VARIABLE: ${ENV_VARIABLE} 32 | NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE} 33 | expose: 34 | - 3000 35 | ports: 36 | - 3000:3000 37 | 38 | nginx: 39 | container_name: nginx 40 | image: nginx:latest 41 | restart: always 42 | volumes: 43 | - ./conf/nginx.conf:/etc/nginx/nginx.conf 44 | ports: 45 | - 80:80 46 | depends_on: 47 | - api-server 48 | - front -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/assets/images/BUDDY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/BUDDY.png -------------------------------------------------------------------------------- /public/assets/images/PIPI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/PIPI.png -------------------------------------------------------------------------------- /public/assets/images/TOBBY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/TOBBY.png -------------------------------------------------------------------------------- /public/assets/images/TRUE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/TRUE.png -------------------------------------------------------------------------------- /public/assets/images/apple_login_large_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/apple_login_large_wide.png -------------------------------------------------------------------------------- /public/assets/images/character-buddy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/character-buddy.png -------------------------------------------------------------------------------- /public/assets/images/character-pipi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/character-pipi.png -------------------------------------------------------------------------------- /public/assets/images/character-toby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/character-toby.png -------------------------------------------------------------------------------- /public/assets/images/character-true.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/character-true.png -------------------------------------------------------------------------------- /public/assets/images/congrats-clap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/congrats-clap.png -------------------------------------------------------------------------------- /public/assets/images/default-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/default-og-image.png -------------------------------------------------------------------------------- /public/assets/images/default-profile-image-buddy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/default-profile-image-buddy.png -------------------------------------------------------------------------------- /public/assets/images/default-profile-image-pipi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/default-profile-image-pipi.png -------------------------------------------------------------------------------- /public/assets/images/default-profile-image-tobby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/default-profile-image-tobby.png -------------------------------------------------------------------------------- /public/assets/images/default-profile-image-true.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/default-profile-image-true.png -------------------------------------------------------------------------------- /public/assets/images/default_planet_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/default_planet_logo.png -------------------------------------------------------------------------------- /public/assets/images/error-ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/error-ufo.png -------------------------------------------------------------------------------- /public/assets/images/explain-edit-id-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/explain-edit-id-card.png -------------------------------------------------------------------------------- /public/assets/images/explain-id-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/explain-id-card.png -------------------------------------------------------------------------------- /public/assets/images/id_card_creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/id_card_creation.png -------------------------------------------------------------------------------- /public/assets/images/invitation-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/invitation-og-image.png -------------------------------------------------------------------------------- /public/assets/images/invitation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/invitation.png -------------------------------------------------------------------------------- /public/assets/images/kakao_login_large_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/kakao_login_large_wide.png -------------------------------------------------------------------------------- /public/assets/images/memo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/memo.png -------------------------------------------------------------------------------- /public/assets/images/onboarding-planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/onboarding-planet.png -------------------------------------------------------------------------------- /public/assets/images/onboarding-question-food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/onboarding-question-food.png -------------------------------------------------------------------------------- /public/assets/images/onboarding-question-snack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/onboarding-question-snack.png -------------------------------------------------------------------------------- /public/assets/images/onboarding-question-ticket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/onboarding-question-ticket.png -------------------------------------------------------------------------------- /public/assets/images/onboarding-question-ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/onboarding-question-ufo.png -------------------------------------------------------------------------------- /public/assets/images/planet-cover-default-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/planet-cover-default-image.png -------------------------------------------------------------------------------- /public/assets/images/planet-create-result-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/planet-create-result-bg.png -------------------------------------------------------------------------------- /public/assets/images/planet-with-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/planet-with-shadow.png -------------------------------------------------------------------------------- /public/assets/images/planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/planet.png -------------------------------------------------------------------------------- /public/assets/images/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/rocket.png -------------------------------------------------------------------------------- /public/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/splash.png -------------------------------------------------------------------------------- /public/assets/images/talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/talk.png -------------------------------------------------------------------------------- /public/assets/images/ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/public/assets/images/ufo.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | Sentry.init({ 4 | dsn: 'https://89643f584abe40b9a6a7a6d501316a7a@o4505509160026112.ingest.sentry.io/4505509161140224', 5 | // Replay may only be enabled for the client-side 6 | integrations: [new Sentry.Replay()], 7 | 8 | // Set tracesSampleRate to 1.0 to capture 100% 9 | // of transactions for performance monitoring. 10 | // We recommend adjusting this value in production 11 | tracesSampleRate: 1.0, 12 | 13 | // Capture Replay for 10% of all sessions, 14 | // plus for 100% of sessions with an error 15 | replaysSessionSampleRate: 0.1, 16 | replaysOnErrorSampleRate: 1.0, 17 | 18 | // ... 19 | 20 | // Note: if you want to override the automatic release value, do not set a 21 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 22 | // that it will also get attached to your source maps 23 | }); 24 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | Sentry.init({ 4 | dsn: 'https://89643f584abe40b9a6a7a6d501316a7a@o4505509160026112.ingest.sentry.io/4505509161140224', 5 | 6 | // Set tracesSampleRate to 1.0 to capture 100% 7 | // of transactions for performance monitoring. 8 | // We recommend adjusting this value in production 9 | tracesSampleRate: 1.0, 10 | 11 | // ... 12 | 13 | // Note: if you want to override the automatic release value, do not set a 14 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 15 | // that it will also get attached to your source maps 16 | }); 17 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | Sentry.init({ 4 | dsn: 'https://89643f584abe40b9a6a7a6d501316a7a@o4505509160026112.ingest.sentry.io/4505509161140224', 5 | 6 | // Set tracesSampleRate to 1.0 to capture 100% 7 | // of transactions for performance monitoring. 8 | // We recommend adjusting this value in production 9 | tracesSampleRate: 1.0, 10 | 11 | // ... 12 | 13 | // Note: if you want to override the automatic release value, do not set a 14 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 15 | // that it will also get attached to your source maps 16 | }); 17 | -------------------------------------------------------------------------------- /src/api/config/customError.ts: -------------------------------------------------------------------------------- 1 | export class ApiError extends Error { 2 | success: boolean; 3 | statusCode: number; 4 | errorCode: string; 5 | reason: string; 6 | constructor(success: boolean, statusCode: number, errorCode: string, message: string) { 7 | super(message); 8 | this.success = success; 9 | this.statusCode = statusCode; 10 | this.errorCode = errorCode; 11 | this.reason = message; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/api/config/privateApi.server.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { CustomInstance } from './api.types'; 4 | import { onResponse } from './interceptor'; 5 | import { onRequestServer, onResponseErrorServer } from './interceptor.server'; 6 | import { ROOT_API_URL } from './requestUrl'; 7 | 8 | export const privateApi: CustomInstance = axios.create({ 9 | baseURL: ROOT_API_URL, 10 | }); 11 | 12 | privateApi.defaults.timeout = 2500; 13 | 14 | privateApi.interceptors.request.use(onRequestServer); 15 | 16 | privateApi.interceptors.response.use(onResponse, onResponseErrorServer); 17 | -------------------------------------------------------------------------------- /src/api/config/privateApi.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import axios from 'axios'; 3 | 4 | import { CustomInstance } from '~/api/config/api.types'; 5 | 6 | import { onRequestError, onResponse } from './interceptor'; 7 | import { onRequestClient, onResponseErrorClient } from './interceptor.client'; 8 | import { ROOT_API_URL } from './requestUrl'; 9 | 10 | const privateApi: CustomInstance = axios.create({ 11 | baseURL: ROOT_API_URL, 12 | }); 13 | 14 | privateApi.defaults.timeout = 2500; 15 | 16 | privateApi.interceptors.request.use(onRequestClient, onRequestError); 17 | 18 | privateApi.interceptors.response.use(onResponse, onResponseErrorClient); 19 | 20 | export default privateApi; 21 | -------------------------------------------------------------------------------- /src/api/config/publicApi.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { CustomInstance } from '~/api/config/api.types'; 4 | 5 | import { onResponse, onResponseError } from './interceptor'; 6 | import { ROOT_API_URL } from './requestUrl'; 7 | 8 | const publicApi: CustomInstance = axios.create({ 9 | baseURL: ROOT_API_URL, 10 | }); 11 | 12 | publicApi.defaults.timeout = 2500; 13 | 14 | publicApi.interceptors.response.use(onResponse, onResponseError); 15 | 16 | export default publicApi; 17 | -------------------------------------------------------------------------------- /src/api/config/requestUrl.ts: -------------------------------------------------------------------------------- 1 | export const ROOT_API_URL = process.env.ROOT_API_URL ?? 'https://ding-dong-planet.com/api'; 2 | export const ROOT_URL = process.env.ROOT_URL ?? 'https://ding-dong-planet.com'; 3 | -------------------------------------------------------------------------------- /src/api/domain/auth.api.client.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, UseQueryOptions } from '@tanstack/react-query'; 2 | 3 | import { AuthResponse } from '~/types/auth'; 4 | import { generateCookiesKeyValues } from '~/utils/auth/tokenHandlers'; 5 | 6 | import publicApi from '../config/publicApi'; 7 | 8 | export const login = async (code: string | null) => { 9 | const origin = window.location.origin; 10 | const authData = await publicApi.post('/auth/login/kakao', { 11 | authCode: code, 12 | redirectUri: `${origin}/auth/callback/kakao`, 13 | }); 14 | if (authData.data) { 15 | const cookies = generateCookiesKeyValues(authData.data as AuthResponse); 16 | cookies.forEach(([key, value]) => { 17 | document.cookie = `${key}=${value}; path=/;`; 18 | }); 19 | } 20 | 21 | return authData; 22 | }; 23 | 24 | export const useLogin = (code: string | null, options?: UseQueryOptions) => { 25 | return useQuery(['login'], async () => login(code), options); 26 | }; 27 | 28 | export const reissue = async (refreshToken: string) => 29 | publicApi.get('/auth/login/reissue', { 30 | headers: { 'REFRESH-TOKEN': refreshToken }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/api/domain/auth.api.server.ts: -------------------------------------------------------------------------------- 1 | import { AuthResponse } from '~/types/auth'; 2 | 3 | import publicApi from '../config/publicApi'; 4 | 5 | export const reissue = async (refreshToken: string) => 6 | publicApi.get('/auth/login/reissue', { 7 | headers: { 'REFRESH-TOKEN': refreshToken }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/api/domain/comment/comment.api.server.ts: -------------------------------------------------------------------------------- 1 | import { privateApi } from '~/api/config/privateApi.server'; 2 | import { 3 | CommentCountGetRequest, 4 | CommentCountGetResponse, 5 | CommentGetRequest, 6 | CommentGetResponse, 7 | } from '~/types/comment'; 8 | 9 | export const getCommentsServer = ({ idCardId, pageParam = 0 }: CommentGetRequest) => 10 | privateApi.get(`/id-cards/${idCardId}/comments?page=${pageParam}&size=10`); 11 | 12 | export const getCommentCountsServer = ({ idCardId }: CommentCountGetRequest) => 13 | privateApi.get(`/id-cards/${idCardId}/comments-count`); 14 | -------------------------------------------------------------------------------- /src/api/domain/idCard.api.server.ts: -------------------------------------------------------------------------------- 1 | import { privateApi } from '~/api/config/privateApi.server'; 2 | import { CommunityMyIdCardDetailResponse, IdCardDetailResponse } from '~/types/idCard'; 3 | 4 | export const getIdCardDetailServer = (idCardId: number) => 5 | privateApi.get(`/id-cards/${idCardId}`); 6 | 7 | export const getCommunityMyIdCardDetailServer = (communityId: number) => 8 | privateApi.get(`/communities/${communityId}/users/idCards`); 9 | -------------------------------------------------------------------------------- /src/api/domain/image.api.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, UseMutationOptions } from '@tanstack/react-query'; 2 | import { AxiosError } from 'axios'; 3 | 4 | import privateApi from '~/api/config/privateApi'; 5 | import { ImageFileRequest } from '~/types/image/request.type'; 6 | import { IamgeUrlResponse } from '~/types/image/response.type'; 7 | 8 | export const postImageUrl = (imageFile: ImageFileRequest) => { 9 | const formData = new FormData(); 10 | formData.append('image', imageFile); 11 | 12 | return privateApi.post('/images', formData, { 13 | headers: { 'Content-Type': 'multipart/form-data' }, 14 | }); 15 | }; 16 | 17 | export const usePostImageUrl = ( 18 | options?: Omit, 'mutationFn'>, 19 | ) => 20 | useMutation({ 21 | mutationFn: postImageUrl, 22 | ...options, 23 | }); 24 | -------------------------------------------------------------------------------- /src/api/domain/notification.api.server.ts: -------------------------------------------------------------------------------- 1 | import { privateApi } from '~/api/config/privateApi.server'; 2 | import { NotificationGetResponse } from '~/types/notification'; 3 | 4 | export const getNotificationsServer = () => 5 | privateApi.get(`/notifications`); 6 | -------------------------------------------------------------------------------- /src/api/domain/user.api.server.ts: -------------------------------------------------------------------------------- 1 | import { privateApi } from '~/api/config/privateApi.server'; 2 | import { UserInfoResponse } from '~/types/user'; 3 | 4 | export const getUserInfoServer = () => privateApi.get(`/user/profile`); 5 | -------------------------------------------------------------------------------- /src/app/admin/planet/[id]/components/AdminPlanetDetailPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AdminPlanetDetailPage'; 2 | -------------------------------------------------------------------------------- /src/app/admin/planet/[id]/edit/components/PlanetAdminEdit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PlanetAdminEdit'; 2 | -------------------------------------------------------------------------------- /src/app/admin/planet/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { PlanetAdminEdit } from '~/app/admin/planet/[id]/edit/components/PlanetAdminEdit'; 2 | 3 | type AdminCommunityEditPageProps = { 4 | params: { 5 | id: number; 6 | }; 7 | }; 8 | 9 | const AdminCommunityEditPage = ({ params: { id } }: AdminCommunityEditPageProps) => { 10 | return ; 11 | }; 12 | 13 | export default AdminCommunityEditPage; 14 | -------------------------------------------------------------------------------- /src/app/admin/planet/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | import { getCommunityDetailServer } from '~/api/domain/community.api.server'; 5 | 6 | type AdminCommunityDetailLayoutProps = { 7 | params: { 8 | id: string; 9 | }; 10 | }; 11 | 12 | export async function generateMetadata({ 13 | params, 14 | }: AdminCommunityDetailLayoutProps): Promise { 15 | const communityId = Number(params.id); 16 | 17 | try { 18 | const { communityDetailsDto } = await getCommunityDetailServer(communityId); 19 | const title = `${communityDetailsDto.title} / 관리자 페이지`; 20 | return { 21 | title, 22 | }; 23 | } catch (error) { 24 | console.error(error); 25 | return { 26 | title: '', 27 | }; 28 | } 29 | } 30 | 31 | const AdminCommunityDetailLayout = ({ 32 | children, 33 | }: PropsWithChildren) => { 34 | return
{children}
; 35 | }; 36 | 37 | export default AdminCommunityDetailLayout; 38 | -------------------------------------------------------------------------------- /src/app/admin/planet/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { AdminPlanetDetail } from '~/app/admin/planet/[id]/components/AdminPlanetDetailPage'; 2 | 3 | type AdminCommunityDetailPageProps = { 4 | params: { 5 | id: string; 6 | }; 7 | }; 8 | 9 | const AdminCommunityDetailPage = ({ params: { id } }: AdminCommunityDetailPageProps) => { 10 | return ; 11 | }; 12 | 13 | export default AdminCommunityDetailPage; 14 | -------------------------------------------------------------------------------- /src/app/admin/planet/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { CommunityAdminCreate } from '~/modules/CommunityAdmin/CommunityAdminCreate.client'; 2 | 3 | const AdminCommunityCreatePage = () => { 4 | return ; 5 | }; 6 | 7 | export default AdminCommunityCreatePage; 8 | -------------------------------------------------------------------------------- /src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/src/app/apple-icon.png -------------------------------------------------------------------------------- /src/app/auth/callback/kakao/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter, useSearchParams } from 'next/navigation'; 4 | 5 | import { useLogin } from '~/api/domain/auth.api.client'; 6 | import { generateCookiesKeyValues } from '~/utils/auth/tokenHandlers'; 7 | 8 | const KakaoCallbackPage = () => { 9 | const searchParams = useSearchParams(); 10 | const router = useRouter(); 11 | 12 | const code = searchParams.get('code'); 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | const authData = useLogin(code, { 15 | onSuccess: data => { 16 | for (const [cookieKey, cookieValue] of generateCookiesKeyValues(data)) { 17 | document.cookie = `${cookieKey}=${cookieValue}; path=/;`; 18 | } 19 | router.push('/'); 20 | }, 21 | }); 22 | 23 | return <>; 24 | }; 25 | 26 | export default KakaoCallbackPage; 27 | -------------------------------------------------------------------------------- /src/app/auth/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginStep } from '~/modules/LoginStep'; 2 | 3 | const SignInPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default SignInPage; 12 | -------------------------------------------------------------------------------- /src/app/auth/signin/signInProviders.server.tsx: -------------------------------------------------------------------------------- 1 | import 'server-only'; 2 | 3 | import { getProviders } from 'next-auth/react'; 4 | 5 | const SignInProviders = async () => { 6 | const providers = await getProviders(); 7 | if (!providers) return <>; 8 | return
; 9 | }; 10 | 11 | export { SignInProviders }; 12 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/depromeet/Ding-dong-fe/0a10a8513a2c91c69a4a1f6ac130ff40b1c66274/src/app/icon.png -------------------------------------------------------------------------------- /src/app/invitation/[code]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | const INVITATION_OG_TITLE = '초대'; 4 | const INVITATION_OG_DESC = '딩동! 초대장이 도착했어요. 지금 바로 행성에 방문해 보세요!'; 5 | const INVITATION_OG_IMAGE = '/assets/images/invitation-og-image.png'; 6 | 7 | export const metadata = { 8 | title: INVITATION_OG_TITLE, 9 | description: { 10 | default: INVITATION_OG_DESC, 11 | }, 12 | openGraph: { 13 | title: INVITATION_OG_TITLE, 14 | description: INVITATION_OG_DESC, 15 | images: [INVITATION_OG_IMAGE], 16 | }, 17 | twitter: { 18 | title: INVITATION_OG_TITLE, 19 | description: INVITATION_OG_DESC, 20 | images: [INVITATION_OG_IMAGE], 21 | }, 22 | }; 23 | 24 | const InvitationLayout = ({ children }: PropsWithChildren) => { 25 | return
{children}
; 26 | }; 27 | 28 | export default InvitationLayout; 29 | -------------------------------------------------------------------------------- /src/app/my-page/[communityId]/components/MyPageIdCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MyPageIdCard.client'; 2 | -------------------------------------------------------------------------------- /src/app/my-page/[communityId]/edit/components/MyPageEditIdCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MyPageEditIdCard'; 2 | -------------------------------------------------------------------------------- /src/app/my-page/[communityId]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { MyPageEditIdCard } from '~/app/my-page/[communityId]/edit/components/MyPageEditIdCard'; 2 | 3 | type MyPageEditProps = { 4 | params: { 5 | communityId: number; 6 | }; 7 | }; 8 | 9 | const MyPageEdit = ({ params: { communityId } }: MyPageEditProps) => { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default MyPageEdit; 18 | -------------------------------------------------------------------------------- /src/app/my-page/[communityId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | import { getCommunityDetailServer } from '~/api/domain/community.api.server'; 5 | 6 | type MyPageLayoutProps = { 7 | params: { 8 | communityId: string; 9 | }; 10 | }; 11 | 12 | export async function generateMetadata({ params }: MyPageLayoutProps): Promise { 13 | const communityId = Number(params.communityId); 14 | 15 | try { 16 | const { communityDetailsDto } = await getCommunityDetailServer(communityId); 17 | const title = communityDetailsDto.title; 18 | return { 19 | title, 20 | }; 21 | } catch (error) { 22 | console.error(error); 23 | return { 24 | title: '', 25 | }; 26 | } 27 | } 28 | 29 | const MyPageLayout = ({ children }: PropsWithChildren) => { 30 | return
{children}
; 31 | }; 32 | 33 | export default MyPageLayout; 34 | -------------------------------------------------------------------------------- /src/app/my-page/config/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | import { BottomNavigation } from '~/components/BottomNavigation'; 4 | import { TopNavigation } from '~/components/TopNavigation'; 5 | 6 | const Layout = ({ children }: PropsWithChildren) => { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 |

설정

15 |
16 | 17 |
18 | {children} 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default Layout; 25 | -------------------------------------------------------------------------------- /src/app/my-page/empty/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { BottomNavigation } from '~/components/BottomNavigation'; 4 | import { ThreeDotsVerticalIcon } from '~/components/Icon'; 5 | import { TopNavigation } from '~/components/TopNavigation'; 6 | import { CreateIdCardButton } from '~/modules/CreateIdCardButton'; 7 | import { PlanetCreationButton } from '~/modules/PlanetCreationButton'; 8 | 9 | const EmptyPlanet = () => { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 | ); 31 | }; 32 | 33 | export default EmptyPlanet; 34 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | import { Button } from '~/components/Button'; 7 | 8 | const NotFound = () => { 9 | const router = useRouter(); 10 | return ( 11 |
12 | error-ufo 19 |

찾을 수 없는 페이지예요

20 |
21 |

입력한 주소의 페이지를 찾을 수 없어요.

22 |

주소를 다시 한 번 확인해 주세요.

23 |
24 |
25 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default NotFound; 34 | -------------------------------------------------------------------------------- /src/app/notification/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | import { BottomNavigation } from '~/components/BottomNavigation'; 4 | import { TopNavigation } from '~/components/TopNavigation'; 5 | 6 | export const metadata = { 7 | title: '알림', 8 | }; 9 | 10 | const NotificationLayout = ({ children }: PropsWithChildren) => { 11 | return ( 12 | <> 13 | 14 | 15 |
16 |

알림

17 |
18 |
19 |
20 |
21 | {children} 22 |
23 | 24 | 25 | ); 26 | }; 27 | 28 | export default NotificationLayout; 29 | -------------------------------------------------------------------------------- /src/app/onboarding/page.tsx: -------------------------------------------------------------------------------- 1 | import { CharacterCreationSteps } from '~/modules/Onboarding'; 2 | 3 | const OnBoardingPage = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default OnBoardingPage; 12 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | const Home = () => { 4 | return ( 5 |
6 | splash 14 |
15 | ); 16 | }; 17 | 18 | export default Home; 19 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/components/CommunityDetail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommunityDetail'; 2 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/components/CommunityIdCards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommunityIdCards.client'; 2 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/components/IdCardCreatorButton/IdCardCreatorButton.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | 5 | import { useCheckIdCards } from '~/api/domain/community.api'; 6 | import { ChevronRightIcon } from '~/components/Icon'; 7 | 8 | type IdCardCreatorButtonProps = { 9 | communityId: number; 10 | }; 11 | 12 | export const IdCardCreatorButton = ({ communityId }: IdCardCreatorButtonProps) => { 13 | const { data: checkIdCard } = useCheckIdCards(communityId); 14 | const router = useRouter(); 15 | 16 | const onClickCreateIdCardButton = () => { 17 | router.push(`/planet/${communityId}/id-card/create`); 18 | }; 19 | 20 | return ( 21 |
22 | {checkIdCard?.userMakeIdCard || ( 23 | 30 | )} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/components/IdCardCreatorButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCardCreatorButton.client'; 2 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/[idCardId]/components/CommentCount/CommentCount.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Suspense } from 'react'; 3 | 4 | import { useGetCommentCounts } from '~/api/domain/comment/comment.api'; 5 | import RetryErrorBoundary from '~/components/ErrorBoundary/RetryErrorBoundary.client'; 6 | 7 | type CommentCountProps = { 8 | idCardId: number; 9 | }; 10 | 11 | const CommentCountComponent = ({ idCardId }: CommentCountProps) => { 12 | const { data: totalCommentCount } = useGetCommentCounts({ idCardId }); 13 | 14 | return ( 15 |
16 | {totalCommentCount && 댓글 {totalCommentCount.count}개} 17 |
18 | ); 19 | }; 20 | 21 | export const CommentCount = ({ idCardId }: CommentCountProps) => { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/[idCardId]/components/CommentCount/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommentCount.client'; 2 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/[idCardId]/components/CommentList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommentList.client'; 2 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/[idCardId]/components/IdCardDetail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCardDetail'; 2 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/[idCardId]/components/Nudge/Button/NudgeButton.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from 'react'; 2 | 3 | import { NudgeCloseIcon } from '~/components/Icon'; 4 | import { NudgeIconSelector } from '~/components/NudgeIconSelector'; 5 | import { NudgeIconSelectorType } from '~/types/nudge'; 6 | import { twMerge } from '~/utils/tailwind.util'; 7 | 8 | type NudgeButtonProps = ButtonHTMLAttributes & { 9 | nudgeType: NudgeIconSelectorType; 10 | onClick?: () => void; 11 | isOpen?: boolean; 12 | }; 13 | export const NudgeButton = ({ 14 | className, 15 | nudgeType, 16 | onClick, 17 | isOpen, 18 | ...props 19 | }: NudgeButtonProps) => ( 20 | 31 | ); 32 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/[idCardId]/components/Nudge/Nudge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Nudge } from '~/app/planet/[communityId]/id-card/[idCardId]/components/Nudge/Nudge.client'; 4 | 5 | const meta: Meta = { 6 | title: 'components/Nudge', 7 | component: Nudge, 8 | }; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = {}; 13 | 14 | export default meta; 15 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/id-card/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { IdCardCreationSteps } from '~/modules/IdCardCreation'; 2 | 3 | type IdCardCreationPageProps = { 4 | params: { 5 | communityId: string; 6 | }; 7 | }; 8 | 9 | const IdCardCreationPage = ({ params: { communityId } }: IdCardCreationPageProps) => { 10 | return ( 11 | <> 12 |
13 | 14 |
15 |
16 | 17 | ); 18 | }; 19 | 20 | export default IdCardCreationPage; 21 | -------------------------------------------------------------------------------- /src/app/planet/[communityId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | import { getCommunityDetailServer } from '~/api/domain/community.api.server'; 5 | 6 | type PlanetLayoutProps = { 7 | params: { 8 | communityId: string; 9 | }; 10 | }; 11 | 12 | export async function generateMetadata({ params }: PlanetLayoutProps): Promise { 13 | const communityId = Number(params.communityId); 14 | 15 | try { 16 | const { communityDetailsDto } = await getCommunityDetailServer(communityId); 17 | const title = communityDetailsDto.title; 18 | return { 19 | title, 20 | }; 21 | } catch (error) { 22 | console.error(error); 23 | return { 24 | title: '', 25 | }; 26 | } 27 | } 28 | 29 | const PlanetLayout = ({ children }: PropsWithChildren) => { 30 | return
{children}
; 31 | }; 32 | 33 | export default PlanetLayout; 34 | -------------------------------------------------------------------------------- /src/app/planet/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | import { ThreeDotsVerticalIcon } from '~/components/Icon'; 5 | import { TopNavigation } from '~/components/TopNavigation'; 6 | import { PlanetEnterButton } from '~/modules/PlanetEnterButton'; 7 | 8 | const PlanetPage = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

아직 소속된 행성이 없네요!

22 |

행성을 만들거나 초대된 행성으로 이동해보세요.

23 | planet 24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default PlanetPage; 31 | -------------------------------------------------------------------------------- /src/components/BottomNavigation/BottomNavigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from '@storybook/react'; 2 | 3 | import { BottomNavigation } from './BottomNavigation'; 4 | 5 | export default { 6 | title: 'components/BottomNavigation', 7 | component: BottomNavigation, 8 | }; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | render: () => , 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/BottomNavigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BottomNavigation'; 2 | -------------------------------------------------------------------------------- /src/components/BottomSheet/BottomSheetContent.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PropsWithChildren } from 'react'; 4 | 5 | export const BottomSheetContent = ({ children }: PropsWithChildren) => { 6 | return
{children}
; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/BottomSheet/BottomSheetFooter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PropsWithChildren } from 'react'; 4 | 5 | import { BottomSheetFooterButton } from '~/components/BottomSheet/BottomSheetFooterButton'; 6 | 7 | export const BottomSheetFooter = ({ children }: PropsWithChildren) => { 8 | return
{children}
; 9 | }; 10 | 11 | BottomSheetFooter.Button = BottomSheetFooterButton; 12 | -------------------------------------------------------------------------------- /src/components/BottomSheet/BottomSheetFooterButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button, ButtonProps } from '~/components/Button'; 4 | 5 | export const BottomSheetFooterButton = ({ 6 | size = 'large', 7 | color = 'primary', 8 | className = 'text-b1 h-[50px]', 9 | ...buttonProps 10 | }: ButtonProps) => { 11 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | export * from './TextButton'; 3 | -------------------------------------------------------------------------------- /src/components/Chip/Chip.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Chip } from './Chip'; 4 | 5 | const meta: Meta = { 6 | title: 'components/Chip', 7 | component: Chip, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | 19 | export const Selected: Story = { 20 | render: () => , 21 | }; 22 | 23 | export const WithCancelIcon: Story = { 24 | render: () => , 25 | }; 26 | export const WithPlusIcon: Story = { 27 | render: () => , 28 | }; 29 | 30 | export const SelectedWithIcon: Story = { 31 | render: () => , 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/Chip/index.ts: -------------------------------------------------------------------------------- 1 | export { Chip } from './Chip'; 2 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/ConfirmCreateIdCard/ConfirmCreateIdCard.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '~/components/Button'; 3 | import { TextButton } from '~/components/Button'; 4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup'; 5 | import Popup from '~/components/Popup/Popup'; 6 | 7 | // const TITLE = '주민증이 없으면 댓글을 남길 수 없어요'; 8 | const DESC = '주민증이 없으면 댓글을 남길 수 없어요'; 9 | 10 | type ConfirmCreateIdCardProps = { 11 | confirm: (type: ConfirmType) => void; 12 | }; 13 | 14 | export const ConfirmCreateIdCard = ({ confirm }: ConfirmCreateIdCardProps) => { 15 | const buttons = ( 16 |
17 | 20 | confirm('CANCEL')} 22 | type="button" 23 | className="rounded-xl pt-13pxr text-b3 text-grey-900" 24 | > 25 | 다음에 만들기 26 | 27 |
28 | ); 29 | 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/ConfirmCreateIdCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConfirmCreateIdCard.client'; 2 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/ConfirmDeleteKeyword/ConfirmDeleteKeyword.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '~/components/Button'; 3 | import { TextButton } from '~/components/Button'; 4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup'; 5 | import Popup from '~/components/Popup/Popup'; 6 | 7 | const TITLE = '키워드를 삭제하시겠어요?'; 8 | const DESC = '키워드 세부 내용까지 없어져요'; 9 | 10 | type ConfirmDeleteKeywordProps = { 11 | confirm: (type: ConfirmType) => void; 12 | }; 13 | 14 | export const ConfirmDeleteKeyword = ({ confirm }: ConfirmDeleteKeywordProps) => { 15 | const buttons = ( 16 |
17 | 20 | confirm('OK')} 22 | type="button" 23 | className="rounded-xl pt-13pxr text-b3 text-grey-900" 24 | > 25 | 삭제하기 26 | 27 |
28 | ); 29 | 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/ConfirmDeleteKeyword/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConfirmDeleteKeyword.client'; 2 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/ConfirmUnSave/ConfirmUnSave.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '~/components/Button'; 3 | import { TextButton } from '~/components/Button'; 4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup'; 5 | import Popup from '~/components/Popup/Popup'; 6 | 7 | const TITLE = '저장되지 않은 변경사항이 있어요'; 8 | const DESC = '변경 사항을 취소할까요?'; 9 | 10 | type ConfirmUnSaveProps = { 11 | confirm: (type: ConfirmType) => void; 12 | }; 13 | 14 | export const ConfirmUnSave = ({ confirm }: ConfirmUnSaveProps) => { 15 | const buttons = ( 16 |
17 | 20 | confirm('OK')} 22 | type="button" 23 | className="rounded-xl pt-13pxr text-b3 text-grey-900" 24 | > 25 | 네, 저장하지 않을래요 26 | 27 |
28 | ); 29 | 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/ConfirmUnSave/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConfirmUnSave.client'; 2 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/CopyInvitation/CopyInvitation.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '~/components/Button'; 3 | import { TextButton } from '~/components/Button'; 4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup'; 5 | import Popup from '~/components/Popup/Popup'; 6 | 7 | const TITLE = '초대 링크 복사 완료'; 8 | const DESC = '초대 링크를 공유해 보세요'; 9 | 10 | type CopyInvitationProps = { 11 | confirm: (type: ConfirmType) => void; 12 | }; 13 | 14 | export const CopyInvitation = ({ confirm }: CopyInvitationProps) => { 15 | const buttons = ( 16 |
17 | 20 | confirm('CANCEL')} 22 | type="button" 23 | className="rounded-xl pt-13pxr text-b3 text-grey-900" 24 | > 25 | 닫기 26 | 27 |
28 | ); 29 | 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/CopyInvitation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CopyInvitation.client'; 2 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/SimpleConfirmPopup/SimpleConfirmPopup.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '~/components/Button'; 3 | import { TextButton } from '~/components/Button'; 4 | import { ConfirmType } from '~/components/ConfirmPopup/useConfirmPopup'; 5 | import Popup from '~/components/Popup/Popup'; 6 | 7 | type SimpleConfirmPopupProps = { 8 | confirm: (type: ConfirmType) => void; 9 | title: string; 10 | description: string; 11 | confirmText?: string; 12 | cancelText?: string; 13 | }; 14 | 15 | export const SimpleConfirmPopup = ({ 16 | confirm, 17 | title, 18 | description, 19 | confirmText, 20 | cancelText, 21 | }: SimpleConfirmPopupProps) => { 22 | const buttons = ( 23 |
24 | 27 | confirm('CANCEL')} 29 | type="button" 30 | className="rounded-xl pt-13pxr text-b3 text-grey-900" 31 | > 32 | {cancelText} 33 | 34 |
35 | ); 36 | 37 | return ; 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/SimpleConfirmPopup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SimpleConfirmPopup.client'; 2 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConfirmDeleteKeyword'; 2 | export * from './ConfirmUnSave'; 3 | export * from './CopyInvitation'; 4 | export * from './SimpleConfirmPopup'; 5 | export * from './useConfirmPopup'; 6 | -------------------------------------------------------------------------------- /src/components/ConfirmPopup/useConfirmPopup.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | /** 4 | * 단순 실행만 할 경우 "OPEN" 5 | */ 6 | export type ConfirmType = 'OK' | 'CANCEL' | 'OPEN'; 7 | 8 | export const useConfirmPopup = (initialState = false) => { 9 | const [isOpen, setIsOpen] = useState(initialState); 10 | 11 | const [resolveFunc, setResolveFunc] = useState<((result: boolean) => void) | null>(null); 12 | 13 | const openPopup = () => { 14 | setIsOpen(true); 15 | 16 | return new Promise(resolve => { 17 | setResolveFunc(() => resolve); 18 | }); 19 | }; 20 | 21 | const closePopup = () => { 22 | setIsOpen(false); 23 | }; 24 | 25 | const confirm = (type: ConfirmType = 'OPEN') => { 26 | const isOk = type === 'OK'; 27 | if (resolveFunc) { 28 | resolveFunc(isOk); 29 | setResolveFunc(null); 30 | } 31 | }; 32 | 33 | return { 34 | isOpen, 35 | openPopup, 36 | closePopup, 37 | confirm, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { tw } from '~/utils/tailwind.util'; 2 | 3 | type DividerProps = { 4 | className?: string; 5 | }; 6 | 7 | export const Divider = ({ className = 'bg-grey-100' }: DividerProps) => { 8 | return
; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Divider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Divider'; 2 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/RetryErrorBoundary.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ErrorBoundary } from 'react-error-boundary'; 4 | 5 | const RetryErrorBoundary = ({ children }: { children: React.ReactNode }) => { 6 | return ( 7 | ( 9 |
10 |

문제가 발생했습니다

11 |

페이지를 불러오는데 실패했습니다.

12 | 18 |
19 | )} 20 | > 21 | {children} 22 |
23 | ); 24 | }; 25 | 26 | export default RetryErrorBoundary; 27 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RetryErrorBoundary.client'; 2 | -------------------------------------------------------------------------------- /src/components/HydrationProvider/HydrationProvider.tsx: -------------------------------------------------------------------------------- 1 | import { dehydrate, Hydrate, QueryClient } from '@tanstack/react-query'; 2 | import { type QueryFunction, QueryKey } from '@tanstack/react-query'; 3 | import { cache, PropsWithChildren } from 'react'; 4 | 5 | type HydrationProviderProps = { 6 | queryKey: QueryKey; 7 | queryFn: QueryFunction; 8 | }; 9 | 10 | export const HydrationProvider = async ({ 11 | children, 12 | queryKey, 13 | queryFn, 14 | }: PropsWithChildren) => { 15 | const getQueryClient = cache(() => new QueryClient()); 16 | 17 | const queryClient = getQueryClient(); 18 | await queryClient.prefetchQuery(queryKey, queryFn); 19 | const dehydratedState = dehydrate(queryClient); 20 | 21 | return {children}; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/HydrationProvider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HydrationProvider'; 2 | -------------------------------------------------------------------------------- /src/components/Icon/ArrowVerticalIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | export const ArrowVerticalIcon = ({ 7 | className, 8 | size, 9 | width, 10 | height, 11 | ...rest 12 | }: ComponentProps) => { 13 | return ( 14 | 23 | 30 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/Icon/CancelIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | export const CancelIcon = ({ 7 | className, 8 | size, 9 | width, 10 | height, 11 | ...rest 12 | }: ComponentProps) => { 13 | return ( 14 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const CancelBoldIcon = ({ 28 | className, 29 | size, 30 | width, 31 | height, 32 | ...rest 33 | }: ComponentProps) => { 34 | return ( 35 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Icon/CheckCircleFillIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | export const CheckCircleFillIcon = ({ 7 | className, 8 | size, 9 | width, 10 | height, 11 | ...rest 12 | }: ComponentProps) => { 13 | return ( 14 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Icon/ChevronLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const ChevronLeftIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Icon/ChevronRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const ChevronRightIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Icon/DashIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const DashIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Icon/EyeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const EyeIcon = ({ ...rest }: ComponentProps) => { 6 | return ( 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Icon/HeartFillIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const HeartFillIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Icon/HeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const HeartIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Icon/HomeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const HomeIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Icon/KakaoIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const KakaoIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Icon/NudgeCloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const NudgeCloseIcon = ({ ...rest }: ComponentProps) => { 6 | return ( 7 | 15 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Icon/NudgeMessageIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Svg } from '~/components/Svg'; 2 | 3 | export const NudgeMessageIcon = () => { 4 | return ( 5 | 12 | 13 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Icon/PersonIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const PersonIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 18 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Icon/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const PlusIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Icon/QuestionCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const QuestionCircleIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Icon/SendIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | 5 | export const SendIcon = ({ 6 | className, 7 | size, 8 | width, 9 | height, 10 | ...rest 11 | }: ComponentProps) => { 12 | return ( 13 | 14 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Icon/ThreeDotsVerticalIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { Svg } from '~/components/Svg'; 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | export const ThreeDotsVerticalIcon = ({ 7 | className, 8 | size, 9 | width, 10 | height, 11 | ...rest 12 | }: ComponentProps) => { 13 | return ( 14 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ArrowLeftIcon'; 2 | export * from './ArrowVerticalIcon'; 3 | export * from './CameraIcon'; 4 | export * from './CancelCircleIcon'; 5 | export * from './CancelIcon'; 6 | export * from './CelebrationIcon'; 7 | export * from './ChatBubbleIcon'; 8 | export * from './CheckCircleFillIcon'; 9 | export * from './ChevronLeftIcon'; 10 | export * from './ChevronRightIcon'; 11 | export * from './DashIcon'; 12 | export * from './EyeIcon'; 13 | export * from './GearIcon'; 14 | export * from './HeartExchangeIcon'; 15 | export * from './HeartFillIcon'; 16 | export * from './HeartIcon'; 17 | export * from './HomeIcon'; 18 | export * from './NotificationIcon'; 19 | export * from './NudgeCloseIcon'; 20 | export * from './NudgeIcon'; 21 | export * from './NudgeMessageIcon'; 22 | export * from './PersonIcon'; 23 | export * from './PlusIcon'; 24 | export * from './QuestionCircleIcon'; 25 | export * from './RiceIcon'; 26 | export * from './SendIcon'; 27 | export * from './ThreeDotsVerticalIcon'; 28 | -------------------------------------------------------------------------------- /src/components/KeywordInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './KeywordInput'; 2 | export * from './keywordInput.type'; 3 | -------------------------------------------------------------------------------- /src/components/KeywordInput/keywordInput.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 확장성을 고려해서 객체 타입으로 만들었습니다. 3 | */ 4 | export type OptionType = { 5 | title: string; 6 | imageUrl: string; 7 | content: string; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Menu } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'components/Menu', 7 | component: Menu, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => ( 17 | 18 | 메뉴 헤더 19 | {[1, 2, 3, 4, 5].map(v => ( 20 | {v}번 요소 21 | ))} 22 | 23 | ), 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Menu/MenuElement.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, PropsWithChildren } from 'react'; 2 | 3 | import { ChevronRightIcon } from '~/components/Icon'; 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | export const MenuElement = ({ 7 | className, 8 | children, 9 | ...rest 10 | }: PropsWithChildren>) => { 11 | return ( 12 |
  • 20 | {children} 21 | 22 |
  • 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Menu/MenuHeader.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, PropsWithChildren } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | export const MenuHeader = ({ 6 | className, 7 | children, 8 | ...rest 9 | }: PropsWithChildren>) => { 10 | return ( 11 |

    12 | {children} 13 |

    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Menu/MenuWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, PropsWithChildren } from 'react'; 2 | 3 | export const MenuWrapper = ({ 4 | className, 5 | children, 6 | ...rest 7 | }: PropsWithChildren>) => { 8 | return ( 9 |
      10 | {children} 11 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | import { MenuElement } from '~/components/Menu/MenuElement'; 2 | import { MenuHeader } from '~/components/Menu/MenuHeader'; 3 | import { MenuWrapper } from '~/components/Menu/MenuWrapper'; 4 | 5 | export const Menu = Object.assign(MenuWrapper, { 6 | Header: MenuHeader, 7 | Element: MenuElement, 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/NudgeIconSelector/NudgeIconSelector.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CelebrationIcon, 3 | EyeIcon, 4 | HeartExchangeIcon, 5 | NudgeIcon, 6 | RiceIcon, 7 | } from '~/components/Icon'; 8 | import { NudgeIconSelectorType } from '~/types/nudge'; 9 | import { ClassNameType } from '~/types/util'; 10 | 11 | type NudgeIconSelectorProps = { 12 | nudgeType: NudgeIconSelectorType; 13 | className?: ClassNameType; 14 | }; 15 | 16 | type IconWithProps = { 17 | className: ClassNameType; 18 | }; 19 | 20 | const bellIconMap: Record> = { 21 | DEFAULT: ({ className, ...rest }) => , 22 | MEET: ({ className, ...rest }) => , 23 | FRIENDLY: ({ className, ...rest }) => , 24 | SIMILARITY: ({ className, ...rest }) => , 25 | TALKING: ({ className, ...rest }) => , 26 | }; 27 | 28 | export const NudgeIconSelector = ({ nudgeType, className, ...rest }: NudgeIconSelectorProps) => { 29 | const IconComponent = bellIconMap[nudgeType]; 30 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/NudgeIconSelector/index.tsx: -------------------------------------------------------------------------------- 1 | export { NudgeIconSelector } from './NudgeIconSelector'; 2 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Button } from '~/components/Button'; 4 | 5 | import Popup from './Popup'; 6 | 7 | const meta: Meta = { 8 | title: 'components/Popup', 9 | component: Popup, 10 | }; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | title: '타이틀', 17 | description: '세부 내용', 18 | buttons: ( 19 | <> 20 | 23 | 26 | 27 | ), 28 | }, 29 | }; 30 | 31 | export default meta; 32 | 33 | export const Positive: Story = { 34 | args: { 35 | title: '타이틀', 36 | description: '세부 내용', 37 | buttons: ( 38 | <> 39 | 42 | 43 | ), 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ReactNode } from 'react'; 4 | 5 | import { AnimatedPortal } from '~/components/Portal'; 6 | 7 | export type PopupProps = { 8 | title?: string; 9 | description?: string; 10 | buttons?: ReactNode; 11 | }; 12 | 13 | const Popup = ({ title, description, buttons }: PopupProps) => { 14 | return ( 15 | 18 |
    19 |
    20 | {title &&

    {title}

    } 21 | {description &&

    {description}

    } 22 | {buttons &&
    {buttons}
    } 23 |
    24 |
    25 |
    26 | ); 27 | }; 28 | 29 | export default Popup; 30 | -------------------------------------------------------------------------------- /src/components/Popup/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Popup'; 2 | -------------------------------------------------------------------------------- /src/components/Popup/usePopup.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const usePopup = (initialState = false) => { 4 | const [isOpen, setIsOpen] = useState(initialState); 5 | 6 | const open = () => setIsOpen(true); 7 | const close = () => setIsOpen(false); 8 | 9 | return { 10 | isOpen, 11 | open, 12 | close, 13 | }; 14 | }; 15 | 16 | export default usePopup; 17 | -------------------------------------------------------------------------------- /src/components/Portal/AnimatedPortal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AnimatePresence, motion, MotionProps } from 'framer-motion'; 4 | import { PropsWithChildren } from 'react'; 5 | 6 | import { Portal } from '~/components/Portal'; 7 | 8 | type AnimatedPortalProps = { 9 | motionProps: MotionProps; 10 | } & PropsWithChildren; 11 | 12 | export const AnimatedPortal = ({ children, motionProps }: AnimatedPortalProps) => { 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Portal/Portal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PropsWithChildren, useEffect, useRef } from 'react'; 4 | import { createPortal } from 'react-dom'; 5 | 6 | import useIsMounted from '~/hooks/useIsMounted.hooks'; 7 | 8 | export type PortalProps = { 9 | documentId?: string; 10 | }; 11 | 12 | const findWrapperElement = (documentId: string): Element | null => { 13 | const wrapper = document.getElementById(documentId); 14 | if (wrapper) { 15 | return wrapper; 16 | } else { 17 | console.warn(`Element with ID '${documentId}'가 root layout에 없어요....추가해주세요.`); 18 | return null; 19 | } 20 | }; 21 | 22 | export const Portal = ({ documentId, children }: PropsWithChildren) => { 23 | const ref = useRef(null); 24 | const isMounted = useIsMounted(); 25 | 26 | useEffect(() => { 27 | if (documentId) { 28 | const wrapper = findWrapperElement(documentId); 29 | ref.current = wrapper; 30 | } else { 31 | ref.current = findWrapperElement('portal'); 32 | } 33 | }, [isMounted, documentId]); 34 | 35 | if (!(isMounted && ref.current)) return null; 36 | 37 | return createPortal(children, ref.current); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Portal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AnimatedPortal'; 2 | export * from './Portal'; 3 | -------------------------------------------------------------------------------- /src/components/ProfileImageEdit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProfileImageEdit.client'; 2 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import { ProgressBar } from './ProgressBar'; 5 | 6 | const meta: Meta = { 7 | title: 'components/ProgressBar', 8 | component: ProgressBar, 9 | }; 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Defulat: Story = { 15 | render: () => { 16 | const stepsLength = 5; 17 | // eslint-disable-next-line react-hooks/rules-of-hooks 18 | const [currentStep, setCurrentStep] = useState(1); 19 | 20 | return ( 21 |
    22 | 23 |
    24 |
    {currentStep}
    25 | 26 |
    27 | ); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | type ProgressBarProps = { 2 | currentStep: number; 3 | stepsLength: number; 4 | }; 5 | 6 | export const ProgressBar = ({ currentStep, stepsLength }: ProgressBarProps) => { 7 | const percentage = (currentStep / stepsLength) * 100; 8 | 9 | return ( 10 |
    11 |
    15 |
    16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProgressBar'; 2 | -------------------------------------------------------------------------------- /src/components/SpeechBubble/SpeechBubbleThumbnail.tsx: -------------------------------------------------------------------------------- 1 | import { NudgeIconSelector } from '~/components/NudgeIconSelector'; 2 | import { nudgeMessages, NudgeModel } from '~/types/nudge'; 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type SpeechBubbleThumbnailProps = { 6 | nudgeType: NudgeModel; 7 | }; 8 | 9 | export const SpeechBubbleThumbnail = ({ nudgeType }: SpeechBubbleThumbnailProps) => { 10 | const message = nudgeMessages.find(x => x.id === nudgeType)?.text; 11 | 12 | return ( 13 |
    14 |
    15 | 16 | <> 17 | {message} 18 | 를 나에게 보냈어요 19 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/SpeechBubble/index.ts: -------------------------------------------------------------------------------- 1 | import { SpeechBubbleDetail } from '~/components/SpeechBubble/SpeechBubbleDetail'; 2 | import { SpeechBubbleThumbnail } from '~/components/SpeechBubble/SpeechBubbleThumbnail'; 3 | 4 | export const SpeechBubble = Object.assign( 5 | {}, 6 | { 7 | Detail: SpeechBubbleDetail, 8 | Thumbnail: SpeechBubbleThumbnail, 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/Svg/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Svg'; 2 | -------------------------------------------------------------------------------- /src/components/Swiper/Swiper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'swiper/css'; 4 | import 'swiper/css/navigation'; 5 | import 'swiper/css/pagination'; 6 | import 'swiper/css/scrollbar'; 7 | 8 | import { PropsWithChildren, useRef } from 'react'; 9 | import { Navigation, Pagination, Scrollbar } from 'swiper'; 10 | import * as SwiperReact from 'swiper/react'; 11 | 12 | type SwiperProps = PropsWithChildren & SwiperReact.SwiperProps; 13 | export const Swiper = ({ 14 | spaceBetween, 15 | slidesPerView, 16 | pagination, 17 | scrollbar, 18 | onSwiper, 19 | onSlideChange, 20 | allowTouchMove, 21 | children, 22 | }: SwiperProps) => { 23 | const swiperRef = useRef(null); 24 | 25 | return ( 26 | 37 | {children} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Swiper/SwiperSlide.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import * as SwiperReact from 'swiper/react'; 3 | 4 | type SwiperCardProps = PropsWithChildren; 5 | 6 | export const SwiperSlide = ({ children, ...rest }: SwiperCardProps) => { 7 | return {children}; 8 | }; 9 | 10 | SwiperSlide.displayName = 'SwiperSlide'; 11 | -------------------------------------------------------------------------------- /src/components/Swiper/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Swiper'; 2 | export * from './SwiperSlide'; 3 | -------------------------------------------------------------------------------- /src/components/Tag/Tag.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Tag from './Tag'; 4 | 5 | const meta: Meta = { 6 | title: 'components/Tag', 7 | component: Tag, 8 | }; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | type: 'TRUE', 15 | label: '태그', 16 | }, 17 | }; 18 | 19 | export default meta; 20 | 21 | export const Tobby: Story = { 22 | args: { 23 | type: 'TOBBY', 24 | label: '태그', 25 | }, 26 | }; 27 | 28 | export const Buddy: Story = { 29 | args: { 30 | type: 'BUDDY', 31 | label: '태그', 32 | }, 33 | }; 34 | 35 | export const Pipi: Story = { 36 | args: { 37 | type: 'PIPI', 38 | label: '태그', 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Tag/Tag.tsx: -------------------------------------------------------------------------------- 1 | import { CharacterNameModel } from '~/types/idCard'; 2 | 3 | type TagProps = { 4 | type: CharacterNameModel; 5 | label: string; 6 | }; 7 | 8 | const colors: Record = { 9 | BUDDY: 'text-buddy-700 border-buddy-200', 10 | TOBBY: 'text-tobby-700 border-tobby-200', 11 | PIPI: 'text-pipi-700 border-pipi-200', 12 | TRUE: 'text-true-700 border-true-200', 13 | }; 14 | 15 | const getCharacterColor = (type: CharacterNameModel) => { 16 | return `${colors[type]}`; 17 | }; 18 | 19 | const getClassName = (type: CharacterNameModel) => { 20 | return `${getCharacterColor( 21 | type, 22 | )} inline-block rounded-xl border border-solid px-2 py-1 text-detail font-medium bg-white`; 23 | }; 24 | 25 | const Tag = ({ type, label }: TagProps) => { 26 | return
    {label}
    ; 27 | }; 28 | 29 | export default Tag; 30 | -------------------------------------------------------------------------------- /src/components/Tag/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Tag'; 2 | -------------------------------------------------------------------------------- /src/components/Template/TemplateButton.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { Button, ButtonProps } from '~/components/Button/Button'; 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | type TemplateButtonProps = Partial> & { 7 | children: ReactNode | string; 8 | className?: string; 9 | }; 10 | 11 | export const TemplateButton = ({ children, className, onClick, ...rest }: TemplateButtonProps) => { 12 | return ( 13 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Template/TemplateContent.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type TemplateContentProps = { 6 | children?: ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export const TemplateContent = ({ children, className }: TemplateContentProps) => { 11 | return
    {children}
    ; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Template/TemplateDescription.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | type TemplateDescriptionProps = { 4 | children: ReactNode; 5 | className?: string; 6 | }; 7 | 8 | export const TemplateDescription = ({ children, className }: TemplateDescriptionProps) => { 9 | return
    {children}
    ; 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Template/TemplateTitle.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | type TemplateTitleProps = { 4 | children: ReactNode; 5 | className?: string; 6 | }; 7 | 8 | export const TemplateTitle = ({ children, className }: TemplateTitleProps) => { 9 | return
    {children}
    ; 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Template/TemplateWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type TemplateWrapperProps = { 6 | children: ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export const TemplateWrapper = ({ children, className }: TemplateWrapperProps) => { 11 | return
    {children}
    ; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Template/index.ts: -------------------------------------------------------------------------------- 1 | import { TemplateButton } from './TemplateButton'; 2 | import { TemplateContent } from './TemplateContent'; 3 | import { TemplateDescription } from './TemplateDescription'; 4 | import { TemplateTitle } from './TemplateTitle'; 5 | import { TemplateWrapper } from './TemplateWrapper'; 6 | 7 | export const Template = Object.assign(TemplateWrapper, { 8 | Title: TemplateTitle, 9 | Description: TemplateDescription, 10 | Content: TemplateContent, 11 | Button: TemplateButton, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/TextArea/TextAreaHeader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PropsWithChildren } from 'react'; 4 | 5 | import { tw } from '~/utils/tailwind.util'; 6 | 7 | type TextAreaHeaderProps = { 8 | className?: string; 9 | }; 10 | 11 | export const TextAreaHeader = ({ className, children }: PropsWithChildren) => { 12 | return

    {children}

    ; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/TextArea/TextAreaImage.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Image from 'next/image'; 3 | 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | type SafeNumber = number | `${number}`; 7 | 8 | type TextAreaImageProps = { src: string; alt: string; height?: SafeNumber }; 9 | 10 | export const TextAreaImage = ({ src, alt, height }: TextAreaImageProps) => { 11 | return ( 12 |
    13 | {alt} 20 |
    21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/TextArea/TextAreaLabel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | import { ClassNameType } from '~/types/util'; 5 | import { tw } from '~/utils/tailwind.util'; 6 | 7 | type TextAreaLabelProps = { 8 | name: string; 9 | required?: boolean; 10 | className?: ClassNameType; 11 | }; 12 | 13 | export const TextAreaLabel = ({ 14 | name, 15 | required, 16 | className, 17 | children, 18 | }: PropsWithChildren) => { 19 | const requiredPseudoCss = 20 | required && 21 | 'after:content-[" "] after:inline-block after:w-[4px] after:h-[4px] after:rounded-full after:bg-[#FF5555] after:absolute after:top-0 after:right-[-10px] '; 22 | 23 | return ( 24 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/TextArea/TextAreaWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ReactNode } from 'react'; 3 | 4 | import { tw } from '~/utils/tailwind.util'; 5 | 6 | type TextAreaWrapperProps = { 7 | children: ReactNode; 8 | className?: string; 9 | }; 10 | 11 | export const TextAreaWrapper = ({ children, className }: TextAreaWrapperProps) => { 12 | return
    {children}
    ; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/TextArea/index.tsx: -------------------------------------------------------------------------------- 1 | import { TextAreaBorder } from './TextAreaBorder'; 2 | import { TextAreaContent } from './TextAreaContent'; 3 | import { TextAreaHeader } from './TextAreaHeader'; 4 | import { TextAreaImage } from './TextAreaImage'; 5 | import { TextAreaLabel } from './TextAreaLabel'; 6 | import { TextAreaWrapper } from './TextAreaWrapper'; 7 | 8 | export * from './useTextArea'; 9 | 10 | export const TextArea = Object.assign(TextAreaWrapper, { 11 | Label: TextAreaLabel, 12 | Header: TextAreaHeader, 13 | Content: TextAreaContent, 14 | Border: TextAreaBorder, 15 | Image: TextAreaImage, 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/TextArea/useAutoHeightTextArea.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react'; 2 | 3 | type UseAutoHeightTextAreaProps = { 4 | isAutoSize: boolean; 5 | ref: RefObject | null; 6 | value: string | number | readonly string[] | undefined; 7 | }; 8 | 9 | export const useAutoHeightTextArea = ({ isAutoSize, ref, value }: UseAutoHeightTextAreaProps) => { 10 | useEffect(() => { 11 | if (isAutoSize && ref && ref.current) { 12 | ref.current.style.height = '0px'; 13 | const scrollHeight = ref.current.scrollHeight; 14 | ref.current.style.height = scrollHeight + 'px'; 15 | } 16 | }, [ref, isAutoSize, value]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/TextArea/useTextArea.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ChangeEvent, ChangeEventHandler, useCallback, useState } from 'react'; 3 | 4 | export type UseTextAreaProps = { 5 | initValue?: string; 6 | onChange: ChangeEventHandler; 7 | maxLength?: number; 8 | }; 9 | 10 | export const useTextArea = ({ initValue = '', onChange, maxLength }: UseTextAreaProps) => { 11 | const [value, setValue] = useState(initValue); 12 | 13 | const onChangeHandler = useCallback( 14 | (e: ChangeEvent) => { 15 | e.target.value = e.target.value.slice(0, maxLength); 16 | onChange(e); 17 | setValue(e.target.value); 18 | }, 19 | [maxLength, onChange], 20 | ); 21 | 22 | return { value, onChangeHandler }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/TextInput/TextInputContent.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { forwardRef, InputHTMLAttributes } from 'react'; 3 | 4 | import { ClassNameType } from '~/types/util'; 5 | import { tw } from '~/utils/tailwind.util'; 6 | 7 | type TextInputProps = InputHTMLAttributes & { 8 | inputClassName?: ClassNameType; 9 | }; 10 | 11 | // eslint-disable-next-line react/display-name 12 | export const TextInputContent = forwardRef( 13 | ( 14 | { 15 | inputClassName, 16 | name, 17 | placeholder, 18 | value, 19 | onChange, 20 | onBlur, 21 | type = 'text', 22 | disabled, 23 | ...rest 24 | }, 25 | ref, 26 | ) => { 27 | const disabledCss = disabled && 'cursor-not-allowed'; 28 | return ( 29 | 43 | ); 44 | }, 45 | ); 46 | -------------------------------------------------------------------------------- /src/components/TextInput/TextInputLabel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | import { ClassNameType } from '~/types/util'; 5 | import { tw } from '~/utils/tailwind.util'; 6 | 7 | type TextInputLabelProps = { 8 | name: string; 9 | required?: boolean; 10 | className?: ClassNameType; 11 | }; 12 | 13 | export const TextInputLabel = ({ 14 | name, 15 | required, 16 | className, 17 | children, 18 | }: PropsWithChildren) => { 19 | const requiredPseudoCss = 20 | required && 21 | 'after:content-[" "] after:inline-block after:w-[4px] after:h-[4px] after:rounded-full after:bg-[#FF5555] after:absolute after:top-0 after:right-[-10px] '; 22 | 23 | return ( 24 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/TextInput/TextInputWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { HTMLAttributes, PropsWithChildren } from 'react'; 4 | 5 | import { tw } from '~/utils/tailwind.util'; 6 | 7 | export const TextInputWrapper = ({ 8 | className, 9 | children, 10 | ...rest 11 | }: PropsWithChildren>) => { 12 | return ( 13 |
    14 | {children} 15 |
    16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/TextInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { TextInputBorder } from './TextInputBorder'; 2 | import { TextInputContent } from './TextInputContent'; 3 | import { TextInputLabel } from './TextInputLabel'; 4 | import { TextInputWrapper } from './TextInputWrapper'; 5 | 6 | export * from './useTextInput'; 7 | 8 | export const TextInput = Object.assign(TextInputWrapper, { 9 | Label: TextInputLabel, 10 | Content: TextInputContent, 11 | Border: TextInputBorder, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/TextInput/useTextInput.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ChangeEvent, ChangeEventHandler, useCallback, useState } from 'react'; 3 | 4 | export type UseTextInputProps = { 5 | initValue?: string; 6 | onChange: ChangeEventHandler; 7 | maxLength?: number; 8 | }; 9 | 10 | export const useTextInput = ({ initValue = '', onChange, maxLength }: UseTextInputProps) => { 11 | const [value, setValue] = useState(initValue); 12 | 13 | const onChangeHandler = useCallback( 14 | (e: ChangeEvent) => { 15 | e.target.value = e.target.value.slice(0, maxLength); 16 | onChange(e); 17 | setValue(e.target.value); 18 | }, 19 | [maxLength, onChange], 20 | ); 21 | 22 | return { value, onChangeHandler }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ToastMessage/ToastMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ToastMessageModel, ToastMessageType } from '~/stores/toastMessage.store'; 2 | import { tw } from '~/utils/tailwind.util'; 3 | 4 | type ToastMessageProps = Omit; 5 | 6 | const colors: Record = { 7 | error: 'bg-grey-500 text-white', 8 | success: 'bg-grey-500 text-white', 9 | info: 'bg-grey-500 text-white', 10 | }; 11 | 12 | export const ToastMessage = ({ message, type }: ToastMessageProps) => { 13 | return ( 14 |
    21 | {message} 22 |
    23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/ToastMessage/ToastMessageProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { AnimatePresence, motion } from 'framer-motion'; 3 | 4 | import { Portal } from '~/components/Portal'; 5 | import { ToastMessage } from '~/components/ToastMessage/ToastMessage'; 6 | import { useToastMessageStore } from '~/stores/toastMessage.store'; 7 | 8 | export const ToastMessageProvider = () => { 9 | const { toastMessageList } = useToastMessageStore(); 10 | return ( 11 | 12 |
    13 |
    14 | 15 | {toastMessageList.map(({ toastId, message, type }) => ( 16 | 23 | 24 | 25 | ))} 26 | 27 |
    28 |
    29 |
    30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/ToastMessage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ToastMessage'; 2 | export * from './ToastMessageProvider'; 3 | -------------------------------------------------------------------------------- /src/components/TopNavigation/TopNavigationLeft.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type TopNavigationLeftProps = { 6 | className?: string; 7 | }; 8 | 9 | export const TopNavigationLeft = ({ 10 | children, 11 | className, 12 | }: PropsWithChildren) => { 13 | return ( 14 |
    {children}
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/TopNavigation/TopNavigationProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { ProgressBar } from '../ProgressBar'; 2 | 3 | type TopNavigationProgressBarProps = { 4 | currentStep: number; 5 | stepsLength: number; 6 | }; 7 | 8 | export const TopNavigationProgressBar = ({ 9 | currentStep, 10 | stepsLength, 11 | }: TopNavigationProgressBarProps) => { 12 | return ( 13 |
    14 | 15 |
    16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/TopNavigation/TopNavigationRight.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type TopNavigationRightProps = { 6 | className?: string; 7 | }; 8 | 9 | export const TopNavigationRight = ({ 10 | children, 11 | className, 12 | }: PropsWithChildren) => { 13 | return
    {children}
    ; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/TopNavigation/TopNavigationTitle.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type TopNavigationTitleProps = { 6 | className?: string; 7 | }; 8 | 9 | export const TopNavigationTitle = ({ 10 | children, 11 | className, 12 | }: PropsWithChildren) => { 13 | return
    {children}
    ; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/TopNavigation/TopNavigationWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | type TopNavigationWrapperProps = { 4 | /** 5 | * border-bottom 컬러 값이 주어지면 표시합니다. ex) color-primary 6 | */ 7 | bottomBorderColor?: string; 8 | bgColor?: string; 9 | }; 10 | export const TopNavigationWrapper = ({ 11 | bottomBorderColor, 12 | bgColor = 'bg-white', 13 | children, 14 | }: PropsWithChildren) => { 15 | const borderBottomStyle = bottomBorderColor ? `border-b-${bottomBorderColor} border-b-[1px]` : ''; 16 | return ( 17 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/TopNavigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { TopNavigationBackButton } from './TopNavigationBackButton'; 2 | import { TopNavigationLeft } from './TopNavigationLeft'; 3 | import { TopNavigationProgressBar } from './TopNavigationProgressBar'; 4 | import { TopNavigationRight } from './TopNavigationRight'; 5 | import { TopNavigationTitle } from './TopNavigationTitle'; 6 | import { TopNavigationWrapper } from './TopNavigationWrapper'; 7 | 8 | export const TopNavigation = Object.assign(TopNavigationWrapper, { 9 | Left: TopNavigationLeft, 10 | Title: TopNavigationTitle, 11 | Right: TopNavigationRight, 12 | BackButton: TopNavigationBackButton, 13 | ProgressBar: TopNavigationProgressBar, 14 | }); 15 | -------------------------------------------------------------------------------- /src/constant/planet.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SELECT_PLANET_INDEX = 0; 2 | -------------------------------------------------------------------------------- /src/constant/recommendKeyword.ts: -------------------------------------------------------------------------------- 1 | import { OptionType } from '~/components/KeywordInput'; 2 | 3 | export const DEFAULT_RECOMMEND_KEYWORD_OPTIONS: OptionType[] = [ 4 | { 5 | title: '밤 산책', 6 | imageUrl: '', 7 | content: '', 8 | }, 9 | { 10 | title: '아메리카노', 11 | imageUrl: '', 12 | content: '', 13 | }, 14 | { 15 | title: '해외 축구', 16 | imageUrl: '', 17 | content: '', 18 | }, 19 | { 20 | title: '음악', 21 | imageUrl: '', 22 | content: '', 23 | }, 24 | { 25 | title: '공포영화', 26 | imageUrl: '', 27 | content: '', 28 | }, 29 | { 30 | title: '셀프 인테리어', 31 | imageUrl: '', 32 | content: '', 33 | }, 34 | { 35 | title: '엽떡', 36 | imageUrl: '', 37 | content: '', 38 | }, 39 | { 40 | title: '우주 맛집 투어', 41 | imageUrl: '', 42 | content: '', 43 | }, 44 | { 45 | title: '필름카메라', 46 | imageUrl: '', 47 | content: '', 48 | }, 49 | { 50 | title: '강아지 영상보기', 51 | imageUrl: '', 52 | content: '', 53 | }, 54 | { 55 | title: '오버워치', 56 | imageUrl: '', 57 | content: '', 58 | }, 59 | ]; 60 | -------------------------------------------------------------------------------- /src/hooks/useIsMounted.hooks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | function useIsMounted() { 4 | const [isMounted, setIsMounted] = useState(false); 5 | 6 | useEffect(() => { 7 | setIsMounted(true); 8 | }, []); 9 | 10 | return isMounted; 11 | } 12 | 13 | export default useIsMounted; 14 | -------------------------------------------------------------------------------- /src/hooks/usePlanetNavigate.ts: -------------------------------------------------------------------------------- 1 | export const usePlanetNavigate = () => { 2 | const extractPlanetIdFromPathname = (currentPathname: string) => { 3 | const parts = currentPathname.split('/'); 4 | const planetIdIndex = parts.findIndex(part => part === 'planet' || part === 'my-page'); 5 | 6 | const notFoundPlanetID = planetIdIndex === -1; 7 | const planetIdIndexOverPathLength = planetIdIndex + 1 >= parts.length; 8 | 9 | if (notFoundPlanetID) return null; 10 | if (planetIdIndexOverPathLength) return null; 11 | 12 | return Number(parts[planetIdIndex + 1]); 13 | }; 14 | return { 15 | extractPlanetIdFromPathname, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/tanstackQuery/getQueryClient.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | import { cache } from 'react'; 3 | 4 | const getQueryClient = cache(() => new QueryClient()); 5 | export default getQueryClient; 6 | -------------------------------------------------------------------------------- /src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw'; 2 | 3 | import { handlers } from './handlers'; 4 | 5 | export const worker = setupWorker(...handlers); 6 | -------------------------------------------------------------------------------- /src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { commentMockHandler } from '~/mocks/comment/comment.mockHandler'; 2 | import { communityMockHandler } from '~/mocks/community/community.mockHandler'; 3 | import { idCardMockHandler } from '~/mocks/idCard/idCard.mockHandler'; 4 | import { notificationMockHandler } from '~/mocks/notification/notification.mockHandler'; 5 | import { nudgeMockHandler } from '~/mocks/nudge/nudge.mockHandler'; 6 | import { userMockHandler } from '~/mocks/user/user.mockHandler'; 7 | 8 | export const handlers = [ 9 | ...idCardMockHandler, 10 | ...communityMockHandler, 11 | ...commentMockHandler, 12 | ...notificationMockHandler, 13 | ...userMockHandler, 14 | ...nudgeMockHandler, 15 | ]; 16 | -------------------------------------------------------------------------------- /src/mocks/idCard/idCard.mock.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/ko'; 2 | 3 | import { generateRandomNudge } from '~/mocks/nudge.util'; 4 | import { IdCardCreateResponse, IdCardDetailModel } from '~/types/idCard'; 5 | 6 | export const idCardDetailMock = (): IdCardDetailModel => ({ 7 | idCardId: faker.number.int(), 8 | userId: faker.number.int(), 9 | nickname: faker.person.fullName(), 10 | profileImageUrl: faker.image.avatar(), 11 | aboutMe: faker.lorem.paragraph(), 12 | keywords: Array.from({ length: 3 }, () => ({ 13 | keywordId: faker.number.int(), 14 | title: faker.lorem.word(), 15 | imageUrl: faker.image.avatar(), 16 | content: faker.lorem.paragraph(), 17 | })), 18 | characterType: faker.helpers.arrayElement(['TRUE', 'PIPI', 'TOBBY', 'BUDDY']), 19 | commentCount: faker.number.int({ min: 0, max: 999 }), 20 | toNudgeType: generateRandomNudge(), 21 | unreadNudges: faker.number.int({ min: 0, max: 999 }), 22 | }); 23 | 24 | export const createIdCardMock: IdCardCreateResponse = { id: 1 }; 25 | -------------------------------------------------------------------------------- /src/mocks/idCard/idCard.mockHandler.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | import { ROOT_API_URL } from '~/api/config/requestUrl'; 4 | import { createIdCardMock, idCardDetailMock } from '~/mocks/idCard/idCard.mock'; 5 | import { generateResponse } from '~/mocks/mock.util'; 6 | 7 | export const idCardMockHandler = [ 8 | rest.get(`${ROOT_API_URL}/id-cards/:idCardId`, () => { 9 | return generateResponse({ statusCode: 200, data: { idCardDetailsDto: idCardDetailMock() } }); 10 | }), 11 | rest.get(`${ROOT_API_URL}/communities/:communityId/users/idCards`, () => { 12 | return generateResponse({ statusCode: 200, data: { idCardDetailsDto: idCardDetailMock() } }); 13 | }), 14 | rest.put(`${ROOT_API_URL}/id-cards/:idCardId`, () => { 15 | return generateResponse({ statusCode: 200, data: idCardDetailMock() }); 16 | }), 17 | rest.post(`${ROOT_API_URL}/id-cards`, () => { 18 | return generateResponse({ statusCode: 200, data: createIdCardMock }); 19 | }), 20 | ]; 21 | -------------------------------------------------------------------------------- /src/mocks/image/image.mock.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/ko'; 2 | 3 | export const imageUrlMock = { 4 | imageUrl: faker.image.avatar(), 5 | }; 6 | -------------------------------------------------------------------------------- /src/mocks/image/image.mockHandler.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | import { ROOT_API_URL } from '~/api/config/requestUrl'; 4 | import { generateResponse } from '~/mocks/mock.util'; 5 | 6 | import { imageUrlMock } from './image.mock'; 7 | 8 | export const imageMockHandler = [ 9 | rest.post(`${ROOT_API_URL}/images`, () => { 10 | return generateResponse({ statusCode: 200, data: imageUrlMock }); 11 | }), 12 | ]; 13 | -------------------------------------------------------------------------------- /src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | const initMocks = async () => { 2 | const isServer = typeof window === 'undefined'; 3 | 4 | if (isServer) { 5 | const { server } = await require('./server'); 6 | server.listen({ onUnhandledRequest: 'bypass' }); 7 | } else { 8 | const { worker } = await require('./browser'); 9 | worker.start({ onUnhandledRequest: 'bypass' }); 10 | } 11 | }; 12 | 13 | export default initMocks; 14 | -------------------------------------------------------------------------------- /src/mocks/mock.util.ts: -------------------------------------------------------------------------------- 1 | import { context, response } from 'msw'; 2 | 3 | import { DefaultServerResponseType } from '~/api/config/api.types'; 4 | 5 | export const generateResponse = ({ 6 | data, 7 | statusCode, 8 | delay = 0, 9 | }: Omit, 'success'> & { delay?: number }) => { 10 | const isSuccess = statusCode < 400; 11 | return response( 12 | context.status(statusCode), 13 | context.delay(delay), 14 | context.json({ 15 | data, 16 | statusCode, 17 | success: isSuccess, 18 | }), 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/mocks/notification/notification.mockHandler.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | import { ROOT_API_URL } from '~/api/config/requestUrl'; 4 | import { generateResponse } from '~/mocks/mock.util'; 5 | 6 | import { createNotificationList } from './notification.mock'; 7 | 8 | export const notificationMockHandler = [ 9 | rest.get(`${ROOT_API_URL}/notifications?page=:page&size=10`, req => { 10 | const { searchParams } = req.url; 11 | const page = Number(searchParams.get('page')); 12 | return generateResponse({ 13 | statusCode: 200, 14 | data: createNotificationList(10, page, 10), 15 | }); 16 | }), 17 | ]; 18 | -------------------------------------------------------------------------------- /src/mocks/nudge.util.ts: -------------------------------------------------------------------------------- 1 | import { NudgeModel } from '~/types/nudge'; 2 | 3 | const keywords: NudgeModel[] = ['MEET', 'SIMILARITY', 'TALKING', 'FRIENDLY']; 4 | 5 | export const generateRandomNudge = () => { 6 | const randomIndex = Math.floor(Math.random() * keywords.length); 7 | const keyword = keywords[randomIndex]; 8 | 9 | return keyword; 10 | }; 11 | -------------------------------------------------------------------------------- /src/mocks/nudge/nudge.mock.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/ko'; 2 | 3 | import { NudgeListModel } from '~/types/nudge/model.type'; 4 | 5 | export const createNudgeList = (): NudgeListModel[] => { 6 | return Array.from({ length: 5 }, () => ({ 7 | nudgeId: faker.number.int(), 8 | opponentUser: { 9 | userId: faker.number.int(), 10 | profileImageUrl: faker.image.avatar(), 11 | nickname: faker.person.fullName(), 12 | }, 13 | toUserNudgeType: faker.helpers.arrayElement(['FRIENDLY', 'SIMILARITY', 'TALKING', 'MEET']), 14 | fromUserNudgeType: faker.helpers.arrayElement([ 15 | 'FRIENDLY', 16 | 'SIMILARITY', 17 | 'TALKING', 18 | 'MEET', 19 | null, 20 | ]), 21 | })); 22 | }; 23 | -------------------------------------------------------------------------------- /src/mocks/nudge/nudge.mockHandler.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | import { ROOT_API_URL } from '~/api/config/requestUrl'; 4 | import { generateResponse } from '~/mocks/mock.util'; 5 | import { createNudgeList } from '~/mocks/nudge/nudge.mock'; 6 | 7 | export const nudgeMockHandler = [ 8 | rest.get(`${ROOT_API_URL}/nudges/id-cards/:idCardsId`, () => { 9 | return generateResponse({ statusCode: 200, data: { nudgeInfoDtos: createNudgeList() } }); 10 | }), 11 | ]; 12 | -------------------------------------------------------------------------------- /src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | 3 | import { handlers } from './handlers'; 4 | 5 | export const server = setupServer(...handlers); 6 | -------------------------------------------------------------------------------- /src/mocks/user/user.mock.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/ko'; 2 | 3 | import { UserInfoModel } from '~/types/user'; 4 | 5 | export const createUserInfo = (): UserInfoModel => ({ 6 | userId: faker.number.int(), 7 | email: faker.internet.email(), 8 | nickname: faker.person.fullName(), 9 | gender: faker.person.gender(), 10 | ageRange: '', 11 | profileImageUrl: faker.image.avatar(), 12 | characterType: 'BUDDY', 13 | communityIds: [1], 14 | }); 15 | -------------------------------------------------------------------------------- /src/mocks/user/user.mockHandler.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | import { ROOT_API_URL } from '~/api/config/requestUrl'; 4 | import { generateResponse } from '~/mocks/mock.util'; 5 | import { createUserInfo } from '~/mocks/user/user.mock'; 6 | 7 | export const userMockHandler = [ 8 | rest.get(`${ROOT_API_URL}/user/profile`, () => { 9 | return generateResponse({ 10 | statusCode: 200, 11 | data: { 12 | userProfileDto: createUserInfo(), 13 | }, 14 | }); 15 | }), 16 | rest.post(`${ROOT_API_URL}/user/character`, () => { 17 | return generateResponse({ statusCode: 200, data: {} }); 18 | }), 19 | ]; 20 | -------------------------------------------------------------------------------- /src/modules/CommentInput/CommentInput.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useGetCommunityUserInfo } from '~/api/domain/community.api'; 4 | import { ActiveCommentInput } from '~/modules/CommentInput/ActiveCommentInput.client'; 5 | import { DisabledCommentInput } from '~/modules/CommentInput/DisabledCommentInput.client'; 6 | 7 | type CommentInputProps = { 8 | idCardId: number; 9 | communityId: number; 10 | }; 11 | 12 | export const CommentInput = ({ idCardId, communityId }: CommentInputProps) => { 13 | const { data } = useGetCommunityUserInfo(communityId); 14 | 15 | const shouldActiveCommentInput = data?.myInfoInInCommunityDto.isExistsIdCard; 16 | 17 | return ( 18 |
    19 | {shouldActiveCommentInput ? ( 20 | 25 | ) : ( 26 | 27 | )} 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/CommentInput/CommentInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { CommentInput } from '~/modules/CommentInput/CommentInput.client'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/CommentInput', 7 | component: CommentInput, 8 | }; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | render: () => , 14 | }; 15 | 16 | export default meta; 17 | -------------------------------------------------------------------------------- /src/modules/CommentInput/ReplyIndicator.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect } from 'react'; 4 | 5 | import { Divider } from '~/components/Divider'; 6 | import { CancelIcon } from '~/components/Icon'; 7 | import { useReplyRecipientStore } from '~/stores/comment.store'; 8 | 9 | export const ReplyIndicator = () => { 10 | const { nickname, clear } = useReplyRecipientStore(); 11 | 12 | useEffect(() => { 13 | return () => { 14 | clear(); 15 | }; 16 | }, []); 17 | 18 | return nickname ? ( 19 | <> 20 | 21 |
    22 | {nickname}님에게 답글 남기는 중 23 | 26 |
    27 | 28 | ) : ( 29 | <> 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/CommentInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommentInput.client'; 2 | -------------------------------------------------------------------------------- /src/modules/CommentList/Comment/Comment.stories.tsx: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/ko'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { createComment } from '~/mocks/comment/comment.mock'; 5 | 6 | import { Comment } from './index'; 7 | 8 | const meta: Meta = { 9 | title: 'modules/Comment', 10 | component: Comment, 11 | args: {}, 12 | }; 13 | 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | const MOCK_COMMENT = createComment(123123, 123); 19 | 20 | export const Primary: Story = { 21 | render: () => , 22 | }; 23 | 24 | export const DetailShow: Story = { 25 | render: () => , 26 | }; 27 | -------------------------------------------------------------------------------- /src/modules/CommentList/Comment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Comment.client'; 2 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/CommentOptions.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | 5 | import { DeleteButton } from '~/modules/CommentList/CommentCommon/DeleteButton.client'; 6 | import { ReportButton } from '~/modules/CommentList/CommentCommon/ReportButton.client'; 7 | import { CommentModel } from '~/types/comment'; 8 | import { getUserIdClient } from '~/utils/auth/getUserId.client'; 9 | 10 | type CommentOptionsProps = Pick & { 11 | onClickToDeleteComment: VoidFunction; 12 | }; 13 | 14 | export const CommentOptions = ({ writerInfo, onClickToDeleteComment }: CommentOptionsProps) => { 15 | const { userId: writerId } = writerInfo; 16 | const userId = getUserIdClient(); 17 | 18 | const [isMine, setIsMine] = useState(false); 19 | 20 | const isWriterSameAsUser = userId === writerId; 21 | 22 | // Text content does not match server-rendered HTML 이슈로 useEffect로 분리처리 23 | useEffect(() => { 24 | setIsMine(isWriterSameAsUser); 25 | }, [isWriterSameAsUser, userId, writerId]); 26 | 27 | return ( 28 | <> 29 | {isMine ? : } 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/DeleteButton.client.tsx: -------------------------------------------------------------------------------- 1 | import { SimpleConfirmPopup, useConfirmPopup } from '~/components/ConfirmPopup'; 2 | 3 | type DeleteButtonProps = { 4 | onClickToDeleteComment: () => void; 5 | }; 6 | 7 | export const DeleteButton = ({ onClickToDeleteComment }: DeleteButtonProps) => { 8 | const { isOpen, openPopup, closePopup, confirm } = useConfirmPopup(); 9 | 10 | const deleteComment = () => { 11 | onClickToDeleteComment(); 12 | }; 13 | 14 | const onDeleteComment = async () => { 15 | const isOk = await openPopup(); 16 | closePopup(); 17 | if (isOk) { 18 | deleteComment(); 19 | } 20 | }; 21 | 22 | return ( 23 | <> 24 | 27 | {isOpen && ( 28 | 35 | )} 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/Empty.client.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | export const Empty = () => { 4 | return ( 5 |
    6 | congrats-clap 7 | 가장 먼저 댓글을 남겨보세요 8 |
    9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/Header.client.tsx: -------------------------------------------------------------------------------- 1 | import { CommentModel, CommentWriterIntoModel } from '~/types/comment'; 2 | import { getCreatedAtFormat } from '~/utils/time.util'; 3 | 4 | type HeaderProps = Pick & Pick; 5 | 6 | export const Header = ({ nickname, createdAt }: HeaderProps) => { 7 | return ( 8 |

    9 | {nickname} 10 | {getCreatedAtFormat(createdAt)}전 11 |

    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/LikeCount.client.tsx: -------------------------------------------------------------------------------- 1 | import { CommentLikeModel } from '~/types/comment'; 2 | 3 | type LikeCountProps = Pick; 4 | 5 | export const LikeCount = ({ likeCount }: LikeCountProps) => { 6 | const isShowLikeText = likeCount !== 0; 7 | return ( 8 | <> 9 | {isShowLikeText && ( 10 | 좋아요 {likeCount}개 11 | )} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/LikeIcon.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { HeartFillIcon, HeartIcon } from '~/components/Icon'; 4 | import { CommentLikeModel } from '~/types/comment'; 5 | 6 | type LikeIconProps = Pick & { 7 | onClickToLikeCancel: () => Promise; 8 | onClickToLike: () => Promise; 9 | }; 10 | 11 | export const LikeIcon = ({ 12 | likedByCurrentUser, 13 | onClickToLikeCancel, 14 | onClickToLike, 15 | }: LikeIconProps) => { 16 | return ( 17 | <> 18 | {likedByCurrentUser ? ( 19 | 20 | ) : ( 21 | 22 | )} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/ReplyHideButton.client.tsx: -------------------------------------------------------------------------------- 1 | import { DashIcon } from '~/components/Icon'; 2 | 3 | type ReplyHideButtonProps = { 4 | isShowReplyList: boolean; 5 | onClickHideReplyList: VoidFunction; 6 | }; 7 | 8 | export const ReplyHideButton = ({ 9 | isShowReplyList, 10 | onClickHideReplyList, 11 | }: ReplyHideButtonProps) => { 12 | return ( 13 | <> 14 | {isShowReplyList && ( 15 | 19 | )} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/ReplyShowButton.client.tsx: -------------------------------------------------------------------------------- 1 | import { DashIcon } from '~/components/Icon'; 2 | import { CommentModel } from '~/types/comment'; 3 | 4 | type ReplyShowButtonProps = Pick & { 5 | isShowReplyList: boolean; 6 | onClickShowReplyList: VoidFunction; 7 | }; 8 | 9 | export const ReplyShowButton = ({ 10 | isShowReplyList, 11 | onClickShowReplyList, 12 | repliesCount, 13 | }: ReplyShowButtonProps) => { 14 | const isReplyListEmpty = repliesCount === 0; 15 | 16 | const isShowButton = !isShowReplyList && !isReplyListEmpty; 17 | 18 | return ( 19 | <> 20 | {isShowButton && ( 21 | 31 | )} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/ReplySubmitButton.client.tsx: -------------------------------------------------------------------------------- 1 | import { useReplyRecipientStore } from '~/stores/comment.store'; 2 | 3 | type ReplySubmitButtonProps = { 4 | nickname: string; 5 | commentId: number; 6 | }; 7 | 8 | export const ReplySubmitButton = ({ nickname, commentId }: ReplySubmitButtonProps) => { 9 | const { setReplyRecipient } = useReplyRecipientStore(); 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/ReportButton.client.tsx: -------------------------------------------------------------------------------- 1 | import { SimpleConfirmPopup, useConfirmPopup } from '~/components/ConfirmPopup'; 2 | import { useToastMessageStore } from '~/stores/toastMessage.store'; 3 | 4 | export const ReportButton = () => { 5 | const { isOpen, openPopup, closePopup, confirm } = useConfirmPopup(); 6 | const { infoToast } = useToastMessageStore(); 7 | const handleReport = async () => { 8 | const isOk = await openPopup(); 9 | closePopup(); 10 | if (isOk) { 11 | infoToast('신고가 접수됐습니다'); 12 | } 13 | }; 14 | 15 | return ( 16 | <> 17 | 20 | {isOpen && ( 21 | 28 | )} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/UserProfile.client.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import { CommentWriterIntoModel } from '~/types/comment'; 4 | 5 | type UserProfileProps = Pick; 6 | 7 | export const UserProfile = ({ profileImageUrl }: UserProfileProps) => { 8 | return ( 9 |
    10 | profile image 17 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentCommon/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommentOptions.client'; 2 | export * from './Content.client'; 3 | export * from './DeleteButton.client'; 4 | export * from './Empty.client'; 5 | export * from './Header.client'; 6 | export * from './LikeCount.client'; 7 | export * from './LikeIcon.client'; 8 | export * from './ReplyHideButton.client'; 9 | export * from './ReplyShowButton.client'; 10 | export * from './ReplySubmitButton.client'; 11 | export * from './ReportButton.client'; 12 | export * from './UserProfile.client'; 13 | -------------------------------------------------------------------------------- /src/modules/CommentList/CommentReplyList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommentReply.client'; 2 | export * from './CommentReplyList.client'; 3 | -------------------------------------------------------------------------------- /src/modules/CommentList/useLike.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { CommentLikeModel } from '~/types/comment'; 4 | 5 | type UseLikeProps = CommentLikeModel; 6 | 7 | export const useLike = ({ 8 | likedByCurrentUser: initLikedByCurrentUser, 9 | likeCount: initLikeCount, 10 | }: UseLikeProps) => { 11 | const [likedByCurrentUser, setLikedByCurrentUser] = useState(initLikedByCurrentUser); 12 | const [likeCount, setLikeCount] = useState(initLikeCount); 13 | 14 | const likeComment = () => { 15 | setLikedByCurrentUser(true); 16 | setLikeCount(prev => prev + 1); 17 | }; 18 | 19 | const cancelLikeComment = () => { 20 | setLikedByCurrentUser(false); 21 | setLikeCount(prev => (prev === 0 ? prev : prev - 1)); 22 | }; 23 | 24 | return { 25 | likedByCurrentUser, 26 | likeCount, 27 | likeComment, 28 | cancelLikeComment, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/CommunityAdmin/CommunityAdmin.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { CommunityAdmin } from './CommunityAdmin'; 4 | import { CommunityAdminCreate } from './CommunityAdminCreate.client'; 5 | import { CommunityAdminEdit } from './CommunityAdminEdit.client'; 6 | 7 | const meta: Meta = { 8 | title: 'modules/CommunityAdmin', 9 | component: CommunityAdmin, 10 | }; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | render: () => , 16 | }; 17 | 18 | export default meta; 19 | 20 | export const Create: StoryObj = { 21 | render: () => , 22 | }; 23 | 24 | export const Edit: StoryObj = { 25 | render: () => , 26 | }; 27 | -------------------------------------------------------------------------------- /src/modules/CommunityAdmin/CommunityAdmin.type.ts: -------------------------------------------------------------------------------- 1 | export type DuplicateState = 'DEFAULT' | 'SUCCESS' | 'ERROR'; 2 | -------------------------------------------------------------------------------- /src/modules/CommunityProfile/CommunityLogoImage.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | type LogoSize = 'small' | 'medium' | 'large'; 5 | type CommunityLogoImageProps = { 6 | logoImageUrl?: string; 7 | size?: LogoSize; 8 | }; 9 | 10 | const sizes: Record = { 11 | small: 'h-20pxr w-20pxr', 12 | medium: 'h-32pxr w-32pxr', 13 | large: 'h-60pxr w-60pxr', 14 | }; 15 | export const CommunityLogoImage = ({ logoImageUrl, size = 'large' }: CommunityLogoImageProps) => { 16 | const logoSize = sizes[size]; 17 | const defaultPlanetLogoImage = logoImageUrl || '/assets/images/default_planet_logo.png'; 18 | 19 | return ( 20 |
    21 | planet logo image 27 |
    28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/modules/CommunityProfile/CommunityProfile.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { CommunityBgImage } from './CommunityBgImage.client'; 4 | import { CommunityProfile } from './CommunityProfile'; 5 | 6 | const meta: Meta = { 7 | title: 'modules/CommunityProfile', 8 | component: CommunityProfile, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | export const CommunityProfileStory: Story = { 15 | render: () => ( 16 | 21 | ), 22 | }; 23 | 24 | export const CommunityBgImageStory: StoryObj = { 25 | render: () => ( 26 | 37 | ), 38 | }; 39 | -------------------------------------------------------------------------------- /src/modules/CommunityProfile/CommunityProfile.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { CommunityDetailModel } from '~/types/community'; 4 | 5 | import { CommunityLogoImage } from './CommunityLogoImage'; 6 | 7 | type CommunityProfileProps = Pick< 8 | CommunityDetailModel, 9 | 'logoImageUrl' | 'description' | 'userCount' 10 | > & { top?: ReactNode }; 11 | 12 | export const CommunityProfile = ({ 13 | logoImageUrl, 14 | description, 15 | userCount, 16 | top, 17 | }: CommunityProfileProps) => { 18 | return ( 19 |
    20 | {top} 21 |
    22 | 23 |
    24 |

    {`주민 ${userCount}명`}

    25 |

    {description}

    26 |
    27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/CommunityProfile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CommunityBgImage.client'; 2 | export * from './CommunityLogoImage'; 3 | export * from './CommunityProfile'; 4 | -------------------------------------------------------------------------------- /src/modules/CreateIdCardButton/CreateIdCardButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { CreateIdCardButton } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/CreateIdCardButton', 7 | component: CreateIdCardButton, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/CreateIdCardButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CreateIdCardButton.client'; 2 | -------------------------------------------------------------------------------- /src/modules/IdCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCard.client'; 2 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/Form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCardCreationForm'; 2 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/IdCardCreation.type.ts: -------------------------------------------------------------------------------- 1 | export type CreationSteps = 'BOARDING' | 'PROFILE' | 'KEYWORD' | 'KEYWORD_CONTENT' | 'COMPLETE'; 2 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/Step/KeywordContentImage.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCallback } from 'react'; 4 | import { useFormContext } from 'react-hook-form'; 5 | 6 | import { CancelCircleIcon } from '~/components/Icon'; 7 | import { IdCardCreationFormModel } from '~/types/idCard'; 8 | type KeywordContentImageProps = { 9 | index: number; 10 | }; 11 | 12 | export const KeywordContentImage = ({ index }: KeywordContentImageProps) => { 13 | const { watch, setValue } = useFormContext(); 14 | const { keywords } = watch(); 15 | const imageUrl = keywords[index].imageUrl; 16 | 17 | const onCancelClick = useCallback(() => { 18 | setValue(`keywords.${index}.imageUrl`, ''); 19 | }, [index, setValue]); 20 | 21 | return imageUrl ? ( 22 |
    23 | {/* eslint-disable-next-line @next/next/no-img-element */} 24 | image preview 25 |
    26 | 27 |
    28 |
    29 | ) : null; 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/Step/KeywordContentStep.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useFormContext } from 'react-hook-form'; 3 | 4 | import { KeywordContentEditCard } from '~/modules/KeywordContentEditCard'; 5 | import { FormKeywordModel, IdCardCreationFormModel } from '~/types/idCard'; 6 | 7 | const title = '나를 소개하는 키워드의\n 설명을 적어주세요!'; 8 | 9 | export const KeywordContentStep = () => { 10 | const { getValues } = useFormContext(); 11 | const keywords = getValues('keywords'); 12 | 13 | return ( 14 |
    15 |

    {title}

    16 |
    17 | {keywords.map((keyword: FormKeywordModel, index: number) => { 18 | return ; 19 | })} 20 |
    21 |
    22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/Step/KeywordStep.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Controller, useFormContext } from 'react-hook-form'; 4 | 5 | import { KeywordInput } from '~/components/KeywordInput'; 6 | import { DEFAULT_RECOMMEND_KEYWORD_OPTIONS } from '~/constant/recommendKeyword'; 7 | import { IdCardCreationFormModel } from '~/types/idCard'; 8 | 9 | const title = '이웃 주민에게 자신을 소개할\n 키워드를 적어주세요!'; 10 | 11 | export const KeywordStep = () => { 12 | const { control } = useFormContext(); 13 | 14 | return ( 15 |
    16 |

    {title}

    17 | ( 21 | 32 | )} 33 | /> 34 |
    35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/Step/LoadingStep.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | 5 | type LoadingStepProps = { 6 | planetName: string; 7 | }; 8 | 9 | export const LoadingStep = ({ planetName }: LoadingStepProps) => { 10 | return ( 11 |
    12 |

    {`${planetName}으로\n광속으로 이동중...`}

    13 | planet image 22 |
    23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/Step/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BoardingStep.client'; 2 | export * from './CompleteStep.client'; 3 | export * from './KeywordContentImage.client'; 4 | export * from './KeywordContentStep.client'; 5 | export * from './KeywordStep.client'; 6 | export * from './LoadingStep.client'; 7 | export * from './ProfileStep.client'; 8 | -------------------------------------------------------------------------------- /src/modules/IdCardCreation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Form'; 2 | export * from './IdCardCreationSteps'; 3 | export * from './Step'; 4 | -------------------------------------------------------------------------------- /src/modules/IdCardDetail/KeywordContentCard.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { tw } from '~/utils/tailwind.util'; 4 | 5 | type KeywordContentCardProps = { 6 | title: ReactNode | string; 7 | image: ReactNode | null; 8 | content: ReactNode | string; 9 | className?: string; 10 | onClick?: () => void; 11 | }; 12 | 13 | export const KeywordContentCard = ({ 14 | title, 15 | image, 16 | content, 17 | className, 18 | onClick, 19 | ...props 20 | }: KeywordContentCardProps) => { 21 | return ( 22 |
    23 |

    {title}

    24 |
    25 | {image} 26 |

    {content}

    27 |
    28 |
    29 |
    30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/IdCardDetail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Intro'; 2 | export * from './KeywordContentCard'; 3 | -------------------------------------------------------------------------------- /src/modules/IdCardEditButton/IdCardEditButton.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { usePathname, useRouter } from 'next/navigation'; 3 | 4 | import { Button } from '~/components/Button'; 5 | 6 | export const IdCardEditButton = () => { 7 | const pathname = usePathname(); 8 | const router = useRouter(); 9 | 10 | const onClickIdCardEditButton = () => { 11 | router.push(`${pathname}/edit`); 12 | }; 13 | 14 | return ( 15 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/IdCardEditButton/IdCardEditButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { IdCardEditButton } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/IdCardEditButton', 7 | component: IdCardEditButton, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/IdCardEditButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCardEditButton.client'; 2 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/Form/IdCardEditorForm.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from 'react'; 2 | 3 | import { EditorSteps } from '~/modules/IdCardEditor/IdCardEditor.type'; 4 | import { 5 | EditKeywordContentStep, 6 | EditKeywordStep, 7 | EditProfileInfoStep, 8 | } from '~/modules/IdCardEditor/Step'; 9 | 10 | type IdCardEditorFormProps = { 11 | steps: EditorSteps[]; 12 | stepOrder: number; 13 | 14 | onClickMoveTargetStep: (targetStep: EditorSteps) => void; 15 | }; 16 | 17 | export const IdCardEditorForm = ({ 18 | steps, 19 | stepOrder, 20 | onClickMoveTargetStep, 21 | }: IdCardEditorFormProps) => { 22 | return ( 23 |
    ) => e.preventDefault()}> 24 | {steps[stepOrder] === 'KEYWORD_CONTENT' && ( 25 | 26 | )} 27 | {steps[stepOrder] === 'KEYWORD' && } 28 | {steps[stepOrder] === 'PROFILE' && } 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/Form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCardEditorForm'; 2 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/IdCardEditor.constant.ts: -------------------------------------------------------------------------------- 1 | import { EditorSteps } from '~/modules/IdCardEditor/IdCardEditor.type'; 2 | 3 | // 순서가 있지는 않음. KEYWORD_CONTENT: 최초 진인접, / PROFILE, KEYWORD은 같은 깊이 4 | export const editorSteps: EditorSteps[] = ['KEYWORD_CONTENT', 'PROFILE', 'KEYWORD']; 5 | 6 | export const KEYWORD_CONTENT_STEP = 0; 7 | export const PROFILE_STEP = 1; 8 | export const KEYWORD_STEP = 2; 9 | 10 | // TODO: 전체 form에서 사용되는 로직과 동일합니다! 추후 src/constant로 옮겨도 될 듯 해요~ 11 | export const MAX_KEYWORD_LIST_LENGTH = 7; 12 | export const MAX_KEYWORD_INPUT_LENGTH = 8; 13 | export const MAX_NICKNAME_LENGTH = 16; 14 | export const MAX_ABOUT_ME_LENGTH = 50; 15 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/IdCardEditor.type.ts: -------------------------------------------------------------------------------- 1 | import { IdCardEditorFormModel } from '~/types/idCard'; 2 | 3 | export type EditorSteps = 'PROFILE' | 'KEYWORD' | 'KEYWORD_CONTENT'; 4 | 5 | export type IdCardEditorFormValues = Omit; 6 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/IdCardEditorSteps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { IdCardEditor } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/IdCardEditor', 7 | component: IdCardEditor, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/Step/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EditKeywordContentStep.client'; 2 | export * from './EditKeywordStep.client'; 3 | export * from './EditProfileInfoStep.client'; 4 | -------------------------------------------------------------------------------- /src/modules/IdCardEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IdCardEditor'; 2 | -------------------------------------------------------------------------------- /src/modules/InvitationButtons/InvitationButtons.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { InvitationButtons } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/InvitationButtons', 7 | component: InvitationButtons, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/InvitationButtons/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './InvitationButtons.client'; 2 | -------------------------------------------------------------------------------- /src/modules/KeywordContentEditCard/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './KeywordContentEditCard.client'; 2 | -------------------------------------------------------------------------------- /src/modules/LoginStep/AppleLoginButton.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Image from 'next/image'; 3 | 4 | export const AppleLoginButton = () => { 5 | return ( 6 |
    7 | 16 |
    17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/modules/LoginStep/LoginStep.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { LoginStep } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/LoginStep', 7 | component: LoginStep, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/LoginStep/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LoginStep.client'; 2 | -------------------------------------------------------------------------------- /src/modules/LoginStep/kakaoLoginButton.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Image from 'next/image'; 3 | import { ClientSafeProvider, signIn } from 'next-auth/react'; 4 | 5 | type KakaoLoginButtonProps = { 6 | provider: ClientSafeProvider; 7 | }; 8 | 9 | export const KakaoLoginButton = ({ provider }: KakaoLoginButtonProps) => { 10 | return ( 11 |
    12 | 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/LoginStep/style.css: -------------------------------------------------------------------------------- 1 | .swiper-pagination-bullet { 2 | width: 8px !important; 3 | height: 8px !important; 4 | /* bg-grey-500 */ 5 | background-color: #949494 !important; 6 | } 7 | 8 | .swiper-pagination-bullet-active { 9 | width: 28px !important; 10 | border-radius: 41px !important; 11 | /* bg-primary-500 */ 12 | background-color: #5445ff !important; 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/Notification/Notification.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | 3 | import { createNotification, createNotificationList } from '~/mocks/notification/notification.mock'; 4 | 5 | import { NotificationItem } from './NotificationItem'; 6 | import { NotificationList } from './NotificationList'; 7 | import { NotificationNoData } from './NotificationNoData'; 8 | 9 | const meta: Meta = { 10 | title: 'modules/Notification', 11 | component: NotificationItem, 12 | args: {}, 13 | }; 14 | 15 | export default meta; 16 | 17 | const MOCK_NOTIFICATION = createNotification(123); 18 | export const Item = () => ; 19 | 20 | const MOCK_NOTIFICATIONS = createNotificationList(10, 1, 10); 21 | export const List = () => ( 22 |
    23 | 24 |
    25 | ); 26 | 27 | export const NoData = () => ; 28 | -------------------------------------------------------------------------------- /src/modules/Notification/NotificationList.tsx: -------------------------------------------------------------------------------- 1 | import { NotificationModel } from '~/types/notification'; 2 | 3 | import { NotificationItem } from './NotificationItem'; 4 | 5 | type NotificationListProps = { 6 | notifications: NotificationModel[]; 7 | notificationAgoList?: Record; 8 | }; 9 | export const NotificationList = ({ notifications, notificationAgoList }: NotificationListProps) => { 10 | return ( 11 |
      12 | {notifications.map((notification, i) => ( 13 |
      14 | {!!notificationAgoList?.[notification.notificationId] && ( 15 | <> 16 | {i !== 0 &&
      } 17 |

      {notificationAgoList[notification.notificationId]}

      18 | 19 | )} 20 | 21 |
      22 | ))} 23 |
    24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/Notification/NotificationNoData.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | export const NotificationNoData = () => { 4 | return ( 5 |
    6 |
    7 |
    8 | ufo 9 |
    10 |
    11 | 12 |

    13 | 아직 알람이 없어요. 14 |
    15 | 다른 주민들에게 먼저 말을 건네보세요. 16 |

    17 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/modules/Notification/NotificationTabItem.client.tsx: -------------------------------------------------------------------------------- 1 | import { CommunityLogoImage } from '~/modules/CommunityProfile'; 2 | import { twMerge } from '~/utils/tailwind.util'; 3 | 4 | import { CommunityNotification } from './NotificationTab.client'; 5 | 6 | type NotificationTabProps = { 7 | community: CommunityNotification; 8 | isActive: boolean; 9 | onClick: (communityId: number) => void; 10 | }; 11 | export const NotificationTabItem = ({ community, isActive, onClick }: NotificationTabProps) => { 12 | return ( 13 |
  • onClick(community.communityId)} 15 | className={twMerge( 16 | 'relative mr-24pxr flex items-center gap-4pxr border-b-2 border-b-white pb-10pxr pr-8pxr text-gray-400', 17 | isActive && 'border-b-black text-black', 18 | )} 19 | > 20 | {community.logoImageUrl && ( 21 | 22 | )} 23 | {community.title} 24 | {community.hasNewNotification && ( 25 |
    28 | )} 29 |
  • 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/NudgeItem/NudgeItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { NudgeItem } from './NudgeItem'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/NudgeItem', 7 | component: NudgeItem, 8 | }; 9 | 10 | type Story = StoryObj; 11 | 12 | const opponentUser = { 13 | nickname: '최예원', 14 | profileImageUrl: 15 | 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1095.jpg', 16 | userId: 1, 17 | }; 18 | 19 | export const Default: Story = { 20 | render: () => ( 21 | 28 | ), 29 | }; 30 | 31 | export default meta; 32 | -------------------------------------------------------------------------------- /src/modules/NudgeItem/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NudgeItem'; 2 | -------------------------------------------------------------------------------- /src/modules/NudgeList/NudgeList.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useGetNudgeList } from '~/api/domain/nudge.api.client'; 4 | import { UseBottomSheetReturn } from '~/components/BottomSheet'; 5 | import BottomSheet from '~/components/BottomSheet/BottomSheet'; 6 | import { NudgeItem } from '~/modules/NudgeItem'; 7 | 8 | type NudgeListProps = { 9 | bottomSheetHandlers: UseBottomSheetReturn; 10 | idCardsId: number; 11 | communityId: number; 12 | }; 13 | 14 | export const NudgeList = ({ bottomSheetHandlers, idCardsId, communityId }: NudgeListProps) => { 15 | const { data } = useGetNudgeList(idCardsId); 16 | return ( 17 |
    18 | 19 | 받은 딩동 20 | 21 |
      22 | {data?.nudgeInfoDtos.map((nudge, idx) => ( 23 | 24 | ))} 25 |
    26 |
    27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/NudgeList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NudgeList.client'; 2 | -------------------------------------------------------------------------------- /src/modules/Onboarding/CharacterCreation.type.ts: -------------------------------------------------------------------------------- 1 | export type CharacterCreationFormType = { 2 | firstAlphabet: 'E' | 'I'; 3 | secondAlphabet: 'F' | 'T'; 4 | thirdAlphabet: 'J' | 'P'; 5 | fourthAlphabet: 'S' | 'N'; 6 | }; 7 | 8 | export type CharacterAlphabetType = 'E' | 'I' | 'F' | 'T' | 'J' | 'P' | 'S' | 'N'; 9 | 10 | export type CharacterCreationStepsType = 11 | | 'BOARDING' 12 | | 'FIRST' 13 | | 'SECOND' 14 | | 'THIRD' 15 | | 'FOURTH' 16 | | 'COMPLETE'; 17 | -------------------------------------------------------------------------------- /src/modules/Onboarding/CharacterCreationSteps.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { CharacterCreationSteps } from './CharacterCreationSteps.client'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/CharacterCreationSteps', 7 | component: CharacterCreationSteps, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /src/modules/Onboarding/Form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CharacterCreationForm.client'; 2 | -------------------------------------------------------------------------------- /src/modules/Onboarding/Question/CharacterQuestion.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { MouseEvent } from 'react'; 3 | 4 | import { CharacterQuestion } from './CharacterQuestion'; 5 | 6 | const meta: Meta = { 7 | title: 'modules/CharacterQuestion', 8 | component: CharacterQuestion, 9 | args: {}, 10 | }; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = { 17 | args: { 18 | title: '홀로 우주 패키지 여행을\n 가게 되었다', 19 | image: '/assets/images/onboarding-question-ticket.png', 20 | firstOption: { fieldValue: 'E', content: '옆자리에 앉은 사람에게 말을 건다.' }, 21 | secondOption: { fieldValue: 'I', content: '풍경을 보며 나만의 시간을 즐긴다.' }, 22 | onQuestionButtonClick: (e: MouseEvent) => console.log(e.currentTarget.name), 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/Onboarding/Question/CharacterQuestion.type.ts: -------------------------------------------------------------------------------- 1 | import { CharacterAlphabetType, CharacterCreationFormType } from '../CharacterCreation.type'; 2 | 3 | type OptionType = { 4 | fieldValue: CharacterAlphabetType; 5 | content: string; 6 | }; 7 | 8 | export type QuestionDetail = { 9 | title: string; 10 | image: string; 11 | fieldName: keyof CharacterCreationFormType; 12 | firstOption: OptionType; 13 | secondOption: OptionType; 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/Onboarding/Step/CharacterCompleteStep.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { CharacterCompleteStep } from './CharacterCompleteStep.client'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/CharacterCompleteStep', 7 | component: CharacterCompleteStep, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = { 16 | args: { characterName: 'BUDDY' }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/Onboarding/Step/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CharacterCompleteStep.client'; 2 | -------------------------------------------------------------------------------- /src/modules/Onboarding/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CharacterCreation.type'; 2 | export * from './CharacterCreationSteps.client'; 3 | export * from './Form/CharacterCreationForm.client'; 4 | -------------------------------------------------------------------------------- /src/modules/PlanetCreationButton/PlanetCreationButton.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useRouter } from 'next/navigation'; 3 | 4 | import { PlusIcon } from '~/components/Icon'; 5 | import { tw } from '~/utils/tailwind.util'; 6 | 7 | export const PlanetCreationButton = () => { 8 | const router = useRouter(); 9 | 10 | const onClickCreateButton = () => { 11 | router.push('/admin/planet/create'); 12 | }; 13 | 14 | return ( 15 |
    16 | 26 |
    27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/modules/PlanetCreationButton/PlanetCreationButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { PlanetCreationButton } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/PlanetCreationButton', 7 | component: PlanetCreationButton, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/PlanetCreationButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PlanetCreationButton.client'; 2 | -------------------------------------------------------------------------------- /src/modules/PlanetEnterButton/PlanetEnterButton.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | 5 | import { Button, TextButton } from '~/components/Button'; 6 | import { DINGDONG_PLANET } from '~/utils/variable'; 7 | 8 | export const PlanetEnterButton = () => { 9 | const router = useRouter(); 10 | return ( 11 |
    12 | router.push(`/planet/${DINGDONG_PLANET.DINGDONG_PLANET_ID}`)} 15 | > 16 | 딩동행성 둘러보기 17 | 18 |
    19 | 22 |
    23 |
    24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/PlanetEnterButton/PlanetEnterButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { PlanetEnterButton } from './PlanetEnterButton.client'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/PlanetEnterButton', 7 | component: PlanetEnterButton, 8 | }; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | render: () => ( 14 |
    15 | 16 |
    17 | ), 18 | }; 19 | 20 | export default meta; 21 | -------------------------------------------------------------------------------- /src/modules/PlanetEnterButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PlanetEnterButton.client'; 2 | -------------------------------------------------------------------------------- /src/modules/PlanetMenu/PlanetMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { PlanetMenu } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/PlanetMenu', 7 | component: PlanetMenu, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/PlanetMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PlanetMenu.client'; 2 | -------------------------------------------------------------------------------- /src/modules/PlanetSelector/PlanetSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { TopNavigation } from '~/components/TopNavigation'; 4 | 5 | import { PlanetSelector } from './index'; 6 | 7 | const meta: Meta = { 8 | title: 'modules/PlanetSelector', 9 | component: PlanetSelector, 10 | args: {}, 11 | }; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Primary: Story = { 18 | render: () => ( 19 | 20 | 21 | 22 | 23 | 24 | ), 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/PlanetSelector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PlanetSelector.client'; 2 | -------------------------------------------------------------------------------- /src/modules/UserMenu/UserMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { UserMenu } from './index'; 4 | 5 | const meta: Meta = { 6 | title: 'modules/UserMenu', 7 | component: UserMenu, 8 | args: {}, 9 | }; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Primary: Story = { 16 | render: () => , 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/UserMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UserMenu.client'; 2 | -------------------------------------------------------------------------------- /src/stores/comment.store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type ReplyRecipient = { 4 | nickname: string | undefined; 5 | commentId: number | undefined; 6 | setReplyRecipient: (nickname: string, commentId: number) => void; 7 | clear: () => void; 8 | }; 9 | 10 | export const useReplyRecipientStore = create()(set => ({ 11 | nickname: undefined, 12 | commentId: undefined, 13 | setReplyRecipient: (nickname: string, commentId: number) => { 14 | set(() => ({ 15 | nickname: nickname, 16 | commentId: commentId, 17 | })); 18 | }, 19 | clear: () => { 20 | set(() => ({ 21 | nickname: undefined, 22 | commentId: undefined, 23 | })); 24 | }, 25 | })); 26 | -------------------------------------------------------------------------------- /src/stores/community.store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | import { DINGDONG_PLANET } from '~/utils/variable'; 4 | 5 | type Store = { 6 | communityId: number; 7 | switchCommunity: (id: number) => void; 8 | isInitPlanetId: () => boolean; 9 | }; 10 | 11 | export const useCommunityStore = create()((set, get) => ({ 12 | communityId: DINGDONG_PLANET.DINGDONG_PLANET_ID, 13 | switchCommunity: (id: number) => { 14 | document.cookie = `communityId=${id}`; 15 | set(() => ({ 16 | communityId: id, 17 | })); 18 | }, 19 | isInitPlanetId: () => { 20 | return get().communityId === DINGDONG_PLANET.DINGDONG_PLANET_ID; 21 | }, 22 | })); 23 | -------------------------------------------------------------------------------- /src/types/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './response.type'; 2 | -------------------------------------------------------------------------------- /src/types/api/response.type.ts: -------------------------------------------------------------------------------- 1 | export type SliceResponse = { 2 | content: T[]; 3 | page: number; 4 | size: number; 5 | hasNext: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './response.type'; 2 | -------------------------------------------------------------------------------- /src/types/auth/model.type.ts: -------------------------------------------------------------------------------- 1 | export type KakaoPropertiesModel = { 2 | nickname: string; 3 | profile_image: string; 4 | thumbnail_image: string; 5 | }; 6 | 7 | export type KakaoAccountModel = { 8 | email: string; 9 | age_range: string; 10 | birthday: string; 11 | gender: string; 12 | }; 13 | -------------------------------------------------------------------------------- /src/types/auth/response.type.ts: -------------------------------------------------------------------------------- 1 | import { KakaoAccountModel, KakaoPropertiesModel } from '~/types/auth/model.type'; 2 | 3 | export type AuthResponse = { 4 | data: unknown; 5 | accessToken: string; 6 | refreshToken: string; 7 | userId: number; 8 | accessTokenExpireDate: number; 9 | success?: boolean; 10 | }; 11 | 12 | export const AUTH_COOKIE_KEYS: Record = { 13 | accessToken: 'dingdong_at', 14 | refreshToken: 'dingdong_rt', 15 | userId: 'dingdong_uid', 16 | accessTokenExpireDate: 'dingdong_at_expire_date', 17 | } as const; 18 | 19 | export type KakaoUserInfoResponse = { 20 | id: string; 21 | properties: KakaoPropertiesModel; 22 | kakao_account: KakaoAccountModel; 23 | }; 24 | -------------------------------------------------------------------------------- /src/types/comment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | export * from './request.type'; 3 | export * from './response.type'; 4 | -------------------------------------------------------------------------------- /src/types/comment/model.type.ts: -------------------------------------------------------------------------------- 1 | export type CommentLikeModel = { 2 | likeCount: number; 3 | likedByCurrentUser: boolean; 4 | }; 5 | 6 | export type CommentWriterIntoModel = { 7 | userId: number; 8 | nickname: string; 9 | profileImageUrl: string; 10 | }; 11 | 12 | export type CommentModel = { 13 | idCardId: number; 14 | commentId: number; 15 | content: string; 16 | createdAt: string; 17 | writerInfo: CommentWriterIntoModel; 18 | commentLikeInfo: CommentLikeModel; 19 | repliesCount: number; 20 | }; 21 | 22 | export type CommentReplyModel = { 23 | commentReplyId: number; 24 | content: string; 25 | createdAt: string; 26 | writerInfo: CommentWriterIntoModel; 27 | commentReplyLikeInfo: CommentLikeModel; 28 | }; 29 | -------------------------------------------------------------------------------- /src/types/comment/response.type.ts: -------------------------------------------------------------------------------- 1 | import { SliceResponse } from '~/types/api'; 2 | import { CommentModel, CommentReplyModel } from '~/types/comment/model.type'; 3 | 4 | export type CommentGetResponse = SliceResponse; 5 | 6 | export type CommentCountGetResponse = { 7 | count: number; 8 | }; 9 | 10 | export type CommentReplyGetResponse = { 11 | commentId: number; 12 | repliesInfo: CommentReplyModel[]; 13 | }; 14 | 15 | export type CommentPostResponse = { 16 | id: number; 17 | }; 18 | 19 | export type CommentDeleteResponse = { 20 | id: number; 21 | }; 22 | 23 | export type CommentPostReplyResponse = { 24 | id: number; 25 | }; 26 | 27 | export type CommentDeleteReplyResponse = { 28 | id: number; 29 | }; 30 | 31 | export type CommentLikePostResponse = { 32 | id: number; 33 | }; 34 | 35 | export type CommentReplyLikePostResponse = { 36 | id: number; 37 | }; 38 | 39 | export type CommentLikeCancelDeleteResponse = { 40 | id: number; 41 | }; 42 | 43 | export type CommentReplyLikeCancelDeleteResponse = { 44 | id: number; 45 | }; 46 | -------------------------------------------------------------------------------- /src/types/community/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | export * from './request.type'; 3 | export * from './response.type'; 4 | -------------------------------------------------------------------------------- /src/types/community/request.type.ts: -------------------------------------------------------------------------------- 1 | import { CommunityJoinModel } from './model.type'; 2 | 3 | export type CommunityIdCardsRequest = { 4 | pageParam?: number; 5 | communityId: number; 6 | }; 7 | 8 | export type CreateCommunityRequest = { 9 | name: string; 10 | logoImageUrl?: string; 11 | coverImageUrl?: string; 12 | description?: string; 13 | }; 14 | 15 | export type CommunityJoinRequest = CommunityJoinModel; 16 | -------------------------------------------------------------------------------- /src/types/community/response.type.ts: -------------------------------------------------------------------------------- 1 | import { SliceResponse } from '~/types/api'; 2 | import { 3 | CheckIdCardModel, 4 | CommunityDetailModel, 5 | CommunityIdCardsModel, 6 | CommunityListModel, 7 | CommunityUserInfoModel, 8 | InvitationCodeValidationModel, 9 | } from '~/types/community'; 10 | 11 | export type CommunityIdCardsResponse = SliceResponse; 12 | 13 | export type CommunityDetailResponse = { 14 | communityDetailsDto: CommunityDetailModel; 15 | }; 16 | 17 | export type CommunityListResponse = { 18 | communityListDtos: CommunityListModel[]; 19 | }; 20 | 21 | export type CommunityUpdateResponse = { 22 | id: number; 23 | }; 24 | 25 | export type InvitationCodeValidationResponse = InvitationCodeValidationModel; 26 | export type CommunityNameCheckResponse = { 27 | data: boolean; 28 | }; 29 | 30 | export type CheckIdCardResponse = CheckIdCardModel; 31 | 32 | export type CommunityUserInfoResponse = { 33 | myInfoInInCommunityDto: CommunityUserInfoModel; 34 | }; 35 | -------------------------------------------------------------------------------- /src/types/errorCodes.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_ERROR_CODES = { 2 | UNAUTHORIZED_ERROR: '401', 3 | EXPIRED_TOKEN_ERROR: '401-1', 4 | INVALID_TOKEN_ERROR: '401-2', 5 | EXPIRED_REFRESH_TOKEN_ERROR: '401-3', 6 | NOT_VALID_ACCESS_TOKEN_ERROR: '401-4', 7 | } as const; 8 | -------------------------------------------------------------------------------- /src/types/idCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | export * from './request.type'; 3 | export * from './response.type'; 4 | -------------------------------------------------------------------------------- /src/types/idCard/request.type.ts: -------------------------------------------------------------------------------- 1 | import { IdCardCreationFormModel, IdCardEditorFormModel } from '~/types/idCard'; 2 | 3 | export type CreateIdCardRequest = IdCardCreationFormModel; 4 | export type IdCardCreateRequest = IdCardCreationFormModel; 5 | 6 | export type EditIdCardRequest = IdCardEditorFormModel; 7 | -------------------------------------------------------------------------------- /src/types/idCard/response.type.ts: -------------------------------------------------------------------------------- 1 | import { IdCardDetailModel } from '~/types/idCard'; 2 | 3 | export type CommentCountResponse = { 4 | count: number; 5 | }; 6 | 7 | export type IdCardDetailResponse = { 8 | idCardDetailsDto: IdCardDetailModel; 9 | }; 10 | 11 | export type CommunityMyIdCardDetailResponse = { 12 | idCardDetailsDto: IdCardDetailModel; 13 | }; 14 | 15 | export type IdCardCreateResponse = { id: number }; 16 | 17 | export type IdCardEditResponse = { 18 | id: number; 19 | }; 20 | -------------------------------------------------------------------------------- /src/types/image/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | -------------------------------------------------------------------------------- /src/types/image/model.type.ts: -------------------------------------------------------------------------------- 1 | export type ImageUrlModel = { 2 | imageUrl: string; 3 | }; 4 | 5 | export type ImageFileModel = File; 6 | -------------------------------------------------------------------------------- /src/types/image/request.type.ts: -------------------------------------------------------------------------------- 1 | import { ImageFileModel } from './model.type'; 2 | 3 | export type ImageFileRequest = ImageFileModel; 4 | -------------------------------------------------------------------------------- /src/types/image/response.type.ts: -------------------------------------------------------------------------------- 1 | import { ImageUrlModel } from './model.type'; 2 | 3 | export type IamgeUrlResponse = ImageUrlModel; 4 | -------------------------------------------------------------------------------- /src/types/notification/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | export * from './request.type'; 3 | export * from './response.type'; 4 | -------------------------------------------------------------------------------- /src/types/notification/request.type.ts: -------------------------------------------------------------------------------- 1 | export type NotificationGetRequest = { 2 | pageParam: number; 3 | }; 4 | -------------------------------------------------------------------------------- /src/types/notification/response.type.ts: -------------------------------------------------------------------------------- 1 | import { NotificationModel } from '~/types/notification'; 2 | 3 | export type NotificationGetResponse = { 4 | notificationDtos: NotificationModel[]; 5 | }; 6 | 7 | export type UnreadNotification = { 8 | data: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /src/types/nudge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | -------------------------------------------------------------------------------- /src/types/nudge/model.type.ts: -------------------------------------------------------------------------------- 1 | export type NudgeModel = 'FRIENDLY' | 'SIMILARITY' | 'TALKING' | 'MEET'; 2 | 3 | export type NudgeListModel = { 4 | nudgeId: number; 5 | opponentUser: { 6 | userId: number; 7 | profileImageUrl: string; 8 | nickname: string; 9 | }; 10 | toUserNudgeType: NudgeModel; 11 | fromUserNudgeType: NudgeModel | null; 12 | }; 13 | 14 | export type NudgeIconSelectorType = 'DEFAULT' | NudgeModel; 15 | 16 | export type NudgeMessagesType = { text: string; id: NudgeModel }[]; 17 | 18 | export const nudgeMessages: NudgeMessagesType = [ 19 | { 20 | id: 'MEET', 21 | text: '만나서 반가워요', 22 | }, 23 | { 24 | id: 'FRIENDLY', 25 | text: '친해지고 싶어요', 26 | }, 27 | { 28 | id: 'SIMILARITY', 29 | text: '저와 비슷해요', 30 | }, 31 | { 32 | id: 'TALKING', 33 | text: '같이 밥 한끼 해요', 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /src/types/nudge/request.type.ts: -------------------------------------------------------------------------------- 1 | import { NudgeModel } from '~/types/nudge/model.type'; 2 | 3 | export type NudgePostRequest = { nudgeType: NudgeModel; communityId: number }; 4 | 5 | export type NudgePutRequest = NudgePostRequest; 6 | -------------------------------------------------------------------------------- /src/types/nudge/response.type.ts: -------------------------------------------------------------------------------- 1 | import { NudgeListModel } from '~/types/nudge/model.type'; 2 | 3 | export type NudgeListResponse = { 4 | nudgeInfoDtos: NudgeListModel[]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model.type'; 2 | export * from './request.type'; 3 | export * from './response.type'; 4 | -------------------------------------------------------------------------------- /src/types/user/model.type.ts: -------------------------------------------------------------------------------- 1 | import { CharacterNameModel } from '../idCard'; 2 | 3 | export type UserInfoModel = { 4 | userId: number; 5 | email: string; 6 | nickname: string; 7 | gender: string; 8 | ageRange: string; 9 | profileImageUrl: string; 10 | characterType?: CharacterNameModel; 11 | communityIds: number[]; 12 | }; 13 | 14 | export type CharacterCreateModel = CharacterNameModel; 15 | -------------------------------------------------------------------------------- /src/types/user/request.type.ts: -------------------------------------------------------------------------------- 1 | // user.d.ts 정리 후 절대경로 적용 2 | import { CharacterCreateModel } from './model.type'; 3 | import { UserInfoResponse } from './response.type'; 4 | 5 | export type UserInfoRequest = Omit; 6 | 7 | export type CharacterCreateRequest = CharacterCreateModel; 8 | -------------------------------------------------------------------------------- /src/types/user/response.type.ts: -------------------------------------------------------------------------------- 1 | import { UserInfoModel } from './model.type'; 2 | 3 | export type UserInfoResponse = { 4 | userProfileDto: UserInfoModel; 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/util.d.ts: -------------------------------------------------------------------------------- 1 | export type ClassNameType = ClassNameArray | string | null | undefined | 0 | false; 2 | -------------------------------------------------------------------------------- /src/utils/auth/error.ts: -------------------------------------------------------------------------------- 1 | import { ApiError } from '~/api/config/customError'; 2 | 3 | export class UserIdNotFoundError extends Error { 4 | constructor() { 5 | super('로그인이 필요합니다.'); 6 | this.name = 'UserIdNotFoundError'; 7 | } 8 | } 9 | 10 | export const isUnauthorizedError = (error: unknown): boolean => { 11 | if (error instanceof ApiError) { 12 | if (error.statusCode === 401) { 13 | return true; 14 | } 15 | } 16 | // NOTE: redirect가 server side(컴포넌트 외부)에서는 NEXT_REDIRECT 에러를 던지는 것으로 동작합니다. https://github.com/vercel/next.js/issues/42556 17 | // interceptor.server.ts의 onResponseErrorServer 함수에서 미로그인시 NEXT_REDIRECT 에러를 던지고 있습니다. 18 | if ( 19 | error && 20 | typeof error === 'object' && 21 | 'message' in error && 22 | error['message'] === 'NEXT_REDIRECT' 23 | ) { 24 | return true; 25 | } 26 | return false; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/auth/getUserId.client.ts: -------------------------------------------------------------------------------- 1 | import { UserIdNotFoundError } from './error'; 2 | import { getAuthTokensByCookie } from './tokenHandlers'; 3 | 4 | export const getUserIdClient = (): number | undefined => { 5 | try { 6 | if (typeof document === 'undefined') throw new UserIdNotFoundError(); 7 | const { userId } = getAuthTokensByCookie(document.cookie); 8 | if (userId !== undefined) return userId; 9 | throw new UserIdNotFoundError(); // 비로그인한 사용자의 경우 10 | } catch (e) { 11 | return undefined; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/auth/getUserId.server.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | 3 | import { AUTH_COOKIE_KEYS } from '~/types/auth'; 4 | 5 | import { UserIdNotFoundError } from './error'; 6 | 7 | export const getUserIdServer = (): number | undefined => { 8 | try { 9 | const cookieStore = cookies(); 10 | const userId = cookieStore.get(AUTH_COOKIE_KEYS.userId)?.value; 11 | if (userId === undefined) throw new UserIdNotFoundError(); 12 | const userIdNumber = Number(userId); 13 | if (isNaN(userIdNumber)) throw new UserIdNotFoundError(); 14 | return userIdNumber; 15 | } catch (e) { 16 | return undefined; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/auth/loginProviders.ts: -------------------------------------------------------------------------------- 1 | import { ClientSafeProvider } from 'next-auth/react'; 2 | 3 | export const KAKAO_PROVIDER: ClientSafeProvider = { 4 | callbackUrl: '/auth/callback/kakao', 5 | id: 'kakao', 6 | name: 'Kakao', 7 | type: 'oauth', 8 | signinUrl: '/auth/signin/kakao', 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/auth/tokenValidator.client.ts: -------------------------------------------------------------------------------- 1 | import { ApiError } from '~/api/config/customError'; 2 | import { reissue } from '~/api/domain/auth.api.client'; 3 | import { AuthResponse } from '~/types/auth'; 4 | 5 | import { generateCookiesKeyValues } from './tokenHandlers'; 6 | 7 | export const getAccessTokenClient = async ( 8 | authTokens: Partial, 9 | ): Promise => { 10 | try { 11 | const { refreshToken } = authTokens; 12 | if (refreshToken) { 13 | // token refresh 로직 처리 14 | const { success, ...tokens } = await reissue(refreshToken); 15 | if (!success) { 16 | return null; 17 | } 18 | for (const [cookieKey, cookieValue] of generateCookiesKeyValues(tokens)) { 19 | document.cookie = `${cookieKey}=${cookieValue}; path=/;`; 20 | } 21 | 22 | return tokens.accessToken; 23 | } else { 24 | return null; 25 | } 26 | } catch (e) { 27 | return e as ApiError; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/auth/tokenValidator.server.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | 3 | import { ApiError } from '~/api/config/customError'; 4 | import { reissue } from '~/api/domain/auth.api.server'; 5 | import { AuthResponse } from '~/types/auth'; 6 | 7 | import { generateCookiesKeyValues } from './tokenHandlers'; 8 | 9 | export const getAccessTokenServer = async ( 10 | authTokens: Partial, 11 | ): Promise => { 12 | try { 13 | const { refreshToken } = authTokens; 14 | if (refreshToken) { 15 | // token refresh 로직 처리 16 | const { success, ...tokens } = await reissue(refreshToken); 17 | if (!success) { 18 | return null; 19 | } 20 | const cookieStore = cookies(); 21 | for (const [cookieKey, cookieValue] of generateCookiesKeyValues(tokens)) { 22 | cookieStore.set(cookieKey, cookieValue as string); 23 | } 24 | 25 | return tokens.accessToken; 26 | } else { 27 | return null; 28 | } 29 | } catch (e) { 30 | return e as ApiError; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/cookie.util.ts: -------------------------------------------------------------------------------- 1 | // Ref : https://ko.javascript.info/cookie 2 | 3 | export const getCookie = (name: string) => { 4 | const matches = 5 | typeof window !== 'undefined' && 6 | document.cookie.match( 7 | // eslint-disable-next-line no-useless-escape 8 | new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)'), 9 | ); 10 | return matches ? decodeURIComponent(matches[1]) : undefined; 11 | }; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | export const setCookie = (name: string, value: any, options?: any) => { 15 | const cookieOptions = { 16 | path: '/', 17 | ...options, 18 | }; 19 | 20 | let updatedCookie = encodeURIComponent(name) + '=' + encodeURIComponent(value); 21 | 22 | for (const optionKey in cookieOptions) { 23 | updatedCookie += '; ' + optionKey; 24 | const optionValue = cookieOptions[optionKey]; 25 | if (optionValue !== true) { 26 | updatedCookie += '=' + optionValue; 27 | } 28 | } 29 | document.cookie = updatedCookie; 30 | }; 31 | 32 | export const deleteCookie = (name: string) => { 33 | setCookie(name, '', { 34 | 'max-age': -1, 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/route/route.ts: -------------------------------------------------------------------------------- 1 | export const ROUTE_COOKIE_KEYS = { 2 | redirectUri: 'redirectUri', 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/util.common.ts: -------------------------------------------------------------------------------- 1 | export const isProd = (env: string): boolean => env === 'production'; 2 | 3 | type Entries = { 4 | [K in keyof T]: [K, T[K]]; 5 | }[keyof T][]; 6 | 7 | export const getEntries = (obj: T) => Object.entries(obj) as Entries; 8 | 9 | export const isEqual = (obj1: any, obj2: any): boolean => { 10 | // 값이 같으면 true 반환 11 | if (obj1 === obj2) { 12 | return true; 13 | } 14 | 15 | // 객체 혹은 배열인 경우 16 | if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) { 17 | const keys1 = Object.keys(obj1); 18 | const keys2 = Object.keys(obj2); 19 | 20 | // 속성 개수가 다른 경우 false 반환 21 | if (keys1.length !== keys2.length) { 22 | return false; 23 | } 24 | 25 | // 모든 속성에 대해 재귀적으로 비교 26 | for (const key of keys1) { 27 | if (!isEqual(obj1[key], obj2[key])) { 28 | return false; 29 | } 30 | } 31 | 32 | return true; 33 | } 34 | 35 | // 나머지 경우는 값이 다르므로 false 반환 36 | return false; 37 | }; 38 | 39 | export const isEmptyText = (text: string) => { 40 | if (!text || text.trim() === '') { 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | export const isValidUrl = (url: string) => { 2 | try { 3 | new URL(url); 4 | return true; 5 | } catch (e) { 6 | return false; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/variable.ts: -------------------------------------------------------------------------------- 1 | export const DINGDONG_PLANET = { 2 | DINGDONG_PLANET_ID: 1, 3 | CHARACTER_PIPI: 4, 4 | CHARACTER_TRUE: 3, 5 | CHARACTER_TOBBY: 2, 6 | CHARACTER_BUDDY: 1, 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "~/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | --------------------------------------------------------------------------------