From c8a0de16efefdf440651300515dbd49ad341e74c Mon Sep 17 00:00:00 2001 From: kuan Date: Tue, 24 Mar 2026 17:28:02 +0900 Subject: [PATCH 01/39] =?UTF-8?q?chore=20:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/[[...route]]/route.ts | 2 +- src/app/brand/[name]/page.tsx | 8 ++++---- src/app/community/post/[id]/edit/page.tsx | 6 +++--- src/app/community/post/[id]/page.tsx | 10 +++++----- src/app/login-test/page.tsx | 2 +- src/app/perfumes/[id]/page.tsx | 4 ++-- src/app/perfumes/page.tsx | 6 +++--- src/app/user/[id]/activity/page.tsx | 2 +- src/app/user/[id]/bookmarks/page.tsx | 2 +- src/app/user/[id]/layout.tsx | 10 +++++----- src/app/user/[id]/profile/page.tsx | 2 +- src/authConfig.ts | 2 +- .../ckeditor/finalizeWithBlobRegistry.ts | 6 +++--- .../ckeditor/localPreviewUploadPlugin.ts | 0 .../ckeditor/supabaseUploadPlugin.ts | 4 ++-- .../hooks/query/useBrandQuery.ts | 4 ++-- .../hooks/query/useCollectionQuery.ts | 2 +- .../hooks/query/useCommentQuery.ts | 4 ++-- .../hooks/query/useCommunityQuery.ts | 2 +- .../hooks/query/useDraftQuery.ts | 6 +++--- .../hooks/query/useFilterQuery.ts | 10 +++++----- .../hooks/query/usePerfumeQuery.ts | 2 +- .../hooks/query/useReviewQuery.ts | 0 .../hooks/query/useSearchQuery.ts | 6 +++--- .../hooks/query/useUserQuery.ts | 2 +- src/{lib => client}/hooks/useBeforeUnload.ts | 0 src/{lib => client}/hooks/useDebounce.ts | 0 .../hooks/useInfinitePerfumes.ts | 6 +++--- .../hooks/useInfiniteScrollTrigger.ts | 0 .../hooks/useIntersectionObserver.ts | 0 src/{lib => client}/hooks/useMediaQuery.ts | 0 src/{lib => client}/hooks/useOnClickOutside.ts | 0 src/{lib => client}/hooks/usePostAutosave.ts | 4 ++-- .../hooks/useRecentItemsSync.ts | 2 +- src/{lib => client}/hooks/useSearchBar.ts | 0 src/{lib => client}/portal/ModalPortal.tsx | 0 src/{lib => client}/script/KakaoScript.tsx | 0 .../stores/createRecentItemsStore.ts | 0 src/{lib => client}/stores/useCountStore.ts | 0 src/{lib => client}/stores/useFilterStore.ts | 0 src/{lib => client}/stores/useLogRecentItem.ts | 0 src/{lib => client}/stores/useModalStore.ts | 0 .../stores/useRecentPerfumesStore.ts | 0 .../stores/useRecentPostsStore.ts | 2 +- src/{lib => client}/stores/useUserStore.ts | 2 +- .../stores/useVisibilityStore.ts | 0 src/{lib => client}/utils/Compress.ts | 0 src/{lib => client}/utils/api/brands.api.ts | 4 ++-- src/{lib => client}/utils/api/client.ts | 2 +- .../utils/api/collections.api.ts | 4 ++-- src/{lib => client}/utils/api/comment.api.ts | 4 ++-- src/{lib => client}/utils/api/community.api.ts | 4 ++-- src/{lib => client}/utils/api/draft.api.ts | 4 ++-- src/{lib => client}/utils/api/files.api.ts | 4 ++-- src/{lib => client}/utils/api/filter.api.ts | 4 ++-- src/{lib => client}/utils/api/perfumes.api.ts | 4 ++-- src/{lib => client}/utils/api/points.api.ts | 4 ++-- src/{lib => client}/utils/api/reviews.api.ts | 4 ++-- src/{lib => client}/utils/api/search.api.ts | 6 +++--- src/{lib => client}/utils/api/users.api.ts | 12 ++++++------ .../utils/core-request/README.md | 0 .../utils/core-request/config.ts | 0 src/{lib => client}/utils/core-request/core.ts | 0 .../utils/core-request/httpClient.ts | 0 .../utils/core-request/index.ts | 0 .../utils/core-request/serverClient.ts | 0 .../utils/core-request/token.ts | 0 src/{lib => client}/utils/getQueryClient.ts | 0 src/{lib => client}/utils/queryKeys.ts | 6 +++--- src/{lib => client}/utils/share/kakaoShare.ts | 0 src/{lib => client}/utils/share/linkShare.ts | 0 .../commons/Provider/GlobalStateSync.tsx | 6 +++--- .../commons/Provider/RecentSyncManager.tsx | 8 ++++---- src/components/commons/author/PostMeta.tsx | 2 +- .../commons/author/author.constants.ts | 2 +- src/components/commons/author/author.types.ts | 2 +- .../bottomNavBar/BottomNavBar.constants.tsx | 2 +- src/components/commons/bottomNavBar/index.tsx | 2 +- src/components/commons/button/ButtonBase.tsx | 2 +- .../commons/card/perfumeCard/index.tsx | 2 +- .../commons/card/postCard/Thumbnail.tsx | 2 +- src/components/commons/card/postCard/index.tsx | 2 +- .../commons/card/postCard/postCard.types.ts | 4 ++-- .../commons/card/reviewCard/index.tsx | 2 +- .../card/reviewCard/reviewCard.types.ts | 2 +- src/components/commons/chip/AccordChip.tsx | 2 +- src/components/commons/chip/BoardChip.tsx | 4 ++-- src/components/commons/chip/ChipBase.tsx | 2 +- src/components/commons/chip/stories.tsx | 4 ++-- src/components/commons/ckeditor5/CkEditor5.tsx | 2 +- .../commons/dropdown/BoardDropdown.tsx | 2 +- .../commons/dropdown/DropdownBase.tsx | 6 +++--- .../commons/dropdown/DropdownMenu.tsx | 2 +- .../commons/dropdown/ShareDropdownMenu.tsx | 6 +++--- .../commons/dropdown/SortDropdown.tsx | 2 +- src/components/commons/navBar/Dropdown.tsx | 4 ++-- .../commons/navBar/UserProfileSection.tsx | 6 +++--- .../commons/navBar/navBar.constants.ts | 2 +- .../commons/perfumeList/filter/FilterItem.tsx | 8 ++++---- .../commons/perfumeList/filter/FilterList.tsx | 8 ++++---- .../perfumeList/filter/filter.helper.ts | 2 +- .../commons/perfumeList/filter/index.tsx | 4 ++-- .../commons/perfumeList/modal/ItemModal.tsx | 8 ++++---- .../commons/perfumeList/modal/Layout.tsx | 4 ++-- .../commons/perfumeList/modal/ListModal.tsx | 4 ++-- .../commons/perfumeList/perfumes.helpers.ts | 2 +- .../commons/perfumeList/search/index.tsx | 4 ++-- .../perfumeList/section/BrandSection.tsx | 2 +- .../perfumeList/section/PerfumeSection.tsx | 2 +- src/components/commons/search/SearchBar.tsx | 2 +- src/components/commons/wrapper/PageWrapper.tsx | 2 +- .../domains/brandDetail/image/index.tsx | 2 +- .../domains/brandDetail/info/index.tsx | 2 +- .../brandDetail/perfumes/PageClient.tsx | 6 +++--- .../domains/community/PageClient.tsx | 8 ++++---- .../domains/community/community.helpers.ts | 4 ++-- .../domains/community/communityCards/index.tsx | 4 ++-- .../domains/community/header/WriteButton.tsx | 4 ++-- .../domains/community/header/index.tsx | 2 +- src/components/domains/main/MainLogo.tsx | 2 +- src/components/domains/main/MainSearchBar.tsx | 12 ++++++------ .../domains/main/mainBanner/MainBanner.tsx | 2 +- .../main/mainBanner/MainBannerPerfumeList.tsx | 2 +- .../MainContentButtons/MainContentButtons.tsx | 2 +- .../MainContentButtonsSwiper.tsx | 2 +- .../main/mainContent/MainContentReview.tsx | 4 ++-- .../MainContentCommunity.tsx | 2 +- .../MainContentCommunityList.tsx | 8 ++++---- .../domains/perfumeDetail/DetailClient.tsx | 8 ++++---- .../domains/perfumeDetail/overview/index.tsx | 2 +- .../overview/perfumeInfo/header/index.tsx | 2 +- .../overview/perfumeInfo/index.tsx | 4 ++-- .../analytics/ReviewMobileSection/index.tsx | 2 +- .../review/analytics/analytics.constants.ts | 4 ++-- .../review/analytics/analytics.helpers.ts | 4 ++-- .../perfumeDetail/review/analytics/index.tsx | 2 +- .../perfumeDetail/review/review.type.ts | 2 +- .../perfumeDetail/review/reviewList/index.tsx | 2 +- .../domains/perfumeDetail/sidebar/index.tsx | 4 ++-- src/components/domains/perfumes/PageClient.tsx | 8 ++++---- .../perfumes/hook/usePerfumeSearchState.ts | 2 +- src/components/domains/post/PageClient.tsx | 6 +++--- .../domains/post/form/PostEditor.tsx | 2 +- src/components/domains/post/form/index.tsx | 18 +++++++++--------- .../postRelatedPerfume/PerfumeResultItem.tsx | 4 ++-- .../postRelatedPerfume/PostRelatedPerfume.tsx | 10 +++++----- .../postRelatedPerfume/usePerfumeSearch.ts | 2 +- src/components/domains/post/form/postSchema.ts | 2 +- .../domains/postDetail/PageClient.tsx | 6 +++--- .../CategoryPostList.tsx | 2 +- .../categoryPostListSection/Header.tsx | 6 +++--- .../PostListMobileSwiper.tsx | 2 +- .../categoryPostListSection/index.tsx | 2 +- .../commentSection/comment/CommentForm.tsx | 2 +- .../commentSection/comment/CommentListItem.tsx | 6 +++--- .../commentSection/comment/ReplyIcon.tsx | 2 +- .../comment/postComment.types.ts | 2 +- .../postDetail/commentSection/index.tsx | 2 +- .../postDetail/content/RelatedPerfume.tsx | 2 +- .../domains/postDetail/content/index.tsx | 2 +- .../domains/postDetail/header/PostActions.tsx | 4 ++-- .../postDetail/header/PostInteractions.tsx | 4 ++-- .../domains/postDetail/header/index.tsx | 2 +- .../header/usePostInteractionMutation.ts | 4 ++-- .../domains/postDetail/postDetail.helpers.ts | 6 +++--- src/components/domains/user/Reply.tsx | 2 +- .../domains/user/UserLayoutClient.tsx | 2 +- .../user/components/ScrollRowSection.tsx | 2 +- .../components/LikePerfumeList.tsx | 2 +- .../components/LikePostList.tsx | 4 ++-- .../components/MyCommentList.tsx | 2 +- .../ActivitySection/components/MyPostList.tsx | 4 ++-- .../components/MyReviewList.tsx | 2 +- .../user/sections/ActivitySection/index.tsx | 2 +- .../loader/LikePerfumeListLoader.tsx | 2 +- .../loader/LikePostListLoader.tsx | 2 +- .../loader/MyCommentsListLoader.tsx | 2 +- .../loader/MyPostListLoader.tsx | 2 +- .../loader/MyReviewListLoader.tsx | 2 +- .../components/CommunityBookmarkList.tsx | 4 ++-- .../components/PerfumeBookmarkList.tsx | 2 +- .../user/sections/BookmarkSection/index.tsx | 2 +- .../loader/CommunityBookmarksLoader.tsx | 2 +- .../loader/PerfumeBookmarksLoader.tsx | 2 +- .../user/sections/CollectionSection/index.tsx | 4 ++-- .../components/UserHeaderDisplay.tsx | 2 +- .../components/UserInfo.tsx | 2 +- .../sections/ProfileHeaderSection/index.tsx | 4 ++-- .../components/PersonalInfo/field/AgeField.tsx | 2 +- .../PersonalInfo/field/GenderField.tsx | 2 +- .../PersonalInfo/field/NicknameField.tsx | 2 +- .../components/PersonalInfo/index.tsx | 4 ++-- .../components/ProfileImage/ImageDisplay.tsx | 2 +- .../user/sections/ProfileSection/index.tsx | 4 ++-- .../hooks/useRecentPerfumes.ts | 2 +- .../RecentViewSection/hooks/useRecentPosts.ts | 4 ++-- .../user/sections/RecentViewSection/index.tsx | 2 +- .../domains/user/sections/sections.type.ts | 8 ++++---- src/components/domains/user/tabs/MainTabs.tsx | 2 +- .../domains/user/tabs/MobileTabsAccordion.tsx | 4 ++-- src/components/modal/LoginModal.tsx | 4 ++-- src/components/modal/LoginModalProvider.tsx | 4 ++-- src/components/modal/ModalContainer.tsx | 2 +- .../modal/draftRestoreModal/index.tsx | 4 ++-- .../PerfumeTagger.tsx | 2 +- .../modal/photocollectionUploadModal/index.tsx | 8 ++++---- .../photocollectionUploadModal/useSearchTag.ts | 4 ++-- .../components/button/ImageButton.tsx | 2 +- .../components/button/RadioButton.tsx | 2 +- .../components/button/SelectButton.tsx | 2 +- .../components/sections/FeelingSection.tsx | 2 +- .../components/sections/GenderToneSection.tsx | 2 +- .../components/sections/LongevitySection.tsx | 2 +- .../components/sections/PriceSection.tsx | 2 +- .../components/sections/SeasonalSection.tsx | 2 +- .../components/sections/SillageSection.tsx | 2 +- .../components/sections/StatusSection.tsx | 2 +- .../components/sections/TimeSection.tsx | 2 +- src/components/modal/reviewModal/index.tsx | 2 +- .../modal/reviewModal/useReviewModal.ts | 2 +- .../modal/reviewModal/useReviewSubmit.ts | 4 ++-- src/{lib => server}/database/action/login.ts | 0 src/{lib => server}/database/getSession.ts | 0 src/{lib => server}/hono/app.ts | 0 .../hono/middleware/auth.middleware.ts | 0 .../hono/routes/v1/brand.handler.ts | 10 +++++----- .../hono/routes/v1/comment.handler.ts | 12 ++++++------ .../hono/routes/v1/community.handler.ts | 12 ++++++------ .../hono/routes/v1/draft.handler.ts | 12 ++++++------ .../hono/routes/v1/file.handler.ts | 12 ++++++------ .../hono/routes/v1/filter.handler.ts | 10 +++++----- .../hono/routes/v1/me.handler.ts | 12 ++++++------ .../hono/routes/v1/perfume.handler.ts | 16 ++++++++-------- .../hono/routes/v1/point.handler.ts | 12 ++++++------ .../hono/routes/v1/review.handler.ts | 14 +++++++------- .../hono/routes/v1/search.handler.ts | 10 +++++----- .../hono/routes/v1/user.handler.ts | 14 +++++++------- .../hono/schemas/brand.schema.ts | 0 .../hono/schemas/comment.schema.ts | 0 .../hono/schemas/common.schema.ts | 0 .../hono/schemas/community.schema.ts | 0 .../hono/schemas/draft.schema.ts | 0 .../hono/schemas/filter.schema.ts | 2 +- src/{lib => server}/hono/schemas/me.schema.ts | 0 .../hono/schemas/perfume.schema.ts | 0 .../hono/schemas/point.schema.ts | 0 .../hono/schemas/review.schema.ts | 0 .../hono/schemas/search.schema.ts | 0 .../hono/schemas/user.schema.ts | 0 .../services/__tests__/brand.service.test.ts | 4 ++-- .../services/__tests__/comment.service.test.ts | 4 ++-- .../__tests__/community.service.test.ts | 4 ++-- .../__tests__/consecutiveLogin.service.test.ts | 4 ++-- .../__tests__/earnPoints.service.test.ts | 4 ++-- .../services/__tests__/file.service.test.ts | 2 +- .../services/__tests__/filter.service.test.ts | 4 ++-- .../__tests__/helpers/comment.test.helpers.ts | 0 .../helpers/community.test.helpers.ts | 0 .../__tests__/helpers/filter.test.helpers.ts | 0 .../__tests__/helpers/me.test.helpers.ts | 0 .../__tests__/helpers/perfume.test.helpers.ts | 0 .../__tests__/helpers/search.test.helpers.ts | 0 .../__tests__/helpers/user.test.helpers.ts | 0 .../services/__tests__/level.utils.test.ts | 2 +- .../hono/services/__tests__/me.service.test.ts | 4 ++-- .../services/__tests__/perfume.service.test.ts | 4 ++-- .../services/__tests__/point.service.test.ts | 0 .../services/__tests__/review.service.test.ts | 4 ++-- .../services/__tests__/search.service.test.ts | 4 ++-- .../services/__tests__/user.service.test.ts | 4 ++-- .../hono/services/brand.service.ts | 2 +- .../hono/services/comment.service.ts | 2 +- .../hono/services/community.service.ts | 2 +- .../hono/services/draft.service.ts | 17 ++++------------- .../hono/services/file.service.ts | 2 +- .../hono/services/filter.service.ts | 2 +- .../hono/services/me.service.ts | 6 +++--- .../hono/services/perfume.service.ts | 2 +- .../hono/services/point.service.ts | 6 +++--- .../hono/services/review.service.ts | 2 +- .../hono/services/search.service.ts | 2 +- .../hono/services/user.service.ts | 4 ++-- .../hono/utils/__tests__/level.utils.test.ts | 2 +- src/{lib => server}/hono/utils/api.utils.ts | 0 .../hono/utils/http.constants.ts | 0 .../hono/utils/openapi.schema.ts | 2 +- .../hono/utils/pagination.utils.ts | 0 src/{lib => server}/hono/utils/prisma.utils.ts | 0 src/{lib => server}/hono/utils/router.ts | 0 .../hono/utils/sanitize.utils.ts | 0 .../hono/utils/service.utils.ts | 2 +- src/{lib => server}/prisma/index.ts | 0 src/{lib => server}/supabase/init.ts | 0 src/{lib => server}/supabase/server.ts | 0 src/{lib => shared}/constants/accords.ts | 0 src/{lib => shared}/constants/buckets.ts | 0 src/{lib => shared}/constants/colors.ts | 0 .../constants/communityBoard.ts | 0 src/{lib => shared}/constants/icons/index.ts | 0 src/{lib => shared}/constants/icons/navBar.ts | 0 src/{lib => shared}/constants/images.ts | 0 src/{lib => shared}/constants/main.ts | 2 +- src/{lib => shared}/constants/options.ts | 0 src/{lib => shared}/constants/point.ts | 0 src/{lib => shared}/constants/post.ts | 0 src/{lib => shared}/constants/review.ts | 2 +- src/{lib => shared}/mocks/communityCard.ts | 2 +- src/{lib => shared}/mocks/communityPosts.ts | 0 src/{lib => shared}/mocks/fetchUser.ts | 0 src/{lib => shared}/mocks/perfumeCard.ts | 0 src/{lib => shared}/mocks/postCard.ts | 0 src/{lib => shared}/mocks/reply.ts | 0 src/{lib => shared}/mocks/reviewCard.ts | 0 src/{lib => shared}/types/perfumeDetail.d.ts | 0 src/{lib => shared}/types/perfumeReview.d.ts | 0 src/{lib => shared}/types/point.types.ts | 0 src/{lib => shared}/types/postDraft.ts | 0 src/{lib => shared}/types/review.types.ts | 2 +- src/{lib => shared}/types/search.types.ts | 0 .../types/swagger-ui-react.d.ts | 0 src/{lib => shared}/utils/cn.ts | 0 .../utils/extractFirstImageSrc.ts | 0 src/{lib => shared}/utils/generateSeo.ts | 0 src/{lib => shared}/utils/getPlainText.ts | 0 .../utils/getUserSessionInfo.ts | 2 +- src/{lib => shared}/utils/highlightText.tsx | 0 src/{lib => shared}/utils/level.utils.ts | 0 src/{lib => shared}/utils/postDraft.ts | 6 +++--- src/{lib => shared}/utils/review.helpers.ts | 0 src/{lib => shared}/utils/stableStringify.ts | 0 330 files changed, 473 insertions(+), 482 deletions(-) rename src/{lib => client}/ckeditor/finalizeWithBlobRegistry.ts (77%) rename src/{lib => client}/ckeditor/localPreviewUploadPlugin.ts (100%) rename src/{lib => client}/ckeditor/supabaseUploadPlugin.ts (88%) rename src/{lib => client}/hooks/query/useBrandQuery.ts (88%) rename src/{lib => client}/hooks/query/useCollectionQuery.ts (97%) rename src/{lib => client}/hooks/query/useCommentQuery.ts (97%) rename src/{lib => client}/hooks/query/useCommunityQuery.ts (98%) rename src/{lib => client}/hooks/query/useDraftQuery.ts (96%) rename src/{lib => client}/hooks/query/useFilterQuery.ts (87%) rename src/{lib => client}/hooks/query/usePerfumeQuery.ts (96%) rename src/{lib => client}/hooks/query/useReviewQuery.ts (100%) rename src/{lib => client}/hooks/query/useSearchQuery.ts (91%) rename src/{lib => client}/hooks/query/useUserQuery.ts (98%) rename src/{lib => client}/hooks/useBeforeUnload.ts (100%) rename src/{lib => client}/hooks/useDebounce.ts (100%) rename src/{lib => client}/hooks/useInfinitePerfumes.ts (77%) rename src/{lib => client}/hooks/useInfiniteScrollTrigger.ts (100%) rename src/{lib => client}/hooks/useIntersectionObserver.ts (100%) rename src/{lib => client}/hooks/useMediaQuery.ts (100%) rename src/{lib => client}/hooks/useOnClickOutside.ts (100%) rename src/{lib => client}/hooks/usePostAutosave.ts (92%) rename src/{lib => client}/hooks/useRecentItemsSync.ts (99%) rename src/{lib => client}/hooks/useSearchBar.ts (100%) rename src/{lib => client}/portal/ModalPortal.tsx (100%) rename src/{lib => client}/script/KakaoScript.tsx (100%) rename src/{lib => client}/stores/createRecentItemsStore.ts (100%) rename src/{lib => client}/stores/useCountStore.ts (100%) rename src/{lib => client}/stores/useFilterStore.ts (100%) rename src/{lib => client}/stores/useLogRecentItem.ts (100%) rename src/{lib => client}/stores/useModalStore.ts (100%) rename src/{lib => client}/stores/useRecentPerfumesStore.ts (100%) rename src/{lib => client}/stores/useRecentPostsStore.ts (82%) rename src/{lib => client}/stores/useUserStore.ts (84%) rename src/{lib => client}/stores/useVisibilityStore.ts (100%) rename src/{lib => client}/utils/Compress.ts (100%) rename src/{lib => client}/utils/api/brands.api.ts (85%) rename src/{lib => client}/utils/api/client.ts (78%) rename src/{lib => client}/utils/api/collections.api.ts (90%) rename src/{lib => client}/utils/api/comment.api.ts (91%) rename src/{lib => client}/utils/api/community.api.ts (95%) rename src/{lib => client}/utils/api/draft.api.ts (92%) rename src/{lib => client}/utils/api/files.api.ts (96%) rename src/{lib => client}/utils/api/filter.api.ts (86%) rename src/{lib => client}/utils/api/perfumes.api.ts (90%) rename src/{lib => client}/utils/api/points.api.ts (91%) rename src/{lib => client}/utils/api/reviews.api.ts (84%) rename src/{lib => client}/utils/api/search.api.ts (93%) rename src/{lib => client}/utils/api/users.api.ts (93%) rename src/{lib => client}/utils/core-request/README.md (100%) rename src/{lib => client}/utils/core-request/config.ts (100%) rename src/{lib => client}/utils/core-request/core.ts (100%) rename src/{lib => client}/utils/core-request/httpClient.ts (100%) rename src/{lib => client}/utils/core-request/index.ts (100%) rename src/{lib => client}/utils/core-request/serverClient.ts (100%) rename src/{lib => client}/utils/core-request/token.ts (100%) rename src/{lib => client}/utils/getQueryClient.ts (100%) rename src/{lib => client}/utils/queryKeys.ts (96%) rename src/{lib => client}/utils/share/kakaoShare.ts (100%) rename src/{lib => client}/utils/share/linkShare.ts (100%) rename src/{lib => server}/database/action/login.ts (100%) rename src/{lib => server}/database/getSession.ts (100%) rename src/{lib => server}/hono/app.ts (100%) rename src/{lib => server}/hono/middleware/auth.middleware.ts (100%) rename src/{lib => server}/hono/routes/v1/brand.handler.ts (89%) rename src/{lib => server}/hono/routes/v1/comment.handler.ts (94%) rename src/{lib => server}/hono/routes/v1/community.handler.ts (96%) rename src/{lib => server}/hono/routes/v1/draft.handler.ts (92%) rename src/{lib => server}/hono/routes/v1/file.handler.ts (79%) rename src/{lib => server}/hono/routes/v1/filter.handler.ts (84%) rename src/{lib => server}/hono/routes/v1/me.handler.ts (97%) rename src/{lib => server}/hono/routes/v1/perfume.handler.ts (93%) rename src/{lib => server}/hono/routes/v1/point.handler.ts (92%) rename src/{lib => server}/hono/routes/v1/review.handler.ts (92%) rename src/{lib => server}/hono/routes/v1/search.handler.ts (83%) rename src/{lib => server}/hono/routes/v1/user.handler.ts (86%) rename src/{lib => server}/hono/schemas/brand.schema.ts (100%) rename src/{lib => server}/hono/schemas/comment.schema.ts (100%) rename src/{lib => server}/hono/schemas/common.schema.ts (100%) rename src/{lib => server}/hono/schemas/community.schema.ts (100%) rename src/{lib => server}/hono/schemas/draft.schema.ts (100%) rename src/{lib => server}/hono/schemas/filter.schema.ts (96%) rename src/{lib => server}/hono/schemas/me.schema.ts (100%) rename src/{lib => server}/hono/schemas/perfume.schema.ts (100%) rename src/{lib => server}/hono/schemas/point.schema.ts (100%) rename src/{lib => server}/hono/schemas/review.schema.ts (100%) rename src/{lib => server}/hono/schemas/search.schema.ts (100%) rename src/{lib => server}/hono/schemas/user.schema.ts (100%) rename src/{lib => server}/hono/services/__tests__/brand.service.test.ts (98%) rename src/{lib => server}/hono/services/__tests__/comment.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/community.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/consecutiveLogin.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/earnPoints.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/file.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/filter.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/helpers/comment.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/helpers/community.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/helpers/filter.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/helpers/me.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/helpers/perfume.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/helpers/search.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/helpers/user.test.helpers.ts (100%) rename src/{lib => server}/hono/services/__tests__/level.utils.test.ts (98%) rename src/{lib => server}/hono/services/__tests__/me.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/perfume.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/point.service.test.ts (100%) rename src/{lib => server}/hono/services/__tests__/review.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/search.service.test.ts (99%) rename src/{lib => server}/hono/services/__tests__/user.service.test.ts (99%) rename src/{lib => server}/hono/services/brand.service.ts (98%) rename src/{lib => server}/hono/services/comment.service.ts (99%) rename src/{lib => server}/hono/services/community.service.ts (99%) rename src/{lib => server}/hono/services/draft.service.ts (94%) rename src/{lib => server}/hono/services/file.service.ts (98%) rename src/{lib => server}/hono/services/filter.service.ts (98%) rename src/{lib => server}/hono/services/me.service.ts (98%) rename src/{lib => server}/hono/services/perfume.service.ts (99%) rename src/{lib => server}/hono/services/point.service.ts (98%) rename src/{lib => server}/hono/services/review.service.ts (99%) rename src/{lib => server}/hono/services/search.service.ts (98%) rename src/{lib => server}/hono/services/user.service.ts (96%) rename src/{lib => server}/hono/utils/__tests__/level.utils.test.ts (98%) rename src/{lib => server}/hono/utils/api.utils.ts (100%) rename src/{lib => server}/hono/utils/http.constants.ts (100%) rename src/{lib => server}/hono/utils/openapi.schema.ts (95%) rename src/{lib => server}/hono/utils/pagination.utils.ts (100%) rename src/{lib => server}/hono/utils/prisma.utils.ts (100%) rename src/{lib => server}/hono/utils/router.ts (100%) rename src/{lib => server}/hono/utils/sanitize.utils.ts (100%) rename src/{lib => server}/hono/utils/service.utils.ts (99%) rename src/{lib => server}/prisma/index.ts (100%) rename src/{lib => server}/supabase/init.ts (100%) rename src/{lib => server}/supabase/server.ts (100%) rename src/{lib => shared}/constants/accords.ts (100%) rename src/{lib => shared}/constants/buckets.ts (100%) rename src/{lib => shared}/constants/colors.ts (100%) rename src/{lib => shared}/constants/communityBoard.ts (100%) rename src/{lib => shared}/constants/icons/index.ts (100%) rename src/{lib => shared}/constants/icons/navBar.ts (100%) rename src/{lib => shared}/constants/images.ts (100%) rename src/{lib => shared}/constants/main.ts (92%) rename src/{lib => shared}/constants/options.ts (100%) rename src/{lib => shared}/constants/point.ts (100%) rename src/{lib => shared}/constants/post.ts (100%) rename src/{lib => shared}/constants/review.ts (99%) rename src/{lib => shared}/mocks/communityCard.ts (99%) rename src/{lib => shared}/mocks/communityPosts.ts (100%) rename src/{lib => shared}/mocks/fetchUser.ts (100%) rename src/{lib => shared}/mocks/perfumeCard.ts (100%) rename src/{lib => shared}/mocks/postCard.ts (100%) rename src/{lib => shared}/mocks/reply.ts (100%) rename src/{lib => shared}/mocks/reviewCard.ts (100%) rename src/{lib => shared}/types/perfumeDetail.d.ts (100%) rename src/{lib => shared}/types/perfumeReview.d.ts (100%) rename src/{lib => shared}/types/point.types.ts (100%) rename src/{lib => shared}/types/postDraft.ts (100%) rename src/{lib => shared}/types/review.types.ts (79%) rename src/{lib => shared}/types/search.types.ts (100%) rename src/{lib => shared}/types/swagger-ui-react.d.ts (100%) rename src/{lib => shared}/utils/cn.ts (100%) rename src/{lib => shared}/utils/extractFirstImageSrc.ts (100%) rename src/{lib => shared}/utils/generateSeo.ts (100%) rename src/{lib => shared}/utils/getPlainText.ts (100%) rename src/{lib => shared}/utils/getUserSessionInfo.ts (75%) rename src/{lib => shared}/utils/highlightText.tsx (100%) rename src/{lib => shared}/utils/level.utils.ts (100%) rename src/{lib => shared}/utils/postDraft.ts (86%) rename src/{lib => shared}/utils/review.helpers.ts (100%) rename src/{lib => shared}/utils/stableStringify.ts (100%) diff --git a/src/app/api/[[...route]]/route.ts b/src/app/api/[[...route]]/route.ts index 020c8650..2d8ca568 100644 --- a/src/app/api/[[...route]]/route.ts +++ b/src/app/api/[[...route]]/route.ts @@ -1,4 +1,4 @@ -import app from "@/lib/hono/app"; +import app from "@/server/hono/app"; import { handle } from "hono/vercel"; // export const runtime = "edge"; diff --git a/src/app/brand/[name]/page.tsx b/src/app/brand/[name]/page.tsx index 0b9fdf17..abd53069 100644 --- a/src/app/brand/[name]/page.tsx +++ b/src/app/brand/[name]/page.tsx @@ -1,16 +1,16 @@ import { cache } from "react"; import type { Metadata } from "next"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; -import getQueryClient from "@/lib/utils/getQueryClient"; -import { brandApi } from "@/lib/utils/api/brands.api"; -import { perfumeApi } from "@/lib/utils/api/perfumes.api"; +import getQueryClient from "@/client/utils/getQueryClient"; +import { brandApi } from "@/client/utils/api/brands.api"; +import { perfumeApi } from "@/client/utils/api/perfumes.api"; import { BrandDetailImage } from "@/components/domains/brandDetail/image"; import { BrandDetailInfo } from "@/components/domains/brandDetail/info"; import { PageClient } from "@/components/domains/brandDetail/perfumes/PageClient"; import { BrandDetailSearchBar } from "@/components/domains/brandDetail/searchBar"; import { notFound } from "next/navigation"; import { BrandMap } from "@/components/domains/brandDetail/map"; -import { generateSeo } from "@/lib/utils/generateSeo"; +import { generateSeo } from "@/shared/utils/generateSeo"; type Props = { params: Promise<{ name: string }>; diff --git a/src/app/community/post/[id]/edit/page.tsx b/src/app/community/post/[id]/edit/page.tsx index ea6f8a55..4839c58c 100644 --- a/src/app/community/post/[id]/edit/page.tsx +++ b/src/app/community/post/[id]/edit/page.tsx @@ -1,7 +1,7 @@ import PageClient from "@/components/domains/post/PageClient"; -import { communityApi } from "@/lib/utils/api/community.api"; -import getQueryClient from "@/lib/utils/getQueryClient"; -import { queryKeys } from "@/lib/utils/queryKeys"; +import { communityApi } from "@/client/utils/api/community.api"; +import getQueryClient from "@/client/utils/getQueryClient"; +import { queryKeys } from "@/client/utils/queryKeys"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; diff --git a/src/app/community/post/[id]/page.tsx b/src/app/community/post/[id]/page.tsx index a438640b..ce175386 100644 --- a/src/app/community/post/[id]/page.tsx +++ b/src/app/community/post/[id]/page.tsx @@ -1,12 +1,12 @@ import { cookies } from "next/headers"; import PageClient from "@/components/domains/postDetail/PageClient"; -import getQueryClient from "@/lib/utils/getQueryClient"; +import getQueryClient from "@/client/utils/getQueryClient"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; -import { queryKeys } from "@/lib/utils/queryKeys"; -import { commentApi } from "@/lib/utils/api/comment.api"; -import { communityApi } from "@/lib/utils/api/community.api"; +import { queryKeys } from "@/client/utils/queryKeys"; +import { commentApi } from "@/client/utils/api/comment.api"; +import { communityApi } from "@/client/utils/api/community.api"; import type { Metadata } from "next"; -import { generateSeo } from "@/lib/utils/generateSeo"; +import { generateSeo } from "@/shared/utils/generateSeo"; type Props = { params: Promise<{ id: string }>; }; diff --git a/src/app/login-test/page.tsx b/src/app/login-test/page.tsx index 07c70eda..260b51d6 100644 --- a/src/app/login-test/page.tsx +++ b/src/app/login-test/page.tsx @@ -1,7 +1,7 @@ "use client"; import { LoginModal } from "@/components/modal/LoginModal"; -import { logout } from "@/lib/database/action/login"; +import { logout } from "@/server/database/action/login"; import { useState } from "react"; export default function LoginTestPage() { diff --git a/src/app/perfumes/[id]/page.tsx b/src/app/perfumes/[id]/page.tsx index 4c5c80e1..ba75dd7e 100644 --- a/src/app/perfumes/[id]/page.tsx +++ b/src/app/perfumes/[id]/page.tsx @@ -1,8 +1,8 @@ "use client"; import { DetailClient } from "@/components/domains/perfumeDetail/DetailClient"; -import { usePerfumeDetail } from "@/lib/hooks/query/usePerfumeQuery"; -import { usePerfumeReviews } from "@/lib/hooks/query/useReviewQuery"; +import { usePerfumeDetail } from "@/client/hooks/query/usePerfumeQuery"; +import { usePerfumeReviews } from "@/client/hooks/query/useReviewQuery"; import { useParams } from "next/navigation"; import { Spinner } from "@/components/commons/loading/Spinner"; diff --git a/src/app/perfumes/page.tsx b/src/app/perfumes/page.tsx index ca4b695d..32c97075 100644 --- a/src/app/perfumes/page.tsx +++ b/src/app/perfumes/page.tsx @@ -1,8 +1,8 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; import { Suspense } from "react"; -import getQueryClient from "@/lib/utils/getQueryClient"; -import { brandApi } from "@/lib/utils/api/brands.api"; -import { perfumeApi } from "@/lib/utils/api/perfumes.api"; +import getQueryClient from "@/client/utils/getQueryClient"; +import { brandApi } from "@/client/utils/api/brands.api"; +import { perfumeApi } from "@/client/utils/api/perfumes.api"; import PageClient from "@/components/domains/perfumes/PageClient"; import type { Metadata } from "next"; diff --git a/src/app/user/[id]/activity/page.tsx b/src/app/user/[id]/activity/page.tsx index e463a95c..509d70bc 100644 --- a/src/app/user/[id]/activity/page.tsx +++ b/src/app/user/[id]/activity/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { ActivitySection } from "@/components/domains/user/sections"; -import { getUserSessionInfo } from "@/lib/utils/getUserSessionInfo"; +import { getUserSessionInfo } from "@/shared/utils/getUserSessionInfo"; export default async function ActivityPage({ params, diff --git a/src/app/user/[id]/bookmarks/page.tsx b/src/app/user/[id]/bookmarks/page.tsx index 83357659..8932ed0d 100644 --- a/src/app/user/[id]/bookmarks/page.tsx +++ b/src/app/user/[id]/bookmarks/page.tsx @@ -1,5 +1,5 @@ import { BookmarkSection } from "@/components/domains/user/sections"; -import { getUserSessionInfo } from "@/lib/utils/getUserSessionInfo"; +import { getUserSessionInfo } from "@/shared/utils/getUserSessionInfo"; export default async function BookmarksPage({ params, diff --git a/src/app/user/[id]/layout.tsx b/src/app/user/[id]/layout.tsx index a5aaf5b9..d5df4321 100644 --- a/src/app/user/[id]/layout.tsx +++ b/src/app/user/[id]/layout.tsx @@ -1,10 +1,10 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; -import getQueryClient from "@/lib/utils/getQueryClient"; -import { userApi } from "@/lib/utils/api/users.api"; +import getQueryClient from "@/client/utils/getQueryClient"; +import { userApi } from "@/client/utils/api/users.api"; import UserLayoutClient from "@/components/domains/user/UserLayoutClient"; -import { getUserSessionInfo } from "@/lib/utils/getUserSessionInfo"; -import { queryKeys } from "@/lib/utils/queryKeys"; -import { generateSeo } from "@/lib/utils/generateSeo"; +import { getUserSessionInfo } from "@/shared/utils/getUserSessionInfo"; +import { queryKeys } from "@/client/utils/queryKeys"; +import { generateSeo } from "@/shared/utils/generateSeo"; import type { Metadata } from "next"; interface LayoutProps { diff --git a/src/app/user/[id]/profile/page.tsx b/src/app/user/[id]/profile/page.tsx index 7aea52e2..68b43eb8 100644 --- a/src/app/user/[id]/profile/page.tsx +++ b/src/app/user/[id]/profile/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; import { ProfileSection } from "@/components/domains/user/sections"; -import { getUserSessionInfo } from "@/lib/utils/getUserSessionInfo"; +import { getUserSessionInfo } from "@/shared/utils/getUserSessionInfo"; export default async function ProfilePage({ params, diff --git a/src/authConfig.ts b/src/authConfig.ts index cc326a5d..3572ea9a 100644 --- a/src/authConfig.ts +++ b/src/authConfig.ts @@ -1,6 +1,6 @@ import { Account, NextAuthConfig, User } from "next-auth"; import { v4 as uuidv4 } from "uuid"; -import { prisma } from "./lib/prisma"; +import { prisma } from "./server/prisma"; interface UserData { name: string; diff --git a/src/lib/ckeditor/finalizeWithBlobRegistry.ts b/src/client/ckeditor/finalizeWithBlobRegistry.ts similarity index 77% rename from src/lib/ckeditor/finalizeWithBlobRegistry.ts rename to src/client/ckeditor/finalizeWithBlobRegistry.ts index 3912b93d..3d99e1e6 100644 --- a/src/lib/ckeditor/finalizeWithBlobRegistry.ts +++ b/src/client/ckeditor/finalizeWithBlobRegistry.ts @@ -1,6 +1,6 @@ -import type { BlobRegistry } from "@/lib/ckeditor/localPreviewUploadPlugin"; -import { fileApi } from "@/lib/utils/api/files.api"; -import { POST_IMAGES_BUCKET_NAME } from "@/lib/constants/buckets"; +import type { BlobRegistry } from "@/client/ckeditor/localPreviewUploadPlugin"; +import { fileApi } from "@/client/utils/api/files.api"; +import { POST_IMAGES_BUCKET_NAME } from "@/shared/constants/buckets"; export async function finalizeWithBlobRegistry( html: string, diff --git a/src/lib/ckeditor/localPreviewUploadPlugin.ts b/src/client/ckeditor/localPreviewUploadPlugin.ts similarity index 100% rename from src/lib/ckeditor/localPreviewUploadPlugin.ts rename to src/client/ckeditor/localPreviewUploadPlugin.ts diff --git a/src/lib/ckeditor/supabaseUploadPlugin.ts b/src/client/ckeditor/supabaseUploadPlugin.ts similarity index 88% rename from src/lib/ckeditor/supabaseUploadPlugin.ts rename to src/client/ckeditor/supabaseUploadPlugin.ts index 7ff065bf..be598deb 100644 --- a/src/lib/ckeditor/supabaseUploadPlugin.ts +++ b/src/client/ckeditor/supabaseUploadPlugin.ts @@ -1,6 +1,6 @@ import { Editor, FileLoader } from "ckeditor5"; -import { fileApi } from "@/lib/utils/api/files.api"; -import { POST_IMAGES_BUCKET_NAME } from "@/lib/constants/buckets"; +import { fileApi } from "@/client/utils/api/files.api"; +import { POST_IMAGES_BUCKET_NAME } from "@/shared/constants/buckets"; class SupabaseUploadAdapter { loader: FileLoader; diff --git a/src/lib/hooks/query/useBrandQuery.ts b/src/client/hooks/query/useBrandQuery.ts similarity index 88% rename from src/lib/hooks/query/useBrandQuery.ts rename to src/client/hooks/query/useBrandQuery.ts index 7e37a608..b546c46a 100644 --- a/src/lib/hooks/query/useBrandQuery.ts +++ b/src/client/hooks/query/useBrandQuery.ts @@ -1,6 +1,6 @@ -import { brandApi } from "@/lib/utils/api/brands.api"; +import { brandApi } from "@/client/utils/api/brands.api"; import { useQuery } from "@tanstack/react-query"; -import { queryKeys } from "@/lib/utils/queryKeys"; +import { queryKeys } from "@/client/utils/queryKeys"; // 특정 브랜드 상세 조회 (ID) const useBrandDetail = (id: string) => { diff --git a/src/lib/hooks/query/useCollectionQuery.ts b/src/client/hooks/query/useCollectionQuery.ts similarity index 97% rename from src/lib/hooks/query/useCollectionQuery.ts rename to src/client/hooks/query/useCollectionQuery.ts index c084de30..5ec2df76 100644 --- a/src/lib/hooks/query/useCollectionQuery.ts +++ b/src/client/hooks/query/useCollectionQuery.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { collectionApi } from "../../utils/api/collections.api"; import { queryKeys } from "../../utils/queryKeys"; -import type { UploadedImageInfo } from "@/lib/hono/schemas/common.schema"; +import type { UploadedImageInfo } from "@/server/hono/schemas/common.schema"; interface CreateCollectionParams { perfumeId: string; diff --git a/src/lib/hooks/query/useCommentQuery.ts b/src/client/hooks/query/useCommentQuery.ts similarity index 97% rename from src/lib/hooks/query/useCommentQuery.ts rename to src/client/hooks/query/useCommentQuery.ts index 6fc26676..97aa500c 100644 --- a/src/lib/hooks/query/useCommentQuery.ts +++ b/src/client/hooks/query/useCommentQuery.ts @@ -13,8 +13,8 @@ import { GetCommentQuery, PaginatedCommentResponse, UpdateCommentBody, -} from "@/lib/hono/schemas/comment.schema"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +} from "@/server/hono/schemas/comment.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { commentApi } from "../../utils/api/comment.api"; import { queryKeys } from "../../utils/queryKeys"; diff --git a/src/lib/hooks/query/useCommunityQuery.ts b/src/client/hooks/query/useCommunityQuery.ts similarity index 98% rename from src/lib/hooks/query/useCommunityQuery.ts rename to src/client/hooks/query/useCommunityQuery.ts index 7cd185aa..be4f490e 100644 --- a/src/lib/hooks/query/useCommunityQuery.ts +++ b/src/client/hooks/query/useCommunityQuery.ts @@ -5,7 +5,7 @@ import { CreatePostInput, GetPostsQuery, UpdatePostInput, -} from "@/lib/hono/schemas/community.schema"; +} from "@/server/hono/schemas/community.schema"; import { communityApi } from "../../utils/api/community.api"; import { queryKeys } from "../../utils/queryKeys"; diff --git a/src/lib/hooks/query/useDraftQuery.ts b/src/client/hooks/query/useDraftQuery.ts similarity index 96% rename from src/lib/hooks/query/useDraftQuery.ts rename to src/client/hooks/query/useDraftQuery.ts index 6741263d..085d7904 100644 --- a/src/lib/hooks/query/useDraftQuery.ts +++ b/src/client/hooks/query/useDraftQuery.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { draftApi } from "@/lib/utils/api/draft.api"; -import { queryKeys } from "@/lib/utils/queryKeys"; -import { CreateDraftBody } from "@/lib/hono/schemas/draft.schema"; +import { draftApi } from "@/client/utils/api/draft.api"; +import { queryKeys } from "@/client/utils/queryKeys"; +import { CreateDraftBody } from "@/server/hono/schemas/draft.schema"; import { DraftType } from "@prisma/client"; /** diff --git a/src/lib/hooks/query/useFilterQuery.ts b/src/client/hooks/query/useFilterQuery.ts similarity index 87% rename from src/lib/hooks/query/useFilterQuery.ts rename to src/client/hooks/query/useFilterQuery.ts index 2e26691f..1900626c 100644 --- a/src/lib/hooks/query/useFilterQuery.ts +++ b/src/client/hooks/query/useFilterQuery.ts @@ -1,9 +1,9 @@ import { useQuery } from "@tanstack/react-query"; -import { brandApi } from "@/lib/utils/api/brands.api"; -import { perfumeApi } from "@/lib/utils/api/perfumes.api"; -import { filterApi } from "@/lib/utils/api/filter.api"; -import { queryKeys } from "@/lib/utils/queryKeys"; -import type { FilterRequestBody } from "@/lib/hono/schemas/filter.schema"; +import { brandApi } from "@/client/utils/api/brands.api"; +import { perfumeApi } from "@/client/utils/api/perfumes.api"; +import { filterApi } from "@/client/utils/api/filter.api"; +import { queryKeys } from "@/client/utils/queryKeys"; +import type { FilterRequestBody } from "@/server/hono/schemas/filter.schema"; const FILTER_STALE_TIME = { brands: 1000 * 60 * 30, diff --git a/src/lib/hooks/query/usePerfumeQuery.ts b/src/client/hooks/query/usePerfumeQuery.ts similarity index 96% rename from src/lib/hooks/query/usePerfumeQuery.ts rename to src/client/hooks/query/usePerfumeQuery.ts index a9b9972c..b69acb4b 100644 --- a/src/lib/hooks/query/usePerfumeQuery.ts +++ b/src/client/hooks/query/usePerfumeQuery.ts @@ -6,7 +6,7 @@ import { import { queryKeys } from "../../utils/queryKeys"; import { perfumeApi } from "../../utils/api/perfumes.api"; import { searchApi } from "../../utils/api/search.api"; -import { PaginatedSearchResponse } from "@/lib/hono/schemas/search.schema"; +import { PaginatedSearchResponse } from "@/server/hono/schemas/search.schema"; type BrandDetailQueryKey = ReturnType; diff --git a/src/lib/hooks/query/useReviewQuery.ts b/src/client/hooks/query/useReviewQuery.ts similarity index 100% rename from src/lib/hooks/query/useReviewQuery.ts rename to src/client/hooks/query/useReviewQuery.ts diff --git a/src/lib/hooks/query/useSearchQuery.ts b/src/client/hooks/query/useSearchQuery.ts similarity index 91% rename from src/lib/hooks/query/useSearchQuery.ts rename to src/client/hooks/query/useSearchQuery.ts index 115bdd6d..9a85e8f8 100644 --- a/src/lib/hooks/query/useSearchQuery.ts +++ b/src/client/hooks/query/useSearchQuery.ts @@ -4,12 +4,12 @@ import { InfiniteData, } from "@tanstack/react-query"; import { searchApi } from "../../utils/api/search.api"; -import { PaginatedSearchResponse } from "@/lib/hono/schemas/search.schema"; -import { queryKeys } from "@/lib/utils/queryKeys"; +import { PaginatedSearchResponse } from "@/server/hono/schemas/search.schema"; +import { queryKeys } from "@/client/utils/queryKeys"; import { SearchPerfumesParams, SearchPerfumesWithFiltersParams, -} from "@/lib/types/search.types"; +} from "@/shared/types/search.types"; // 향수 검색 (필터 없이) export const useSearchPerfumes = (params: SearchPerfumesParams) => { diff --git a/src/lib/hooks/query/useUserQuery.ts b/src/client/hooks/query/useUserQuery.ts similarity index 98% rename from src/lib/hooks/query/useUserQuery.ts rename to src/client/hooks/query/useUserQuery.ts index a237876c..f4bd0f9e 100644 --- a/src/lib/hooks/query/useUserQuery.ts +++ b/src/client/hooks/query/useUserQuery.ts @@ -6,7 +6,7 @@ import { } from "@tanstack/react-query"; import { meApi, userApi, USER_ID_REGEX } from "../../utils/api/users.api"; import { queryKeys } from "../../utils/queryKeys"; -import type { ApiUpdateMyProfileRequest } from "@/lib/hono/schemas/me.schema"; +import type { ApiUpdateMyProfileRequest } from "@/server/hono/schemas/me.schema"; // 내 프로필 조회 export const useUserProfile = (options?: { enabled?: boolean }) => { diff --git a/src/lib/hooks/useBeforeUnload.ts b/src/client/hooks/useBeforeUnload.ts similarity index 100% rename from src/lib/hooks/useBeforeUnload.ts rename to src/client/hooks/useBeforeUnload.ts diff --git a/src/lib/hooks/useDebounce.ts b/src/client/hooks/useDebounce.ts similarity index 100% rename from src/lib/hooks/useDebounce.ts rename to src/client/hooks/useDebounce.ts diff --git a/src/lib/hooks/useInfinitePerfumes.ts b/src/client/hooks/useInfinitePerfumes.ts similarity index 77% rename from src/lib/hooks/useInfinitePerfumes.ts rename to src/client/hooks/useInfinitePerfumes.ts index 9bbcfb41..d9e95b72 100644 --- a/src/lib/hooks/useInfinitePerfumes.ts +++ b/src/client/hooks/useInfinitePerfumes.ts @@ -1,9 +1,9 @@ "use client"; import { useEffect, useMemo } from "react"; -import { useFilterStore } from "@/lib/stores/useFilterStore"; -import { useTotalStore } from "@/lib/stores/useCountStore"; -import { useInfiniteSearchPerfumesQuery } from "@/lib/hooks/query/useSearchQuery"; +import { useFilterStore } from "@/client/stores/useFilterStore"; +import { useTotalStore } from "@/client/stores/useCountStore"; +import { useInfiniteSearchPerfumesQuery } from "@/client/hooks/query/useSearchQuery"; /** * 향수 검색 페이지용 커스텀 훅 diff --git a/src/lib/hooks/useInfiniteScrollTrigger.ts b/src/client/hooks/useInfiniteScrollTrigger.ts similarity index 100% rename from src/lib/hooks/useInfiniteScrollTrigger.ts rename to src/client/hooks/useInfiniteScrollTrigger.ts diff --git a/src/lib/hooks/useIntersectionObserver.ts b/src/client/hooks/useIntersectionObserver.ts similarity index 100% rename from src/lib/hooks/useIntersectionObserver.ts rename to src/client/hooks/useIntersectionObserver.ts diff --git a/src/lib/hooks/useMediaQuery.ts b/src/client/hooks/useMediaQuery.ts similarity index 100% rename from src/lib/hooks/useMediaQuery.ts rename to src/client/hooks/useMediaQuery.ts diff --git a/src/lib/hooks/useOnClickOutside.ts b/src/client/hooks/useOnClickOutside.ts similarity index 100% rename from src/lib/hooks/useOnClickOutside.ts rename to src/client/hooks/useOnClickOutside.ts diff --git a/src/lib/hooks/usePostAutosave.ts b/src/client/hooks/usePostAutosave.ts similarity index 92% rename from src/lib/hooks/usePostAutosave.ts rename to src/client/hooks/usePostAutosave.ts index 85ead482..f5b82828 100644 --- a/src/lib/hooks/usePostAutosave.ts +++ b/src/client/hooks/usePostAutosave.ts @@ -1,8 +1,8 @@ import { useRef, useCallback } from "react"; import { UseFormGetValues } from "react-hook-form"; import { CreatePostClientInput } from "@/components/domains/post/form/postSchema"; -import { PostDraftInput } from "@/lib/types/postDraft"; -import { saveDraft as saveDraftApi, isDraftValid } from "@/lib/utils/postDraft"; +import { PostDraftInput } from "@/shared/types/postDraft"; +import { saveDraft as saveDraftApi, isDraftValid } from "@/shared/utils/postDraft"; import { DraftType } from "@prisma/client"; interface UsePostAutosaveOptions { diff --git a/src/lib/hooks/useRecentItemsSync.ts b/src/client/hooks/useRecentItemsSync.ts similarity index 99% rename from src/lib/hooks/useRecentItemsSync.ts rename to src/client/hooks/useRecentItemsSync.ts index f52502a8..83ebb8f3 100644 --- a/src/lib/hooks/useRecentItemsSync.ts +++ b/src/client/hooks/useRecentItemsSync.ts @@ -9,7 +9,7 @@ import { ApiRecentPostItem, ApiGetRecentPerfumesResponseSchema, ApiGetRecentPostsResponseSchema, -} from "../hono/schemas/me.schema"; +} from "@/server/hono/schemas/me.schema"; import { meApi } from "../utils/api/users.api"; type RecentApiEndpoint = "recents/perfumes" | "recents/posts"; diff --git a/src/lib/hooks/useSearchBar.ts b/src/client/hooks/useSearchBar.ts similarity index 100% rename from src/lib/hooks/useSearchBar.ts rename to src/client/hooks/useSearchBar.ts diff --git a/src/lib/portal/ModalPortal.tsx b/src/client/portal/ModalPortal.tsx similarity index 100% rename from src/lib/portal/ModalPortal.tsx rename to src/client/portal/ModalPortal.tsx diff --git a/src/lib/script/KakaoScript.tsx b/src/client/script/KakaoScript.tsx similarity index 100% rename from src/lib/script/KakaoScript.tsx rename to src/client/script/KakaoScript.tsx diff --git a/src/lib/stores/createRecentItemsStore.ts b/src/client/stores/createRecentItemsStore.ts similarity index 100% rename from src/lib/stores/createRecentItemsStore.ts rename to src/client/stores/createRecentItemsStore.ts diff --git a/src/lib/stores/useCountStore.ts b/src/client/stores/useCountStore.ts similarity index 100% rename from src/lib/stores/useCountStore.ts rename to src/client/stores/useCountStore.ts diff --git a/src/lib/stores/useFilterStore.ts b/src/client/stores/useFilterStore.ts similarity index 100% rename from src/lib/stores/useFilterStore.ts rename to src/client/stores/useFilterStore.ts diff --git a/src/lib/stores/useLogRecentItem.ts b/src/client/stores/useLogRecentItem.ts similarity index 100% rename from src/lib/stores/useLogRecentItem.ts rename to src/client/stores/useLogRecentItem.ts diff --git a/src/lib/stores/useModalStore.ts b/src/client/stores/useModalStore.ts similarity index 100% rename from src/lib/stores/useModalStore.ts rename to src/client/stores/useModalStore.ts diff --git a/src/lib/stores/useRecentPerfumesStore.ts b/src/client/stores/useRecentPerfumesStore.ts similarity index 100% rename from src/lib/stores/useRecentPerfumesStore.ts rename to src/client/stores/useRecentPerfumesStore.ts diff --git a/src/lib/stores/useRecentPostsStore.ts b/src/client/stores/useRecentPostsStore.ts similarity index 82% rename from src/lib/stores/useRecentPostsStore.ts rename to src/client/stores/useRecentPostsStore.ts index cb7828f1..0f44a142 100644 --- a/src/lib/stores/useRecentPostsStore.ts +++ b/src/client/stores/useRecentPostsStore.ts @@ -1,6 +1,6 @@ import { createRecentItemsStore } from "./createRecentItemsStore"; import type { GenericRecentItem } from "./createRecentItemsStore"; -import type { ApiPostDetailResponse } from "@/lib/hono/schemas/community.schema"; +import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema"; export type RecentPostItem = GenericRecentItem; diff --git a/src/lib/stores/useUserStore.ts b/src/client/stores/useUserStore.ts similarity index 84% rename from src/lib/stores/useUserStore.ts rename to src/client/stores/useUserStore.ts index 039ff282..8173ee3a 100644 --- a/src/lib/stores/useUserStore.ts +++ b/src/client/stores/useUserStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { ApiMyProfileResponse } from "../hono/schemas/me.schema"; +import { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; interface UserState { user: ApiMyProfileResponse | null; diff --git a/src/lib/stores/useVisibilityStore.ts b/src/client/stores/useVisibilityStore.ts similarity index 100% rename from src/lib/stores/useVisibilityStore.ts rename to src/client/stores/useVisibilityStore.ts diff --git a/src/lib/utils/Compress.ts b/src/client/utils/Compress.ts similarity index 100% rename from src/lib/utils/Compress.ts rename to src/client/utils/Compress.ts diff --git a/src/lib/utils/api/brands.api.ts b/src/client/utils/api/brands.api.ts similarity index 85% rename from src/lib/utils/api/brands.api.ts rename to src/client/utils/api/brands.api.ts index 373cbe5a..fd368317 100644 --- a/src/lib/utils/api/brands.api.ts +++ b/src/client/utils/api/brands.api.ts @@ -1,9 +1,9 @@ import { apiClient } from "./client"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { ApiBrandDetailResponse, ApiBrandSimpleResponse, -} from "@/lib/hono/schemas/brand.schema"; +} from "@/server/hono/schemas/brand.schema"; export const brandApi = { /** diff --git a/src/lib/utils/api/client.ts b/src/client/utils/api/client.ts similarity index 78% rename from src/lib/utils/api/client.ts rename to src/client/utils/api/client.ts index 6b70f0f8..2c812d04 100644 --- a/src/lib/utils/api/client.ts +++ b/src/client/utils/api/client.ts @@ -1,4 +1,4 @@ -import { createHttpClient } from "@/lib/utils/core-request"; +import { createHttpClient } from "@/client/utils/core-request"; /** * 중앙 API 클라이언트 diff --git a/src/lib/utils/api/collections.api.ts b/src/client/utils/api/collections.api.ts similarity index 90% rename from src/lib/utils/api/collections.api.ts rename to src/client/utils/api/collections.api.ts index fe5cd478..46f31555 100644 --- a/src/lib/utils/api/collections.api.ts +++ b/src/client/utils/api/collections.api.ts @@ -1,9 +1,9 @@ -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import { ApiResponse, PaginatedResponse, UploadedImageInfo, -} from "@/lib/hono/schemas/common.schema"; +} from "@/server/hono/schemas/common.schema"; import { apiClient } from "./client"; type SearchResponse = PaginatedResponse; diff --git a/src/lib/utils/api/comment.api.ts b/src/client/utils/api/comment.api.ts similarity index 91% rename from src/lib/utils/api/comment.api.ts rename to src/client/utils/api/comment.api.ts index 0e735e3f..ec44d513 100644 --- a/src/lib/utils/api/comment.api.ts +++ b/src/client/utils/api/comment.api.ts @@ -1,5 +1,5 @@ import { apiClient } from "./client"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { CommentResponse, PaginatedCommentResponse, @@ -7,7 +7,7 @@ import { CreateCommentBody, UpdateCommentBody, DeleteCommentResponse, -} from "@/lib/hono/schemas/comment.schema"; +} from "@/server/hono/schemas/comment.schema"; export const COMMENT_URL = `/comments`; diff --git a/src/lib/utils/api/community.api.ts b/src/client/utils/api/community.api.ts similarity index 95% rename from src/lib/utils/api/community.api.ts rename to src/client/utils/api/community.api.ts index edfa515a..729e612f 100644 --- a/src/lib/utils/api/community.api.ts +++ b/src/client/utils/api/community.api.ts @@ -1,5 +1,5 @@ import { apiClient } from "./client"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { ApiPostResponse, ApiPostDetailResponse, @@ -9,7 +9,7 @@ import { GetPostsQuery, CreatePostInput, UpdatePostInput, -} from "@/lib/hono/schemas/community.schema"; +} from "@/server/hono/schemas/community.schema"; export const COMMUNITY_URL = `/community`; diff --git a/src/lib/utils/api/draft.api.ts b/src/client/utils/api/draft.api.ts similarity index 92% rename from src/lib/utils/api/draft.api.ts rename to src/client/utils/api/draft.api.ts index aafbe93c..8e7a54e2 100644 --- a/src/lib/utils/api/draft.api.ts +++ b/src/client/utils/api/draft.api.ts @@ -1,10 +1,10 @@ import { apiClient } from "./client"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { CreateDraftBody, ApiDraftResponse, ApiDraftListResponse, -} from "@/lib/hono/schemas/draft.schema"; +} from "@/server/hono/schemas/draft.schema"; export const DRAFT_URL = `/drafts`; diff --git a/src/lib/utils/api/files.api.ts b/src/client/utils/api/files.api.ts similarity index 96% rename from src/lib/utils/api/files.api.ts rename to src/client/utils/api/files.api.ts index c3b1c97c..4f75f85d 100644 --- a/src/lib/utils/api/files.api.ts +++ b/src/client/utils/api/files.api.ts @@ -1,9 +1,9 @@ import type { ApiResponse, UploadedImageInfo, -} from "@/lib/hono/schemas/common.schema"; +} from "@/server/hono/schemas/common.schema"; import { apiClient } from "./client"; -import { compressToWebP } from "@/lib/utils/Compress"; +import { compressToWebP } from "@/client/utils/Compress"; const ALLOWED_IMAGE_TYPES = [ "image/jpeg", diff --git a/src/lib/utils/api/filter.api.ts b/src/client/utils/api/filter.api.ts similarity index 86% rename from src/lib/utils/api/filter.api.ts rename to src/client/utils/api/filter.api.ts index 4253e9b8..0e8c028e 100644 --- a/src/lib/utils/api/filter.api.ts +++ b/src/client/utils/api/filter.api.ts @@ -1,10 +1,10 @@ import { apiClient } from "./client"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { AvailableFiltersResponse, AvailableFiltersTotalResponse, FilterRequestBody, -} from "@/lib/hono/schemas/filter.schema"; +} from "@/server/hono/schemas/filter.schema"; export const filterApi = { /** diff --git a/src/lib/utils/api/perfumes.api.ts b/src/client/utils/api/perfumes.api.ts similarity index 90% rename from src/lib/utils/api/perfumes.api.ts rename to src/client/utils/api/perfumes.api.ts index 49b71b54..ec63d834 100644 --- a/src/lib/utils/api/perfumes.api.ts +++ b/src/client/utils/api/perfumes.api.ts @@ -1,6 +1,6 @@ import { apiClient } from "./client"; -import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; -import { ApiPerfumeDetailResponse, ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; +import { ApiPerfumeDetailResponse, ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import { PerfumeNote, PerfumeAccord } from "@prisma/client"; export const perfumeApi = { diff --git a/src/lib/utils/api/points.api.ts b/src/client/utils/api/points.api.ts similarity index 91% rename from src/lib/utils/api/points.api.ts rename to src/client/utils/api/points.api.ts index 77b91abc..04bdbeb1 100644 --- a/src/lib/utils/api/points.api.ts +++ b/src/client/utils/api/points.api.ts @@ -1,5 +1,5 @@ import { apiClient } from "./client"; -import { ApiResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiResponse } from "@/server/hono/schemas/common.schema"; import { ApiPointHistoryResponse, ApiUserPointsResponse, @@ -7,7 +7,7 @@ import { ApiProcessLoginRequest, ApiProcessLoginResponse, GetPointHistoryQuery, -} from "@/lib/hono/schemas/point.schema"; +} from "@/server/hono/schemas/point.schema"; /** * 포인트 관련 API 클라이언트 diff --git a/src/lib/utils/api/reviews.api.ts b/src/client/utils/api/reviews.api.ts similarity index 84% rename from src/lib/utils/api/reviews.api.ts rename to src/client/utils/api/reviews.api.ts index a2636c84..247da000 100644 --- a/src/lib/utils/api/reviews.api.ts +++ b/src/client/utils/api/reviews.api.ts @@ -1,9 +1,9 @@ import { apiClient } from "./client"; -import { ApiResponse, ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; +import { ApiResponse, ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; import { ApiReviewResponse, CreateReviewInput, -} from "@/lib/hono/schemas/review.schema"; +} from "@/server/hono/schemas/review.schema"; export const reviewApi = { /** diff --git a/src/lib/utils/api/search.api.ts b/src/client/utils/api/search.api.ts similarity index 93% rename from src/lib/utils/api/search.api.ts rename to src/client/utils/api/search.api.ts index 65ca6502..05f898b1 100644 --- a/src/lib/utils/api/search.api.ts +++ b/src/client/utils/api/search.api.ts @@ -1,10 +1,10 @@ import { apiClient } from "./client"; -import { ApiResponse } from "@/lib/hono/schemas/common.schema"; -import { PaginatedSearchResponse } from "@/lib/hono/schemas/search.schema"; +import { ApiResponse } from "@/server/hono/schemas/common.schema"; +import { PaginatedSearchResponse } from "@/server/hono/schemas/search.schema"; import { SearchPerfumesParams, SearchPerfumesWithFiltersParams, -} from "@/lib/types/search.types"; +} from "@/shared/types/search.types"; const formatFilters = (filters: Record) => { const result: { diff --git a/src/lib/utils/api/users.api.ts b/src/client/utils/api/users.api.ts similarity index 93% rename from src/lib/utils/api/users.api.ts rename to src/client/utils/api/users.api.ts index c13136db..43eabc64 100644 --- a/src/lib/utils/api/users.api.ts +++ b/src/client/utils/api/users.api.ts @@ -1,20 +1,20 @@ import { apiClient } from "./client"; import { fileApi } from "./files.api"; -import { MyComment } from "@/lib/hono/services/me.service"; +import { MyComment } from "@/server/hono/services/me.service"; import { ApiResponse, ApiSuccessResponse, PaginatedResponse, -} from "@/lib/hono/schemas/common.schema"; -import { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; -import { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; -import { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +} from "@/server/hono/schemas/common.schema"; +import { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; +import { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; +import { ApiPostResponse } from "@/server/hono/schemas/community.schema"; import { ApiMyCollectionResponse, ApiMyBookmarkedPerfumesResponse, ApiMyProfileResponse, ApiUpdateMyProfileRequest, -} from "@/lib/hono/schemas/me.schema"; +} from "@/server/hono/schemas/me.schema"; export const USER_ID_REGEX = /^[0-9a-fA-F-]{36}$/; diff --git a/src/lib/utils/core-request/README.md b/src/client/utils/core-request/README.md similarity index 100% rename from src/lib/utils/core-request/README.md rename to src/client/utils/core-request/README.md diff --git a/src/lib/utils/core-request/config.ts b/src/client/utils/core-request/config.ts similarity index 100% rename from src/lib/utils/core-request/config.ts rename to src/client/utils/core-request/config.ts diff --git a/src/lib/utils/core-request/core.ts b/src/client/utils/core-request/core.ts similarity index 100% rename from src/lib/utils/core-request/core.ts rename to src/client/utils/core-request/core.ts diff --git a/src/lib/utils/core-request/httpClient.ts b/src/client/utils/core-request/httpClient.ts similarity index 100% rename from src/lib/utils/core-request/httpClient.ts rename to src/client/utils/core-request/httpClient.ts diff --git a/src/lib/utils/core-request/index.ts b/src/client/utils/core-request/index.ts similarity index 100% rename from src/lib/utils/core-request/index.ts rename to src/client/utils/core-request/index.ts diff --git a/src/lib/utils/core-request/serverClient.ts b/src/client/utils/core-request/serverClient.ts similarity index 100% rename from src/lib/utils/core-request/serverClient.ts rename to src/client/utils/core-request/serverClient.ts diff --git a/src/lib/utils/core-request/token.ts b/src/client/utils/core-request/token.ts similarity index 100% rename from src/lib/utils/core-request/token.ts rename to src/client/utils/core-request/token.ts diff --git a/src/lib/utils/getQueryClient.ts b/src/client/utils/getQueryClient.ts similarity index 100% rename from src/lib/utils/getQueryClient.ts rename to src/client/utils/getQueryClient.ts diff --git a/src/lib/utils/queryKeys.ts b/src/client/utils/queryKeys.ts similarity index 96% rename from src/lib/utils/queryKeys.ts rename to src/client/utils/queryKeys.ts index cb1865e8..a32c0fe4 100644 --- a/src/lib/utils/queryKeys.ts +++ b/src/client/utils/queryKeys.ts @@ -1,10 +1,10 @@ import { SearchPerfumesParams, SearchPerfumesWithFiltersParams, -} from "@/lib/types/search.types"; -import type { FilterRequestBody } from "@/lib/hono/schemas/filter.schema"; +} from "@/shared/types/search.types"; +import type { FilterRequestBody } from "@/server/hono/schemas/filter.schema"; -import { stableStringify } from "./stableStringify"; +import { stableStringify } from "@/shared/utils/stableStringify"; interface PerfumeFilters { brands?: string[]; diff --git a/src/lib/utils/share/kakaoShare.ts b/src/client/utils/share/kakaoShare.ts similarity index 100% rename from src/lib/utils/share/kakaoShare.ts rename to src/client/utils/share/kakaoShare.ts diff --git a/src/lib/utils/share/linkShare.ts b/src/client/utils/share/linkShare.ts similarity index 100% rename from src/lib/utils/share/linkShare.ts rename to src/client/utils/share/linkShare.ts diff --git a/src/components/commons/Provider/GlobalStateSync.tsx b/src/components/commons/Provider/GlobalStateSync.tsx index 5401032a..46d1dcaa 100644 --- a/src/components/commons/Provider/GlobalStateSync.tsx +++ b/src/components/commons/Provider/GlobalStateSync.tsx @@ -1,9 +1,9 @@ "use client"; import { useEffect } from "react"; -import { useUserProfile } from "@/lib/hooks/query/useUserQuery"; -import { useUserStore } from "@/lib/stores/useUserStore"; -import { pointApi } from "@/lib/utils/api/points.api"; +import { useUserProfile } from "@/client/hooks/query/useUserQuery"; +import { useUserStore } from "@/client/stores/useUserStore"; +import { pointApi } from "@/client/utils/api/points.api"; interface GlobalStateSyncProps { isAuthenticated: boolean; diff --git a/src/components/commons/Provider/RecentSyncManager.tsx b/src/components/commons/Provider/RecentSyncManager.tsx index e11cc180..be740af3 100644 --- a/src/components/commons/Provider/RecentSyncManager.tsx +++ b/src/components/commons/Provider/RecentSyncManager.tsx @@ -1,9 +1,9 @@ "use client"; -import { useRecentItemsSync } from "@/lib/hooks/useRecentItemsSync"; -import { useRecentPerfumesStore } from "@/lib/stores/useRecentPerfumesStore"; -import { useRecentPostsStore } from "@/lib/stores/useRecentPostsStore"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useRecentItemsSync } from "@/client/hooks/useRecentItemsSync"; +import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; +import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; +import { useUserStore } from "@/client/stores/useUserStore"; function RecentSyncManager() { const { user, isLoading } = useUserStore(); diff --git a/src/components/commons/author/PostMeta.tsx b/src/components/commons/author/PostMeta.tsx index d57402c5..c120afe9 100644 --- a/src/components/commons/author/PostMeta.tsx +++ b/src/components/commons/author/PostMeta.tsx @@ -1,6 +1,6 @@ import React from "react"; import IconBadge from "../author/IconBadge"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import { PostMetaItem } from "./author.types"; interface PostMetaProps { diff --git a/src/components/commons/author/author.constants.ts b/src/components/commons/author/author.constants.ts index 35d0a123..0753486a 100644 --- a/src/components/commons/author/author.constants.ts +++ b/src/components/commons/author/author.constants.ts @@ -1,4 +1,4 @@ -import IMAGES from "@/lib/constants/images"; +import IMAGES from "@/shared/constants/images"; export const REVIEW_STATUSES = { CURRENTLY_USING: "지금 쓰고 있어요", diff --git a/src/components/commons/author/author.types.ts b/src/components/commons/author/author.types.ts index 05859e95..cda95c86 100644 --- a/src/components/commons/author/author.types.ts +++ b/src/components/commons/author/author.types.ts @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import { REVIEW_STATUSES, SIZE_STATUSES } from "./author.constants"; import { PerfumeUsageStatus } from "@prisma/client"; interface Author { diff --git a/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx b/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx index 5e190439..471298f1 100644 --- a/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx +++ b/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx @@ -1,4 +1,4 @@ -import { NAV_BAR_ICONS } from "@/lib/constants/icons/navBar"; +import { NAV_BAR_ICONS } from "@/shared/constants/icons/navBar"; export const BOTTOM_NAV_BAR_ICONS = (userId: string) => { return [ diff --git a/src/components/commons/bottomNavBar/index.tsx b/src/components/commons/bottomNavBar/index.tsx index bd3cd21a..10077917 100644 --- a/src/components/commons/bottomNavBar/index.tsx +++ b/src/components/commons/bottomNavBar/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useUserStore } from "@/client/stores/useUserStore"; import { BOTTOM_NAV_BAR_ICONS } from "./BottomNavBar.constants"; import { BottomNavBarIcon } from "./BottomNavBarIcon"; import { usePathname } from "next/navigation"; diff --git a/src/components/commons/button/ButtonBase.tsx b/src/components/commons/button/ButtonBase.tsx index 9adeded2..f2827c99 100644 --- a/src/components/commons/button/ButtonBase.tsx +++ b/src/components/commons/button/ButtonBase.tsx @@ -1,4 +1,4 @@ -import { BACKGROUND_COLORS, TBgColor } from "@/lib/constants/colors"; +import { BACKGROUND_COLORS, TBgColor } from "@/shared/constants/colors"; import { ReactNode } from "react"; export interface IButtonBaseProps extends React.ComponentProps<"button"> { diff --git a/src/components/commons/card/perfumeCard/index.tsx b/src/components/commons/card/perfumeCard/index.tsx index 6d001121..7e0839ab 100644 --- a/src/components/commons/card/perfumeCard/index.tsx +++ b/src/components/commons/card/perfumeCard/index.tsx @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; import { PERFUME_CARD_STYLES, diff --git a/src/components/commons/card/postCard/Thumbnail.tsx b/src/components/commons/card/postCard/Thumbnail.tsx index acf38037..9655d4eb 100644 --- a/src/components/commons/card/postCard/Thumbnail.tsx +++ b/src/components/commons/card/postCard/Thumbnail.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import { THUMBNAIL_SIZES } from "@/lib/constants/post"; +import { THUMBNAIL_SIZES } from "@/shared/constants/post"; import { ThumbnailProps } from "./postCard.types"; export default function Thumbnail({ diff --git a/src/components/commons/card/postCard/index.tsx b/src/components/commons/card/postCard/index.tsx index 159a12e9..b6d3b92b 100644 --- a/src/components/commons/card/postCard/index.tsx +++ b/src/components/commons/card/postCard/index.tsx @@ -4,7 +4,7 @@ import AuthorInfo from "../../author/AuthorInfo"; import BoardChip from "../../chip/BoardChip"; import PostMeta from "../../author/PostMeta"; import Thumbnail from "./Thumbnail"; -import { POST_CARD_STYLES, POST_CARD_TYPES } from "@/lib/constants/post"; +import { POST_CARD_STYLES, POST_CARD_TYPES } from "@/shared/constants/post"; import { PostCardProps } from "./postCard.types"; import { PostMetaItem } from "../../author/author.types"; diff --git a/src/components/commons/card/postCard/postCard.types.ts b/src/components/commons/card/postCard/postCard.types.ts index 4512abca..d337cf79 100644 --- a/src/components/commons/card/postCard/postCard.types.ts +++ b/src/components/commons/card/postCard/postCard.types.ts @@ -1,5 +1,5 @@ -import { POST_CARD_TYPES, CATEGORY_TYPES } from "@/lib/constants/post"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import { POST_CARD_TYPES, CATEGORY_TYPES } from "@/shared/constants/post"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; export type PostCardType = (typeof POST_CARD_TYPES)[keyof typeof POST_CARD_TYPES]; diff --git a/src/components/commons/card/reviewCard/index.tsx b/src/components/commons/card/reviewCard/index.tsx index 7e005b39..b48a88d5 100644 --- a/src/components/commons/card/reviewCard/index.tsx +++ b/src/components/commons/card/reviewCard/index.tsx @@ -6,7 +6,7 @@ import { getLayoutSize } from "./reviewCard.helpers"; import { ReviewCardProps } from "./reviewCard.types"; import AuthorInfo from "../../author/AuthorInfo"; import ReviewChip from "../../chip/ReviewChip"; -import { getCategoryById, getTagByKey } from "@/lib/utils/review.helpers"; +import { getCategoryById, getTagByKey } from "@/shared/utils/review.helpers"; export default function ReviewCard({ review, diff --git a/src/components/commons/card/reviewCard/reviewCard.types.ts b/src/components/commons/card/reviewCard/reviewCard.types.ts index 117edc4b..5895241e 100644 --- a/src/components/commons/card/reviewCard/reviewCard.types.ts +++ b/src/components/commons/card/reviewCard/reviewCard.types.ts @@ -1,4 +1,4 @@ -import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; +import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; export interface ReviewCardProps { review: ApiReviewResponse; isMyPage?: boolean; diff --git a/src/components/commons/chip/AccordChip.tsx b/src/components/commons/chip/AccordChip.tsx index cf2228e2..78141060 100644 --- a/src/components/commons/chip/AccordChip.tsx +++ b/src/components/commons/chip/AccordChip.tsx @@ -1,4 +1,4 @@ -import { ACCORDS, TAccords } from "@/lib/constants/accords"; +import { ACCORDS, TAccords } from "@/shared/constants/accords"; import ChipBase from "./ChipBase"; interface IAccordChipProps { diff --git a/src/components/commons/chip/BoardChip.tsx b/src/components/commons/chip/BoardChip.tsx index c584f0f8..4ecd9d5b 100644 --- a/src/components/commons/chip/BoardChip.tsx +++ b/src/components/commons/chip/BoardChip.tsx @@ -1,6 +1,6 @@ -// import { BOARD_TYPES, TBoardType } from "@/lib/constants/communityBoard"; +// import { BOARD_TYPES, TBoardType } from "@/shared/constants/communityBoard"; import { ChipOutlinedPrimary } from "./ChipOutlined"; -import { CATEGORY_LABELS, CategoryType } from "@/lib/constants/post"; +import { CATEGORY_LABELS, CategoryType } from "@/shared/constants/post"; interface IBoardChipProps { size?: "s" | "m"; diff --git a/src/components/commons/chip/ChipBase.tsx b/src/components/commons/chip/ChipBase.tsx index 85a314d9..5c477c4b 100644 --- a/src/components/commons/chip/ChipBase.tsx +++ b/src/components/commons/chip/ChipBase.tsx @@ -1,4 +1,4 @@ -import { BACKGROUND_COLORS, TBgColor } from "@/lib/constants/colors"; +import { BACKGROUND_COLORS, TBgColor } from "@/shared/constants/colors"; import { ReactNode } from "react"; export interface IChipBaseProps extends React.ComponentProps<"div"> { diff --git a/src/components/commons/chip/stories.tsx b/src/components/commons/chip/stories.tsx index 6ecd8cc5..ef1fc251 100644 --- a/src/components/commons/chip/stories.tsx +++ b/src/components/commons/chip/stories.tsx @@ -3,8 +3,8 @@ import BoardChip from "./BoardChip"; import AccordChip from "./AccordChip"; import ReviewChip from "./ReviewChip"; import LevelChip from "./LevelChip"; -import { CATEGORY_LABELS, CategoryType } from "@/lib/constants/post"; -import { ACCORDS, TAccords } from "@/lib/constants/accords"; +import { CATEGORY_LABELS, CategoryType } from "@/shared/constants/post"; +import { ACCORDS, TAccords } from "@/shared/constants/accords"; const meta: Meta = { title: "Components/Chips", diff --git a/src/components/commons/ckeditor5/CkEditor5.tsx b/src/components/commons/ckeditor5/CkEditor5.tsx index e70a50a4..02f0b0e3 100644 --- a/src/components/commons/ckeditor5/CkEditor5.tsx +++ b/src/components/commons/ckeditor5/CkEditor5.tsx @@ -29,7 +29,7 @@ import { import translations from "ckeditor5/translations/ko.js"; import makeLocalPreviewUploadPlugin, { BlobRegistry, -} from "@/lib/ckeditor/localPreviewUploadPlugin"; +} from "@/client/ckeditor/localPreviewUploadPlugin"; const LICENSE_KEY = "GPL"; diff --git a/src/components/commons/dropdown/BoardDropdown.tsx b/src/components/commons/dropdown/BoardDropdown.tsx index 506aca7a..65dc9bec 100644 --- a/src/components/commons/dropdown/BoardDropdown.tsx +++ b/src/components/commons/dropdown/BoardDropdown.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; import Dropdown from "./DropdownBase"; -import { DEFAULT_BOARD, Option, BOARD_OPTIONS } from "@/lib/constants/options"; +import { DEFAULT_BOARD, Option, BOARD_OPTIONS } from "@/shared/constants/options"; import { PostCategory } from "@prisma/client"; interface IBoardDropdownProps { diff --git a/src/components/commons/dropdown/DropdownBase.tsx b/src/components/commons/dropdown/DropdownBase.tsx index fac04178..76bca167 100644 --- a/src/components/commons/dropdown/DropdownBase.tsx +++ b/src/components/commons/dropdown/DropdownBase.tsx @@ -1,9 +1,9 @@ import DropdownButton from "./DropdownButton"; import DropdownMenu from "./DropdownMenu"; import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; -import { Option } from "@/lib/constants/options"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; +import ICONS from "@/shared/constants/icons"; +import { Option } from "@/shared/constants/options"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; interface IFilterDropdownProps { id: string; diff --git a/src/components/commons/dropdown/DropdownMenu.tsx b/src/components/commons/dropdown/DropdownMenu.tsx index 69c7efaf..cd00ac6c 100644 --- a/src/components/commons/dropdown/DropdownMenu.tsx +++ b/src/components/commons/dropdown/DropdownMenu.tsx @@ -1,4 +1,4 @@ -import { Option } from "../../../lib/constants/options"; +import { Option } from "@/shared/constants/options"; interface IDropdownMenuProps { options: Option[]; handleSelectOption: (option: Option) => void; diff --git a/src/components/commons/dropdown/ShareDropdownMenu.tsx b/src/components/commons/dropdown/ShareDropdownMenu.tsx index b8a13eb2..b5e7d0e0 100644 --- a/src/components/commons/dropdown/ShareDropdownMenu.tsx +++ b/src/components/commons/dropdown/ShareDropdownMenu.tsx @@ -1,6 +1,6 @@ -import kakaoShare from "@/lib/utils/share/kakaoShare"; -import handleShare from "@/lib/utils/share/linkShare"; -import { Option } from "../../../lib/constants/options"; +import kakaoShare from "@/client/utils/share/kakaoShare"; +import handleShare from "@/client/utils/share/linkShare"; +import { Option } from "@/shared/constants/options"; import DropdownMenu from "./DropdownMenu"; export default function ShareDropdownMenu({ closeMenu, diff --git a/src/components/commons/dropdown/SortDropdown.tsx b/src/components/commons/dropdown/SortDropdown.tsx index b48007c8..d9efd409 100644 --- a/src/components/commons/dropdown/SortDropdown.tsx +++ b/src/components/commons/dropdown/SortDropdown.tsx @@ -4,7 +4,7 @@ import { Option, SORT_BY_OPTIONS, TSortBy, -} from "@/lib/constants/options"; +} from "@/shared/constants/options"; import Dropdown from "./DropdownBase"; export interface ISortDropdownProps { diff --git a/src/components/commons/navBar/Dropdown.tsx b/src/components/commons/navBar/Dropdown.tsx index 2993f3c6..194fa6ee 100644 --- a/src/components/commons/navBar/Dropdown.tsx +++ b/src/components/commons/navBar/Dropdown.tsx @@ -5,8 +5,8 @@ import Link from "next/link"; import { useQueryClient } from "@tanstack/react-query"; import LevelChip from "../chip/LevelChip"; import { getMyPageNavItems } from "./navBar.constants"; -import { useUserStore } from "@/lib/stores/useUserStore"; -import { logout } from "@/lib/database/action/login"; +import { useUserStore } from "@/client/stores/useUserStore"; +import { logout } from "@/server/database/action/login"; interface DropdownProps { onClose: () => void; diff --git a/src/components/commons/navBar/UserProfileSection.tsx b/src/components/commons/navBar/UserProfileSection.tsx index ca9c7200..18e2bec1 100644 --- a/src/components/commons/navBar/UserProfileSection.tsx +++ b/src/components/commons/navBar/UserProfileSection.tsx @@ -1,11 +1,11 @@ "use client"; import { useState, useRef } from "react"; import Image from "next/image"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useUserStore } from "@/client/stores/useUserStore"; import { NAV_LABELS } from "./navBar.constants"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import { NavDropdown } from "./Dropdown"; -import { useModalStore, MODAL_KEYS } from "@/lib/stores/useModalStore"; +import { useModalStore, MODAL_KEYS } from "@/client/stores/useModalStore"; const UserProfileSection = () => { const navBarRef = useRef(null); diff --git a/src/components/commons/navBar/navBar.constants.ts b/src/components/commons/navBar/navBar.constants.ts index 3a90565c..d0b0ddc1 100644 --- a/src/components/commons/navBar/navBar.constants.ts +++ b/src/components/commons/navBar/navBar.constants.ts @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; export const NAV_PATHS = { HOME: "/", diff --git a/src/components/commons/perfumeList/filter/FilterItem.tsx b/src/components/commons/perfumeList/filter/FilterItem.tsx index d34bc89e..82773167 100644 --- a/src/components/commons/perfumeList/filter/FilterItem.tsx +++ b/src/components/commons/perfumeList/filter/FilterItem.tsx @@ -1,9 +1,9 @@ import { memo, useState, useEffect } from "react"; import Image from "next/image"; -import { Option } from "@/lib/constants/options"; -import { useFilterStore } from "@/lib/stores/useFilterStore"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; -import ICONS from "@/lib/constants/icons"; +import { Option } from "@/shared/constants/options"; +import { useFilterStore } from "@/client/stores/useFilterStore"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import ICONS from "@/shared/constants/icons"; import FilterItemModal from "../modal/ItemModal"; import FilterTriggerButton from "@/components/commons/dropdown/DropdownButton"; import { getFilterOptionMeta } from "./filter.helper"; diff --git a/src/components/commons/perfumeList/filter/FilterList.tsx b/src/components/commons/perfumeList/filter/FilterList.tsx index 02db940b..9b89d66d 100644 --- a/src/components/commons/perfumeList/filter/FilterList.tsx +++ b/src/components/commons/perfumeList/filter/FilterList.tsx @@ -1,8 +1,8 @@ import Image from "next/image"; -import { useFilterStore } from "@/lib/stores/useFilterStore"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; -import ICONS from "@/lib/constants/icons"; -import { Option } from "@/lib/constants/options"; +import { useFilterStore } from "@/client/stores/useFilterStore"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import ICONS from "@/shared/constants/icons"; +import { Option } from "@/shared/constants/options"; import ListModal from "../modal/ListModal"; type FilterOptions = { diff --git a/src/components/commons/perfumeList/filter/filter.helper.ts b/src/components/commons/perfumeList/filter/filter.helper.ts index 62c69864..c57cc8b6 100644 --- a/src/components/commons/perfumeList/filter/filter.helper.ts +++ b/src/components/commons/perfumeList/filter/filter.helper.ts @@ -1,5 +1,5 @@ import { JsonValue } from "@prisma/client/runtime/library"; -import { Option } from "@/lib/constants/options"; +import { Option } from "@/shared/constants/options"; interface LocalizedName { en: string; diff --git a/src/components/commons/perfumeList/filter/index.tsx b/src/components/commons/perfumeList/filter/index.tsx index fa7ecbcb..f0fe1ffa 100644 --- a/src/components/commons/perfumeList/filter/index.tsx +++ b/src/components/commons/perfumeList/filter/index.tsx @@ -1,10 +1,10 @@ import React, { useMemo } from "react"; import FilterItem from "./FilterItem"; -import type { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; +import type { ApiBrandSimpleResponse } from "@/server/hono/schemas/brand.schema"; import { ApiPerfumeNoteResponse, ApiPerfumeAccordResponse, -} from "@/lib/hono/schemas/perfume.schema"; +} from "@/server/hono/schemas/perfume.schema"; import { GENDER_OPTIONS } from "./filter.constants"; import { getLabel } from "../perfumes.helpers"; import { toOption, typedKeys } from "./filter.helper"; diff --git a/src/components/commons/perfumeList/modal/ItemModal.tsx b/src/components/commons/perfumeList/modal/ItemModal.tsx index 8e43fb9a..94bae208 100644 --- a/src/components/commons/perfumeList/modal/ItemModal.tsx +++ b/src/components/commons/perfumeList/modal/ItemModal.tsx @@ -1,11 +1,11 @@ import { useEffect, useMemo } from "react"; -import { Option } from "@/lib/constants/options"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; -import { useFilterStore } from "@/lib/stores/useFilterStore"; +import { Option } from "@/shared/constants/options"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useFilterStore } from "@/client/stores/useFilterStore"; import { ModalContainer } from "@/components/modal/ModalContainer"; import { FilterModalLayout } from "./Layout"; import { getLabel } from "../perfumes.helpers"; -import { useAvailableFilters } from "@/lib/hooks/query/useFilterQuery"; +import { useAvailableFilters } from "@/client/hooks/query/useFilterQuery"; interface IFilterDropdownProps { id: string; diff --git a/src/components/commons/perfumeList/modal/Layout.tsx b/src/components/commons/perfumeList/modal/Layout.tsx index 471ca6bf..f5e23a5d 100644 --- a/src/components/commons/perfumeList/modal/Layout.tsx +++ b/src/components/commons/perfumeList/modal/Layout.tsx @@ -1,8 +1,8 @@ import { ReactNode } from "react"; -// import { useTotalStore } from "@/lib/stores/useCountStore"; +// import { useTotalStore } from "@/client/stores/useCountStore"; import { ButtonFilledPrimaryLFull } from "@/components/commons/button/ButtonFilled"; import { ButtonOutlinedPrimaryLFull } from "@/components/commons/button/ButtonOutlined"; -import { useFilterStore } from "@/lib/stores/useFilterStore"; +import { useFilterStore } from "@/client/stores/useFilterStore"; import { Indicator } from "../../loading/Indicator"; interface FilterModalLayoutProps { diff --git a/src/components/commons/perfumeList/modal/ListModal.tsx b/src/components/commons/perfumeList/modal/ListModal.tsx index 063cc2f1..ece4863f 100644 --- a/src/components/commons/perfumeList/modal/ListModal.tsx +++ b/src/components/commons/perfumeList/modal/ListModal.tsx @@ -3,8 +3,8 @@ import Image from "next/image"; import { ModalContainer } from "@/components/modal/ModalContainer"; import { FilterModalLayout } from "./Layout"; import { getLabel } from "../perfumes.helpers"; -import ICONS from "@/lib/constants/icons"; -import { Option } from "@/lib/constants/options"; +import ICONS from "@/shared/constants/icons"; +import { Option } from "@/shared/constants/options"; type FilterOptions = { gender: Option[]; diff --git a/src/components/commons/perfumeList/perfumes.helpers.ts b/src/components/commons/perfumeList/perfumes.helpers.ts index 015e0b53..44f18337 100644 --- a/src/components/commons/perfumeList/perfumes.helpers.ts +++ b/src/components/commons/perfumeList/perfumes.helpers.ts @@ -1,4 +1,4 @@ -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import { FILTER_LABELS } from "./filter/filter.constants"; /** diff --git a/src/components/commons/perfumeList/search/index.tsx b/src/components/commons/perfumeList/search/index.tsx index e86ccc4f..46313ac8 100644 --- a/src/components/commons/perfumeList/search/index.tsx +++ b/src/components/commons/perfumeList/search/index.tsx @@ -2,8 +2,8 @@ import React, { ChangeEvent, FormEvent } from "react"; import { ApiPerfumeAccordResponse, ApiPerfumeNoteResponse, -} from "@/lib/hono/schemas/perfume.schema"; -import type { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; +} from "@/server/hono/schemas/perfume.schema"; +import type { ApiBrandSimpleResponse } from "@/server/hono/schemas/brand.schema"; import { SearchBar } from "@/components/commons/search/SearchBar"; import PerFumeFilter from "../filter"; diff --git a/src/components/commons/perfumeList/section/BrandSection.tsx b/src/components/commons/perfumeList/section/BrandSection.tsx index d4ffd60b..a89da55c 100644 --- a/src/components/commons/perfumeList/section/BrandSection.tsx +++ b/src/components/commons/perfumeList/section/BrandSection.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import IMAGES from "@/lib/constants/images"; +import IMAGES from "@/shared/constants/images"; export function BrandSection({ brandName }: { brandName: string }) { return ( diff --git a/src/components/commons/perfumeList/section/PerfumeSection.tsx b/src/components/commons/perfumeList/section/PerfumeSection.tsx index e24ebceb..98da372e 100644 --- a/src/components/commons/perfumeList/section/PerfumeSection.tsx +++ b/src/components/commons/perfumeList/section/PerfumeSection.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { Spinner } from "@/components/commons/loading/Spinner"; import PerfumeCard from "@/components/commons/card/perfumeCard"; import { PERFUME_CARD_TYPES } from "@/components/commons/card/perfumeCard/perfumeCard.constants"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; interface PerfumeSectionProps { perfumes: ApiPerfumeSimpleResponse[]; diff --git a/src/components/commons/search/SearchBar.tsx b/src/components/commons/search/SearchBar.tsx index a91401b5..235204fb 100644 --- a/src/components/commons/search/SearchBar.tsx +++ b/src/components/commons/search/SearchBar.tsx @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; import React from "react"; diff --git a/src/components/commons/wrapper/PageWrapper.tsx b/src/components/commons/wrapper/PageWrapper.tsx index e7d2b5c6..30120bb2 100644 --- a/src/components/commons/wrapper/PageWrapper.tsx +++ b/src/components/commons/wrapper/PageWrapper.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; -import { useModalStore, MODAL_KEYS } from "@/lib/stores/useModalStore"; +import { useModalStore, MODAL_KEYS } from "@/client/stores/useModalStore"; interface PageWrapperProps { className?: string; diff --git a/src/components/domains/brandDetail/image/index.tsx b/src/components/domains/brandDetail/image/index.tsx index 169316f9..f43d1c85 100644 --- a/src/components/domains/brandDetail/image/index.tsx +++ b/src/components/domains/brandDetail/image/index.tsx @@ -7,7 +7,7 @@ import "swiper/css/pagination"; import "swiper/css/navigation"; import Image from "next/image"; import { useState } from "react"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; interface IBrandDetailImageProps { images: { diff --git a/src/components/domains/brandDetail/info/index.tsx b/src/components/domains/brandDetail/info/index.tsx index 0f861029..70eef39b 100644 --- a/src/components/domains/brandDetail/info/index.tsx +++ b/src/components/domains/brandDetail/info/index.tsx @@ -1,6 +1,6 @@ "use client"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; import Link from "next/link"; diff --git a/src/components/domains/brandDetail/perfumes/PageClient.tsx b/src/components/domains/brandDetail/perfumes/PageClient.tsx index 6e355085..7420fc1a 100644 --- a/src/components/domains/brandDetail/perfumes/PageClient.tsx +++ b/src/components/domains/brandDetail/perfumes/PageClient.tsx @@ -5,12 +5,12 @@ import SortDropdown from "@/components/commons/dropdown/SortDropdown"; import { usePerfumeNoteFilter, usePerfumeAccordFilter, -} from "@/lib/hooks/query/useFilterQuery"; +} from "@/client/hooks/query/useFilterQuery"; import { SearchHeader } from "../../../commons/perfumeList/search"; import { PerfumeSection } from "../../../commons/perfumeList/section/PerfumeSection"; import { getUniquePerfumes } from "@/components/commons/perfumeList/perfumes.helpers"; -import { useInfiniteScrollTrigger } from "@/lib/hooks/useInfiniteScrollTrigger"; -import { useInfinitePerfumes } from "@/lib/hooks/useInfinitePerfumes"; +import { useInfiniteScrollTrigger } from "@/client/hooks/useInfiniteScrollTrigger"; +import { useInfinitePerfumes } from "@/client/hooks/useInfinitePerfumes"; export const PageClient = ({ brandName }: { brandName: string }) => { const { data: notes } = usePerfumeNoteFilter(); diff --git a/src/components/domains/community/PageClient.tsx b/src/components/domains/community/PageClient.tsx index f8bbcde6..b35819f3 100644 --- a/src/components/domains/community/PageClient.tsx +++ b/src/components/domains/community/PageClient.tsx @@ -10,10 +10,10 @@ import { getApiSortBy, getUniquePostList, } from "./community.helpers"; -import { Option, TSortBy } from "@/lib/constants/options"; -import { useInfiniteScrollTrigger } from "@/lib/hooks/useInfiniteScrollTrigger"; -import { COMMUNITY_BOARDS } from "@/lib/constants/communityBoard"; -import type { PaginatedApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import { Option, TSortBy } from "@/shared/constants/options"; +import { useInfiniteScrollTrigger } from "@/client/hooks/useInfiniteScrollTrigger"; +import { COMMUNITY_BOARDS } from "@/shared/constants/communityBoard"; +import type { PaginatedApiPostResponse } from "@/server/hono/schemas/community.schema"; export default function PageClient() { const router = useRouter(); diff --git a/src/components/domains/community/community.helpers.ts b/src/components/domains/community/community.helpers.ts index 9b7d4219..edc57e45 100644 --- a/src/components/domains/community/community.helpers.ts +++ b/src/components/domains/community/community.helpers.ts @@ -2,12 +2,12 @@ import { SORT_MAPPING_COMMUNITY_FOR_API, TApiSortBy, TSortBy, -} from "@/lib/constants/options"; +} from "@/shared/constants/options"; import { GetPostsQuery, ApiPostResponse, PaginatedApiPostResponse, -} from "@/lib/hono/schemas/community.schema"; +} from "@/server/hono/schemas/community.schema"; const API_BASE_URL = "/api/v1"; diff --git a/src/components/domains/community/communityCards/index.tsx b/src/components/domains/community/communityCards/index.tsx index fd4257a1..30df0604 100644 --- a/src/components/domains/community/communityCards/index.tsx +++ b/src/components/domains/community/communityCards/index.tsx @@ -2,8 +2,8 @@ import React from "react"; import Link from "next/link"; import { PostCard } from "@/components/commons/card/postCard"; import { Spinner } from "@/components/commons/loading/Spinner"; -import { POST_CARD_TYPES } from "@/lib/constants/post"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import { POST_CARD_TYPES } from "@/shared/constants/post"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; interface ICommunityCardsProps { postData: ApiPostResponse[]; diff --git a/src/components/domains/community/header/WriteButton.tsx b/src/components/domains/community/header/WriteButton.tsx index 89f319cb..408f6622 100644 --- a/src/components/domains/community/header/WriteButton.tsx +++ b/src/components/domains/community/header/WriteButton.tsx @@ -2,7 +2,7 @@ import { ButtonOutlinedPrimaryLFit } from "@/components/commons/button/ButtonOutlined"; import { NAV_PATHS } from "@/components/commons/navBar/navBar.constants"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState, useEffect } from "react"; @@ -10,7 +10,7 @@ import { DraftRestoreModal } from "@/components/modal/draftRestoreModal"; import { useDraftByType, useDeleteDraft, -} from "@/lib/hooks/query/useDraftQuery"; +} from "@/client/hooks/query/useDraftQuery"; import { DraftType } from "@prisma/client"; export default function WriteButton() { diff --git a/src/components/domains/community/header/index.tsx b/src/components/domains/community/header/index.tsx index a448586e..7fe9d2ca 100644 --- a/src/components/domains/community/header/index.tsx +++ b/src/components/domains/community/header/index.tsx @@ -6,7 +6,7 @@ import SortDropdown, { ISortDropdownProps, } from "@/components/commons/dropdown/SortDropdown"; import WriteButton from "./WriteButton"; -import { Option, TSortBy } from "@/lib/constants/options"; +import { Option, TSortBy } from "@/shared/constants/options"; interface IHeaderProps extends IBoardTabBarProps, diff --git a/src/components/domains/main/MainLogo.tsx b/src/components/domains/main/MainLogo.tsx index 3746837a..b92e287f 100644 --- a/src/components/domains/main/MainLogo.tsx +++ b/src/components/domains/main/MainLogo.tsx @@ -1,4 +1,4 @@ -import IMAGES from "@/lib/constants/images"; +import IMAGES from "@/shared/constants/images"; import Image from "next/image"; export const MainLogo = () => { diff --git a/src/components/domains/main/MainSearchBar.tsx b/src/components/domains/main/MainSearchBar.tsx index 7e064373..cb608eb1 100644 --- a/src/components/domains/main/MainSearchBar.tsx +++ b/src/components/domains/main/MainSearchBar.tsx @@ -2,15 +2,15 @@ import { SearchBar } from "@/components/commons/search/SearchBar"; import { useRouter } from "next/navigation"; -import { useSearchBar } from "@/lib/hooks/useSearchBar"; +import { useSearchBar } from "@/client/hooks/useSearchBar"; import { useEffect, useState, useRef } from "react"; -import useDebounce from "@/lib/hooks/useDebounce"; -import { searchApi } from "@/lib/utils/api/search.api"; +import useDebounce from "@/client/hooks/useDebounce"; +import { searchApi } from "@/client/utils/api/search.api"; import { SearchResultsDropdown } from "@/components/commons/dropdown/SearchResultsDropdown"; -import useOnClickOutside from "@/lib/hooks/useOnClickOutside"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; +import useOnClickOutside from "@/client/hooks/useOnClickOutside"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; import Image from "next/image"; -import { highlightText } from "@/lib/utils/highlightText"; +import { highlightText } from "@/shared/utils/highlightText"; import Logo from "../../../..//public/images/Logo.webp"; interface PerfumeSearchResult { diff --git a/src/components/domains/main/mainBanner/MainBanner.tsx b/src/components/domains/main/mainBanner/MainBanner.tsx index 67bebba2..2fb7fe81 100644 --- a/src/components/domains/main/mainBanner/MainBanner.tsx +++ b/src/components/domains/main/mainBanner/MainBanner.tsx @@ -1,5 +1,5 @@ import { MainBannerPerfumeList } from "./MainBannerPerfumeList"; -import { getPerfumesListByThemeService } from "@/lib/hono/services/perfume.service"; +import { getPerfumesListByThemeService } from "@/server/hono/services/perfume.service"; import { unstable_cache } from "next/cache"; const getCachedPerfumes = unstable_cache( diff --git a/src/components/domains/main/mainBanner/MainBannerPerfumeList.tsx b/src/components/domains/main/mainBanner/MainBannerPerfumeList.tsx index 62d7b183..a53ba90c 100644 --- a/src/components/domains/main/mainBanner/MainBannerPerfumeList.tsx +++ b/src/components/domains/main/mainBanner/MainBannerPerfumeList.tsx @@ -1,6 +1,6 @@ import PerfumeCard from "@/components/commons/card/perfumeCard"; import Link from "next/link"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; interface IMainBannerPerfumesListProps { data: ApiPerfumeSimpleResponse[]; diff --git a/src/components/domains/main/mainContent/MainContentButtons/MainContentButtons.tsx b/src/components/domains/main/mainContent/MainContentButtons/MainContentButtons.tsx index e57a7009..cf45a523 100644 --- a/src/components/domains/main/mainContent/MainContentButtons/MainContentButtons.tsx +++ b/src/components/domains/main/mainContent/MainContentButtons/MainContentButtons.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; import { MainContentButton } from "./MainContentButton"; -import { MAIN_BUTTONS } from "@/lib/constants/main"; +import { MAIN_BUTTONS } from "@/shared/constants/main"; const MainContentButtonSwiper = dynamic( () => diff --git a/src/components/domains/main/mainContent/MainContentButtons/MainContentButtonsSwiper.tsx b/src/components/domains/main/mainContent/MainContentButtons/MainContentButtonsSwiper.tsx index d7619629..1e9f397b 100644 --- a/src/components/domains/main/mainContent/MainContentButtons/MainContentButtonsSwiper.tsx +++ b/src/components/domains/main/mainContent/MainContentButtons/MainContentButtonsSwiper.tsx @@ -6,7 +6,7 @@ import "swiper/css"; import "swiper/css/pagination"; import "swiper/css/navigation"; import { MainContentButton } from "./MainContentButton"; -import { MAIN_BUTTONS } from "@/lib/constants/main"; +import { MAIN_BUTTONS } from "@/shared/constants/main"; export const MainContentButtonSwiper = () => { return ( diff --git a/src/components/domains/main/mainContent/MainContentReview.tsx b/src/components/domains/main/mainContent/MainContentReview.tsx index f7434086..7579fff6 100644 --- a/src/components/domains/main/mainContent/MainContentReview.tsx +++ b/src/components/domains/main/mainContent/MainContentReview.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import ReviewCard from "@/components/commons/card/reviewCard"; -import { reviewApi } from "@/lib/utils/api/reviews.api"; -import { DEFAULT_POPULAR_REVIEW } from "@/lib/constants/review"; +import { reviewApi } from "@/client/utils/api/reviews.api"; +import { DEFAULT_POPULAR_REVIEW } from "@/shared/constants/review"; export const MainContentReview = async () => { const review = await reviewApi.popular(); diff --git a/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunity.tsx b/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunity.tsx index a0766f0e..fd3de019 100644 --- a/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunity.tsx +++ b/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunity.tsx @@ -1,5 +1,5 @@ import { MainContentCommunityList } from "./MainContentCommunityList"; -import { getPaginatedPostListService } from "@/lib/hono/services/community.service"; +import { getPaginatedPostListService } from "@/server/hono/services/community.service"; export const MainContentCommunity = async () => { const result = await getPaginatedPostListService({ diff --git a/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx b/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx index 81950066..67df5cdb 100644 --- a/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx +++ b/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx @@ -1,15 +1,15 @@ "use client"; -import { BOARD_TYPES } from "@/lib/constants/communityBoard"; -import ICONS from "@/lib/constants/icons"; +import { BOARD_TYPES } from "@/shared/constants/communityBoard"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; import { useState } from "react"; import BoardTabBar from "@/components/commons/tabBar/BoardTabBar"; import Link from "next/link"; -import { useCommunityPosts } from "@/lib/hooks/query/useCommunityQuery"; +import { useCommunityPosts } from "@/client/hooks/query/useCommunityQuery"; import { PostCategory } from "@prisma/client"; import { Indicator } from "@/components/commons/loading/Indicator"; -import { BasePost } from "@/lib/hono/utils/prisma.utils"; +import { BasePost } from "@/server/hono/utils/prisma.utils"; interface IMainContentCommunityList { size: "s" | "m"; diff --git a/src/components/domains/perfumeDetail/DetailClient.tsx b/src/components/domains/perfumeDetail/DetailClient.tsx index bb0dc592..ad502b51 100644 --- a/src/components/domains/perfumeDetail/DetailClient.tsx +++ b/src/components/domains/perfumeDetail/DetailClient.tsx @@ -1,6 +1,6 @@ "use client"; import { SearchBar } from "@/components/commons/search/SearchBar"; -import type { ApiPerfumeDetailResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeDetailResponse } from "@/server/hono/schemas/perfume.schema"; import { useState, useMemo } from "react"; import { MobileActionBar } from "./mobileActionBar"; import { PerfumeOverview } from "./overview"; @@ -9,9 +9,9 @@ import { PerfumeRecentViewList } from "./recentViewList"; import { PerfumeReviews } from "./review"; import { PerfumeDetailSidebar } from "./sidebar"; import { MobileSeparator } from "@/components/commons/mobileSeparator"; -import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; -import { useLogRecentItem } from "@/lib/stores/useLogRecentItem"; -import { useRecentPerfumesStore } from "@/lib/stores/useRecentPerfumesStore"; +import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; +import { useLogRecentItem } from "@/client/stores/useLogRecentItem"; +import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; // temp // sematic tag 적용하기 (하위 컴포넌트 전부) diff --git a/src/components/domains/perfumeDetail/overview/index.tsx b/src/components/domains/perfumeDetail/overview/index.tsx index 97477731..097c9b7e 100644 --- a/src/components/domains/perfumeDetail/overview/index.tsx +++ b/src/components/domains/perfumeDetail/overview/index.tsx @@ -1,4 +1,4 @@ -import type { ApiPerfumeDetailResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeDetailResponse } from "@/server/hono/schemas/perfume.schema"; import { PerfumeImage } from "./perfumeImage"; import { InteractionStates, PerfumeInfo } from "./perfumeInfo"; diff --git a/src/components/domains/perfumeDetail/overview/perfumeInfo/header/index.tsx b/src/components/domains/perfumeDetail/overview/perfumeInfo/header/index.tsx index 6b22c129..2ece08c7 100644 --- a/src/components/domains/perfumeDetail/overview/perfumeInfo/header/index.tsx +++ b/src/components/domains/perfumeDetail/overview/perfumeInfo/header/index.tsx @@ -7,7 +7,7 @@ import { import { BookmarkIcon } from "@/components/commons/interactions/icons/BookmarkIcon"; import { LikeIcon } from "@/components/commons/interactions/icons/LikeIcon"; import { ShareIcon } from "@/components/commons/interactions/icons/ShareIcon"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; import Link from "next/link"; import { InteractionStates } from ".."; diff --git a/src/components/domains/perfumeDetail/overview/perfumeInfo/index.tsx b/src/components/domains/perfumeDetail/overview/perfumeInfo/index.tsx index 32dd0009..179e8c9b 100644 --- a/src/components/domains/perfumeDetail/overview/perfumeInfo/index.tsx +++ b/src/components/domains/perfumeDetail/overview/perfumeInfo/index.tsx @@ -1,12 +1,12 @@ "use client"; -import type { ApiPerfumeDetailResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeDetailResponse } from "@/server/hono/schemas/perfume.schema"; import { PerfumeInfoHeader } from "./header"; import { PerfumeInfoMainAccord } from "./mainAccords"; import { PerfumeInfoNote } from "./notes"; import { ButtonOutlinedPrimaryLFull } from "@/components/commons/button/ButtonOutlined"; import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; export type InteractionStates = { liked: boolean; diff --git a/src/components/domains/perfumeDetail/review/analytics/ReviewMobileSection/index.tsx b/src/components/domains/perfumeDetail/review/analytics/ReviewMobileSection/index.tsx index 97a6a743..f2bfc12c 100644 --- a/src/components/domains/perfumeDetail/review/analytics/ReviewMobileSection/index.tsx +++ b/src/components/domains/perfumeDetail/review/analytics/ReviewMobileSection/index.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react"; import { countByCategory, type Counts } from "../analytics.helpers"; -import type { ReviewCategory } from "@/lib/types/review.types"; +import type { ReviewCategory } from "@/shared/types/review.types"; export type BarDatum = { label: string; value: number }; export type Block = { diff --git a/src/components/domains/perfumeDetail/review/analytics/analytics.constants.ts b/src/components/domains/perfumeDetail/review/analytics/analytics.constants.ts index ad96cd83..da75018b 100644 --- a/src/components/domains/perfumeDetail/review/analytics/analytics.constants.ts +++ b/src/components/domains/perfumeDetail/review/analytics/analytics.constants.ts @@ -1,5 +1,5 @@ -import { REVIEW_CONFIG } from "@/lib/constants/review"; -import { ReviewCategory, ReviewOptionKey } from "@/lib/types/review.types"; +import { REVIEW_CONFIG } from "@/shared/constants/review"; +import { ReviewCategory, ReviewOptionKey } from "@/shared/types/review.types"; const ANALYTICS_COLORS: { [K in ReviewCategory]?: { [P in ReviewOptionKey]?: string }; diff --git a/src/components/domains/perfumeDetail/review/analytics/analytics.helpers.ts b/src/components/domains/perfumeDetail/review/analytics/analytics.helpers.ts index 2a3a95f8..cf33a26b 100644 --- a/src/components/domains/perfumeDetail/review/analytics/analytics.helpers.ts +++ b/src/components/domains/perfumeDetail/review/analytics/analytics.helpers.ts @@ -1,6 +1,6 @@ -import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; +import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; import { ATTRIBUTE_ID_MAP, CATEGORY_LABEL_MAP } from "./analytics.constants"; -import type { ReviewCategory } from "@/lib/types/review.types"; +import type { ReviewCategory } from "@/shared/types/review.types"; export type AttributeId = number; export type OptionKey = string; diff --git a/src/components/domains/perfumeDetail/review/analytics/index.tsx b/src/components/domains/perfumeDetail/review/analytics/index.tsx index bc2f73df..249701cc 100644 --- a/src/components/domains/perfumeDetail/review/analytics/index.tsx +++ b/src/components/domains/perfumeDetail/review/analytics/index.tsx @@ -5,7 +5,7 @@ import dynamic from "next/dynamic"; import { ButtonFilledPrimaryLFull } from "@/components/commons/button/ButtonFilled"; import { SectionTitle } from "@/components/commons/sectionTitle"; import { ReviewModal } from "@/components/modal/reviewModal"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useUserStore } from "@/client/stores/useUserStore"; import { calculateReviewCounts } from "./analytics.helpers"; import { ReviewAnalyticsProps } from "../review.type"; diff --git a/src/components/domains/perfumeDetail/review/review.type.ts b/src/components/domains/perfumeDetail/review/review.type.ts index df4122b2..e5635526 100644 --- a/src/components/domains/perfumeDetail/review/review.type.ts +++ b/src/components/domains/perfumeDetail/review/review.type.ts @@ -1,5 +1,5 @@ import { REVIEW_STATUSES } from "@/components/commons/author/author.constants"; -import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; +import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; export type ReviewStatus = keyof typeof REVIEW_STATUSES; diff --git a/src/components/domains/perfumeDetail/review/reviewList/index.tsx b/src/components/domains/perfumeDetail/review/reviewList/index.tsx index 8976b55a..0c47fe00 100644 --- a/src/components/domains/perfumeDetail/review/reviewList/index.tsx +++ b/src/components/domains/perfumeDetail/review/reviewList/index.tsx @@ -3,7 +3,7 @@ import { ButtonFilledGrayLFull } from "@/components/commons/button/ButtonFilled" import { SectionTitle } from "@/components/commons/sectionTitle"; import { ReviewItem } from "./reviewItem"; import { ReviewListProps } from "../review.type"; -import { getCategoryById, getTagByKey } from "@/lib/utils/review.helpers"; +import { getCategoryById, getTagByKey } from "@/shared/utils/review.helpers"; export const ReviewList = ({ data }: ReviewListProps) => { const reviewItems = useMemo(() => { diff --git a/src/components/domains/perfumeDetail/sidebar/index.tsx b/src/components/domains/perfumeDetail/sidebar/index.tsx index 64218cf7..bc7601ef 100644 --- a/src/components/domains/perfumeDetail/sidebar/index.tsx +++ b/src/components/domains/perfumeDetail/sidebar/index.tsx @@ -1,8 +1,8 @@ import React from "react"; import { PostCard } from "@/components/commons/card/postCard"; -import { CATEGORY_TYPES, POST_CARD_TYPES } from "@/lib/constants/post"; +import { CATEGORY_TYPES, POST_CARD_TYPES } from "@/shared/constants/post"; import { SectionTitle } from "@/components/commons/sectionTitle"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; export const PerfumeDetailSidebar = () => { const mockPosts: ApiPostResponse[] = [ diff --git a/src/components/domains/perfumes/PageClient.tsx b/src/components/domains/perfumes/PageClient.tsx index dfad32ba..f7304517 100644 --- a/src/components/domains/perfumes/PageClient.tsx +++ b/src/components/domains/perfumes/PageClient.tsx @@ -5,15 +5,15 @@ import { usePerfumeNoteFilter, usePerfumeAccordFilter, useAvailableFilters, -} from "@/lib/hooks/query/useFilterQuery"; +} from "@/client/hooks/query/useFilterQuery"; import { BrandSection } from "@/components/commons/perfumeList/section/BrandSection"; import { PerfumeSection } from "@/components/commons/perfumeList/section/PerfumeSection"; import { SearchHeader } from "@/components/commons/perfumeList/search"; import SortDropdown from "@/components/commons/dropdown/SortDropdown"; -import { useInfinitePerfumes } from "@/lib/hooks/useInfinitePerfumes"; +import { useInfinitePerfumes } from "@/client/hooks/useInfinitePerfumes"; import { usePerfumeSearchState } from "./hook/usePerfumeSearchState"; -import { useInfiniteScrollTrigger } from "@/lib/hooks/useInfiniteScrollTrigger"; -import { useFilterStore } from "@/lib/stores/useFilterStore"; +import { useInfiniteScrollTrigger } from "@/client/hooks/useInfiniteScrollTrigger"; +import { useFilterStore } from "@/client/stores/useFilterStore"; import { useMemo } from "react"; export default function PageClient() { diff --git a/src/components/domains/perfumes/hook/usePerfumeSearchState.ts b/src/components/domains/perfumes/hook/usePerfumeSearchState.ts index f4b78fa1..70edfe47 100644 --- a/src/components/domains/perfumes/hook/usePerfumeSearchState.ts +++ b/src/components/domains/perfumes/hook/usePerfumeSearchState.ts @@ -2,7 +2,7 @@ import { useState, useMemo, ChangeEvent, FormEvent, useEffect } from "react"; import { useSearchParams } from "next/navigation"; -import type { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; +import type { ApiBrandSimpleResponse } from "@/server/hono/schemas/brand.schema"; export const usePerfumeSearchState = ( brands: ApiBrandSimpleResponse[] diff --git a/src/components/domains/post/PageClient.tsx b/src/components/domains/post/PageClient.tsx index a11096dc..d8704173 100644 --- a/src/components/domains/post/PageClient.tsx +++ b/src/components/domains/post/PageClient.tsx @@ -1,11 +1,11 @@ "use client"; -import { useCommunityPostForEdit } from "@/lib/hooks/query/useCommunityQuery"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useCommunityPostForEdit } from "@/client/hooks/query/useCommunityQuery"; +import { useUserStore } from "@/client/stores/useUserStore"; import { useDraft, useDraftByPostId, -} from "@/lib/hooks/query/useDraftQuery"; +} from "@/client/hooks/query/useDraftQuery"; import { useSearchParams } from "next/navigation"; import PostForm, { PostFormInitialData } from "./form"; diff --git a/src/components/domains/post/form/PostEditor.tsx b/src/components/domains/post/form/PostEditor.tsx index bfb7697f..7cbde205 100644 --- a/src/components/domains/post/form/PostEditor.tsx +++ b/src/components/domains/post/form/PostEditor.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; import SubTitleLabel from "./element/SubTitleLabel"; import EditorLoading from "@/components/commons/ckeditor5/EditorLoading"; import { Controller, useFormContext } from "react-hook-form"; -import { BlobRegistry } from "@/lib/ckeditor/localPreviewUploadPlugin"; +import { BlobRegistry } from "@/client/ckeditor/localPreviewUploadPlugin"; import { MutableRefObject } from "react"; const CkEditor5 = dynamic( diff --git a/src/components/domains/post/form/index.tsx b/src/components/domains/post/form/index.tsx index e291e444..800aa1f7 100644 --- a/src/components/domains/post/form/index.tsx +++ b/src/components/domains/post/form/index.tsx @@ -6,19 +6,19 @@ import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { PostCategory as TPostCategory } from "@prisma/client"; -import type { BlobRegistry } from "@/lib/ckeditor/localPreviewUploadPlugin"; -import { finalizeWithBlobRegistry } from "@/lib/ckeditor/finalizeWithBlobRegistry"; -import type { PerfumeForPost } from "@/lib/hono/schemas/community.schema"; +import type { BlobRegistry } from "@/client/ckeditor/localPreviewUploadPlugin"; +import { finalizeWithBlobRegistry } from "@/client/ckeditor/finalizeWithBlobRegistry"; +import type { PerfumeForPost } from "@/server/hono/schemas/community.schema"; import { CreatePostClientSchema, type CreatePostClientInput, } from "./postSchema"; -import { usePostMutation } from "@/lib/hooks/query/useCommunityQuery"; -import { extractFirstImageSrc } from "@/lib/utils/extractFirstImageSrc"; -import getPlainText from "@/lib/utils/getPlainText"; -import { usePostAutosave } from "@/lib/hooks/usePostAutosave"; -import { useBeforeUnload } from "@/lib/hooks/useBeforeUnload"; -import { useDeleteDraft } from "@/lib/hooks/query/useDraftQuery"; +import { usePostMutation } from "@/client/hooks/query/useCommunityQuery"; +import { extractFirstImageSrc } from "@/shared/utils/extractFirstImageSrc"; +import getPlainText from "@/shared/utils/getPlainText"; +import { usePostAutosave } from "@/client/hooks/usePostAutosave"; +import { useBeforeUnload } from "@/client/hooks/useBeforeUnload"; +import { useDeleteDraft } from "@/client/hooks/query/useDraftQuery"; import PostCategory from "./PostCategory"; import PostEditor from "./PostEditor"; diff --git a/src/components/domains/post/form/postRelatedPerfume/PerfumeResultItem.tsx b/src/components/domains/post/form/postRelatedPerfume/PerfumeResultItem.tsx index 4ca444fb..e2e30c17 100644 --- a/src/components/domains/post/form/postRelatedPerfume/PerfumeResultItem.tsx +++ b/src/components/domains/post/form/postRelatedPerfume/PerfumeResultItem.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import ICONS from "@/shared/constants/icons"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import clsx from "clsx"; diff --git a/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx b/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx index fef4c2ac..4dce5104 100644 --- a/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx +++ b/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx @@ -5,11 +5,11 @@ import { ButtonFilledPrimaryLFit } from "@/components/commons/button/ButtonFille import PerfumeCard from "@/components/commons/card/perfumeCard"; import { SearchResultsDropdown } from "@/components/commons/dropdown/SearchResultsDropdown"; import InputBase from "@/components/commons/input"; -import type { PerfumeForPost } from "@/lib/hono/schemas/community.schema"; -import useDebounce from "@/lib/hooks/useDebounce"; -import useOnClickOutside from "@/lib/hooks/useOnClickOutside"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; -import { searchApi } from "@/lib/utils/api/search.api"; +import type { PerfumeForPost } from "@/server/hono/schemas/community.schema"; +import useDebounce from "@/client/hooks/useDebounce"; +import useOnClickOutside from "@/client/hooks/useOnClickOutside"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { searchApi } from "@/client/utils/api/search.api"; import SubTitleLabel from "../element/SubTitleLabel"; import PerfumeResultItem from "./PerfumeResultItem"; diff --git a/src/components/domains/post/form/postRelatedPerfume/usePerfumeSearch.ts b/src/components/domains/post/form/postRelatedPerfume/usePerfumeSearch.ts index 0d5b5d2e..a0d9c85c 100644 --- a/src/components/domains/post/form/postRelatedPerfume/usePerfumeSearch.ts +++ b/src/components/domains/post/form/postRelatedPerfume/usePerfumeSearch.ts @@ -1,4 +1,4 @@ -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import { useState, useEffect } from "react"; diff --git a/src/components/domains/post/form/postSchema.ts b/src/components/domains/post/form/postSchema.ts index 96df7a69..f9722850 100644 --- a/src/components/domains/post/form/postSchema.ts +++ b/src/components/domains/post/form/postSchema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { PostCategory } from "@prisma/client"; -import { PerfumeForPostSchema } from "@/lib/hono/schemas/community.schema"; +import { PerfumeForPostSchema } from "@/server/hono/schemas/community.schema"; export const CreatePostClientSchema = z.object({ category: z.nativeEnum(PostCategory, { diff --git a/src/components/domains/postDetail/PageClient.tsx b/src/components/domains/postDetail/PageClient.tsx index 9fc9191f..b1f83480 100644 --- a/src/components/domains/postDetail/PageClient.tsx +++ b/src/components/domains/postDetail/PageClient.tsx @@ -3,8 +3,8 @@ import dynamic from "next/dynamic"; import PostContent from "./content"; import PostDetailHeader from "./header"; -import { useLogRecentItem } from "@/lib/stores/useLogRecentItem"; -import { useRecentPostsStore } from "@/lib/stores/useRecentPostsStore"; +import { useLogRecentItem } from "@/client/stores/useLogRecentItem"; +import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; import { PageSkeleton, CategoryPostListSectionSkeleton, @@ -14,7 +14,7 @@ import { useCommunityPost, useCommunityPostCategoryPosts, useCommunityPostStatus, -} from "@/lib/hooks/query/useCommunityQuery"; +} from "@/client/hooks/query/useCommunityQuery"; const CommentSection = dynamic(() => import("./commentSection"), { ssr: false, diff --git a/src/components/domains/postDetail/categoryPostListSection/CategoryPostList.tsx b/src/components/domains/postDetail/categoryPostListSection/CategoryPostList.tsx index 0e48b6aa..264e510d 100644 --- a/src/components/domains/postDetail/categoryPostListSection/CategoryPostList.tsx +++ b/src/components/domains/postDetail/categoryPostListSection/CategoryPostList.tsx @@ -1,5 +1,5 @@ import CommunityListItem from "@/components/commons/list/CommunityListItem"; -import type { ApiPostDetailCategoryPostResponse } from "@/lib/hono/schemas/community.schema"; +import type { ApiPostDetailCategoryPostResponse } from "@/server/hono/schemas/community.schema"; interface ICategoryPostListProps { posts: ApiPostDetailCategoryPostResponse[] | []; diff --git a/src/components/domains/postDetail/categoryPostListSection/Header.tsx b/src/components/domains/postDetail/categoryPostListSection/Header.tsx index 9119a80f..8c943557 100644 --- a/src/components/domains/postDetail/categoryPostListSection/Header.tsx +++ b/src/components/domains/postDetail/categoryPostListSection/Header.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; -import { BOARD_TYPES } from "@/lib/constants/communityBoard"; -import type { ApiPostDetailResponse } from "@/lib/hono/schemas/community.schema"; +import ICONS from "@/shared/constants/icons"; +import { BOARD_TYPES } from "@/shared/constants/communityBoard"; +import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema"; interface CommunityListHeaderProps { category: ApiPostDetailResponse["category"]; diff --git a/src/components/domains/postDetail/categoryPostListSection/PostListMobileSwiper.tsx b/src/components/domains/postDetail/categoryPostListSection/PostListMobileSwiper.tsx index 71458545..5e873afc 100644 --- a/src/components/domains/postDetail/categoryPostListSection/PostListMobileSwiper.tsx +++ b/src/components/domains/postDetail/categoryPostListSection/PostListMobileSwiper.tsx @@ -6,7 +6,7 @@ import "swiper/css"; import "swiper/css/pagination"; import CategoryPostList from "./CategoryPostList"; import { Pagination } from "swiper/modules"; -import type { ApiPostDetailCategoryPostResponse } from "@/lib/hono/schemas/community.schema"; +import type { ApiPostDetailCategoryPostResponse } from "@/server/hono/schemas/community.schema"; interface IPostListMobileSwiperProps { posts: ApiPostDetailCategoryPostResponse[] | []; diff --git a/src/components/domains/postDetail/categoryPostListSection/index.tsx b/src/components/domains/postDetail/categoryPostListSection/index.tsx index 55ef483d..11b2fc58 100644 --- a/src/components/domains/postDetail/categoryPostListSection/index.tsx +++ b/src/components/domains/postDetail/categoryPostListSection/index.tsx @@ -3,7 +3,7 @@ import { ApiPostDetailCategoryPostResponse, ApiPostDetailResponse, -} from "@/lib/hono/schemas/community.schema"; +} from "@/server/hono/schemas/community.schema"; import { useState } from "react"; import dynamic from "next/dynamic"; import CategoryPostListHeader from "./Header"; diff --git a/src/components/domains/postDetail/commentSection/comment/CommentForm.tsx b/src/components/domains/postDetail/commentSection/comment/CommentForm.tsx index 3f4dba08..8288798c 100644 --- a/src/components/domains/postDetail/commentSection/comment/CommentForm.tsx +++ b/src/components/domains/postDetail/commentSection/comment/CommentForm.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import { ButtonFilledPrimaryLFit } from "@/components/commons/button/ButtonFilled"; import { ButtonOutlinedPrimaryLFit } from "@/components/commons/button/ButtonOutlined"; import Comment from "@/components/commons/comment"; -import { useCommentMutation } from "@/lib/hooks/query/useCommentQuery"; +import { useCommentMutation } from "@/client/hooks/query/useCommentQuery"; import { CommentFormProps } from "./postComment.types"; diff --git a/src/components/domains/postDetail/commentSection/comment/CommentListItem.tsx b/src/components/domains/postDetail/commentSection/comment/CommentListItem.tsx index b59a90d0..c126917c 100644 --- a/src/components/domains/postDetail/commentSection/comment/CommentListItem.tsx +++ b/src/components/domains/postDetail/commentSection/comment/CommentListItem.tsx @@ -3,9 +3,9 @@ import PostTime from "@/components/commons/author/PostTime"; import { CommentReplyResponse, CommentResponse, -} from "@/lib/hono/schemas/comment.schema"; -import { useCommentMutation } from "@/lib/hooks/query/useCommentQuery"; -import { useUserStore } from "@/lib/stores/useUserStore"; +} from "@/server/hono/schemas/comment.schema"; +import { useCommentMutation } from "@/client/hooks/query/useCommentQuery"; +import { useUserStore } from "@/client/stores/useUserStore"; import { CommentAuthInfo } from "./CommentAuthInfo"; import CommentForm from "./CommentForm"; diff --git a/src/components/domains/postDetail/commentSection/comment/ReplyIcon.tsx b/src/components/domains/postDetail/commentSection/comment/ReplyIcon.tsx index c9f59981..5031cc1c 100644 --- a/src/components/domains/postDetail/commentSection/comment/ReplyIcon.tsx +++ b/src/components/domains/postDetail/commentSection/comment/ReplyIcon.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; export default function ReplyIcon() { return ( diff --git a/src/components/domains/postDetail/commentSection/comment/postComment.types.ts b/src/components/domains/postDetail/commentSection/comment/postComment.types.ts index a30912fc..eeb4bd22 100644 --- a/src/components/domains/postDetail/commentSection/comment/postComment.types.ts +++ b/src/components/domains/postDetail/commentSection/comment/postComment.types.ts @@ -1,4 +1,4 @@ -import type { CommentResponse } from "@/lib/hono/schemas/comment.schema"; +import type { CommentResponse } from "@/server/hono/schemas/comment.schema"; export interface CommentAuthInfoProps { author: string; diff --git a/src/components/domains/postDetail/commentSection/index.tsx b/src/components/domains/postDetail/commentSection/index.tsx index 059227a0..e555483c 100644 --- a/src/components/domains/postDetail/commentSection/index.tsx +++ b/src/components/domains/postDetail/commentSection/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { useInfiniteComments } from "@/lib/hooks/query/useCommentQuery"; +import { useInfiniteComments } from "@/client/hooks/query/useCommentQuery"; import CommentForm from "./comment/CommentForm"; import CommentList from "./comment/CommentList"; diff --git a/src/components/domains/postDetail/content/RelatedPerfume.tsx b/src/components/domains/postDetail/content/RelatedPerfume.tsx index 9eabf542..cb3ed164 100644 --- a/src/components/domains/postDetail/content/RelatedPerfume.tsx +++ b/src/components/domains/postDetail/content/RelatedPerfume.tsx @@ -1,7 +1,7 @@ "use client"; import PerfumeCard from "@/components/commons/card/perfumeCard"; import Link from "next/link"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; interface IRelatedPerfumeProps { perfumes: ApiPerfumeSimpleResponse[]; } diff --git a/src/components/domains/postDetail/content/index.tsx b/src/components/domains/postDetail/content/index.tsx index cdad2086..250eb250 100644 --- a/src/components/domains/postDetail/content/index.tsx +++ b/src/components/domains/postDetail/content/index.tsx @@ -1,7 +1,7 @@ "use client"; import "@/components/commons/ckeditor5/ckeditor5.css"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import PostActions from "../header/PostActions"; import RelatedPerfume from "./RelatedPerfume"; diff --git a/src/components/domains/postDetail/header/PostActions.tsx b/src/components/domains/postDetail/header/PostActions.tsx index af25cf84..d652fe6c 100644 --- a/src/components/domains/postDetail/header/PostActions.tsx +++ b/src/components/domains/postDetail/header/PostActions.tsx @@ -4,11 +4,11 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { ActionItem, Actions } from "@/components/commons/actions"; -import { usePostMutation } from "@/lib/hooks/query/useCommunityQuery"; +import { usePostMutation } from "@/client/hooks/query/useCommunityQuery"; import { useDraftByType, useDeleteDraft, -} from "@/lib/hooks/query/useDraftQuery"; +} from "@/client/hooks/query/useDraftQuery"; import { DraftType } from "@prisma/client"; import { DraftRestoreModal } from "@/components/modal/draftRestoreModal"; diff --git a/src/components/domains/postDetail/header/PostInteractions.tsx b/src/components/domains/postDetail/header/PostInteractions.tsx index 2ef150b5..29a555e5 100644 --- a/src/components/domains/postDetail/header/PostInteractions.tsx +++ b/src/components/domains/postDetail/header/PostInteractions.tsx @@ -6,9 +6,9 @@ import { ShareIcon } from "@/components/commons/interactions/icons/ShareIcon"; import { Interactions } from "@/components/commons/interactions"; import usePostInteractionMutation from "./usePostInteractionMutation"; import { useRef } from "react"; -import { useVisibilityStore } from "@/lib/stores/useVisibilityStore"; +import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; import ShareDropdownMenu from "@/components/commons/dropdown/ShareDropdownMenu"; -import useOnClickOutside from "@/lib/hooks/useOnClickOutside"; +import useOnClickOutside from "@/client/hooks/useOnClickOutside"; interface IPostInteractionsProps { isLiked: boolean; diff --git a/src/components/domains/postDetail/header/index.tsx b/src/components/domains/postDetail/header/index.tsx index 2902b2eb..549df081 100644 --- a/src/components/domains/postDetail/header/index.tsx +++ b/src/components/domains/postDetail/header/index.tsx @@ -6,7 +6,7 @@ import PostActions from "./PostActions"; import { ApiPostDetailResponse, ApiPostStatusResponse, -} from "@/lib/hono/schemas/community.schema"; +} from "@/server/hono/schemas/community.schema"; import Link from "next/link"; type PostDetailHeaderProps = Omit & { diff --git a/src/components/domains/postDetail/header/usePostInteractionMutation.ts b/src/components/domains/postDetail/header/usePostInteractionMutation.ts index 5625207e..586cde0f 100644 --- a/src/components/domains/postDetail/header/usePostInteractionMutation.ts +++ b/src/components/domains/postDetail/header/usePostInteractionMutation.ts @@ -3,8 +3,8 @@ import { toggleBookmarkedPostById, toggleLikedPostById, } from "../postDetail.helpers"; -import type { ApiPostStatusResponse } from "@/lib/hono/schemas/community.schema"; -import { queryKeys } from "@/lib/utils/queryKeys"; +import type { ApiPostStatusResponse } from "@/server/hono/schemas/community.schema"; +import { queryKeys } from "@/client/utils/queryKeys"; export default function usePostInteractionMutation(postId: string) { const queryClient = useQueryClient(); diff --git a/src/components/domains/postDetail/postDetail.helpers.ts b/src/components/domains/postDetail/postDetail.helpers.ts index ae8855ed..f728a06d 100644 --- a/src/components/domains/postDetail/postDetail.helpers.ts +++ b/src/components/domains/postDetail/postDetail.helpers.ts @@ -1,6 +1,6 @@ -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; -import type { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema"; -import { apiClient } from "@/lib/utils/api/client"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; +import type { ApiSuccessResponse } from "@/server/hono/schemas/common.schema"; +import { apiClient } from "@/client/utils/api/client"; import { PostCategory } from "@prisma/client"; export const COMMUNITY_URL = `/community/posts`; diff --git a/src/components/domains/user/Reply.tsx b/src/components/domains/user/Reply.tsx index 9ab74fe8..9905d9a6 100644 --- a/src/components/domains/user/Reply.tsx +++ b/src/components/domains/user/Reply.tsx @@ -1,7 +1,7 @@ import React from "react"; import Link from "next/link"; import dayjs from "dayjs"; -import { MyComment } from "@/lib/hono/services/me.service"; +import { MyComment } from "@/server/hono/services/me.service"; interface ReplyProps extends MyComment { isLast: boolean; diff --git a/src/components/domains/user/UserLayoutClient.tsx b/src/components/domains/user/UserLayoutClient.tsx index 3e507520..5896122f 100644 --- a/src/components/domains/user/UserLayoutClient.tsx +++ b/src/components/domains/user/UserLayoutClient.tsx @@ -3,7 +3,7 @@ import UserHeader from "@/components/domains/user/sections/ProfileHeaderSection"; import UserFooter from "@/components/domains/user/sections/RecentViewSection"; import PageClient from "@/components/domains/user/PageClient"; -import { useUserProfileById } from "@/lib/hooks/query/useUserQuery"; +import { useUserProfileById } from "@/client/hooks/query/useUserQuery"; import { notFound } from "next/navigation"; interface UserLayoutClientProps { diff --git a/src/components/domains/user/components/ScrollRowSection.tsx b/src/components/domains/user/components/ScrollRowSection.tsx index 8a621b8e..f2507301 100644 --- a/src/components/domains/user/components/ScrollRowSection.tsx +++ b/src/components/domains/user/components/ScrollRowSection.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; export const ScrollRowSection = ({ title, diff --git a/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx b/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx index 2b0683be..cdc6b3c1 100644 --- a/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx +++ b/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx @@ -1,7 +1,7 @@ import React from "react"; import Link from "next/link"; import PerfumeCard from "@/components/commons/card/perfumeCard"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; export const LikePerfumeList = ({ likedPerfumes, diff --git a/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx b/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx index 99790201..e8209d75 100644 --- a/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx +++ b/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx @@ -1,8 +1,8 @@ import React from "react"; import Link from "next/link"; import { PostCard } from "@/components/commons/card/postCard"; -import { POST_CARD_TYPES } from "@/lib/constants/post"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import { POST_CARD_TYPES } from "@/shared/constants/post"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; export const LikePostList = ({ likedPosts, diff --git a/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx b/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx index 220e9e97..ac331c11 100644 --- a/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx +++ b/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx @@ -1,6 +1,6 @@ import React from "react"; import Reply from "@/components/domains/user/Reply"; -import { MyComment } from "@/lib/hono/services/me.service"; +import { MyComment } from "@/server/hono/services/me.service"; export const MyCommentsList = ({ comments }: { comments: MyComment[] }) => { return comments.map((item, idx) => ( diff --git a/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx b/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx index 8bab2cb9..42e2f324 100644 --- a/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx +++ b/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx @@ -1,8 +1,8 @@ import React from "react"; import Link from "next/link"; import { PostCard } from "@/components/commons/card/postCard"; -import { POST_CARD_TYPES } from "@/lib/constants/post"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import { POST_CARD_TYPES } from "@/shared/constants/post"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; export const MyPostList = ({ posts }: { posts: ApiPostResponse[] }) => { return posts.length > 0 ? ( diff --git a/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx b/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx index 25fa9595..d3d4af29 100644 --- a/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx +++ b/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx @@ -1,6 +1,6 @@ import React from "react"; import ReviewCard from "@/components/commons/card/reviewCard"; -import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; +import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; export const MyReviewList = ({ reviews }: { reviews: ApiReviewResponse[] }) => { return reviews.length > 0 ? ( diff --git a/src/components/domains/user/sections/ActivitySection/index.tsx b/src/components/domains/user/sections/ActivitySection/index.tsx index 0f807006..735641cc 100644 --- a/src/components/domains/user/sections/ActivitySection/index.tsx +++ b/src/components/domains/user/sections/ActivitySection/index.tsx @@ -10,7 +10,7 @@ import { SkeletonComment, SkeletonPerfume, } from "../../components/skeletons"; -import { useMediaQuery } from "@/lib/hooks/useMediaQuery"; +import { useMediaQuery } from "@/client/hooks/useMediaQuery"; const MyReviewListLoader = dynamic( () => import("./loader/MyReviewListLoader"), diff --git a/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx index c22453d4..ca882b4c 100644 --- a/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx +++ b/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx @@ -1,7 +1,7 @@ "use client"; import { LikePerfumeList } from "../components"; -import { useUserLikedPerfume } from "@/lib/hooks/query/useUserQuery"; +import { useUserLikedPerfume } from "@/client/hooks/query/useUserQuery"; export default function LikePerfumeListLoader() { const { data: likedPerfumes, error } = useUserLikedPerfume(); diff --git a/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx index 843b6b12..57665cab 100644 --- a/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx +++ b/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx @@ -1,7 +1,7 @@ "use client"; import { LikePostList } from "../components"; -import { useUserLikedPost } from "@/lib/hooks/query/useUserQuery"; +import { useUserLikedPost } from "@/client/hooks/query/useUserQuery"; export default function LikePostListLoader() { const { data: likedPosts, error } = useUserLikedPost(); diff --git a/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx index 78683655..3fbc2edf 100644 --- a/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx +++ b/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx @@ -1,7 +1,7 @@ "use client"; import { MyCommentsList } from "../components"; -import { useUserComment } from "@/lib/hooks/query/useUserQuery"; +import { useUserComment } from "@/client/hooks/query/useUserQuery"; export default function MyCommentsListLoader() { const { data: paginatedComments, error } = useUserComment(); diff --git a/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx index 9f2c1904..fbd85f85 100644 --- a/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx +++ b/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx @@ -1,7 +1,7 @@ "use client"; import { MyPostList } from "../components"; -import { useUserPost } from "@/lib/hooks/query/useUserQuery"; +import { useUserPost } from "@/client/hooks/query/useUserQuery"; export default function MyPostListLoader() { const { data: paginatedPosts, error } = useUserPost(); diff --git a/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx index 732bb178..2c58b896 100644 --- a/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx +++ b/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { MyReviewList } from "../components"; -import { useUserReview } from "@/lib/hooks/query/useUserQuery"; +import { useUserReview } from "@/client/hooks/query/useUserQuery"; export default function MyReviewListLoader() { const [isMounted, setIsMounted] = useState(false); diff --git a/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx b/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx index 77bf3ec0..a781df26 100644 --- a/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx +++ b/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { PostCard } from "@/components/commons/card/postCard"; -import { POST_CARD_TYPES } from "@/lib/constants/post"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; +import { POST_CARD_TYPES } from "@/shared/constants/post"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; export const CommunityBookmarkList = ({ communityPosts, diff --git a/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx b/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx index 1ce7a547..483625b1 100644 --- a/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx +++ b/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx @@ -1,5 +1,5 @@ import PerfumeCard from "@/components/commons/card/perfumeCard"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import Link from "next/link"; diff --git a/src/components/domains/user/sections/BookmarkSection/index.tsx b/src/components/domains/user/sections/BookmarkSection/index.tsx index 9749aff3..68e0f1f0 100644 --- a/src/components/domains/user/sections/BookmarkSection/index.tsx +++ b/src/components/domains/user/sections/BookmarkSection/index.tsx @@ -5,7 +5,7 @@ import dynamic from "next/dynamic"; import { SubTabSwitcher } from "@/components/domains/user/tabs/SubTabs"; import { useUrlTabs } from "../../useUrlTabs"; import { SkeletonCard, SkeletonPerfume } from "../../components/skeletons"; -import { useMediaQuery } from "@/lib/hooks/useMediaQuery"; +import { useMediaQuery } from "@/client/hooks/useMediaQuery"; const PerfumeBookmarksLoader = dynamic( () => import("./loader/PerfumeBookmarksLoader"), { diff --git a/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx b/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx index 8cf0da65..cbea58fd 100644 --- a/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx +++ b/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx @@ -1,7 +1,7 @@ "use client"; import { CommunityBookmarkList } from "../components"; -import { useUserPostsBookmarks } from "@/lib/hooks/query/useUserQuery"; +import { useUserPostsBookmarks } from "@/client/hooks/query/useUserQuery"; export default function CommunityBookmarksLoader() { const { data: communityData } = useUserPostsBookmarks(); diff --git a/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx b/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx index 91f508ba..9166b1a3 100644 --- a/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx +++ b/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx @@ -1,7 +1,7 @@ "use client"; import { PerfumeBookmarkList } from "../components"; -import { usePerfumeBookmarks } from "@/lib/hooks/query/useUserQuery"; +import { usePerfumeBookmarks } from "@/client/hooks/query/useUserQuery"; export default function PerfumeBookmarksLoader({ userId }: { userId: string }) { const { data: perfumeData } = usePerfumeBookmarks(userId); diff --git a/src/components/domains/user/sections/CollectionSection/index.tsx b/src/components/domains/user/sections/CollectionSection/index.tsx index 44e2b544..6ace947e 100644 --- a/src/components/domains/user/sections/CollectionSection/index.tsx +++ b/src/components/domains/user/sections/CollectionSection/index.tsx @@ -3,9 +3,9 @@ import Image from "next/image"; import ImageDetailModal from "@/components/modal/imageDetailModal"; import { useImageDetailModal } from "@/components/modal/imageDetailModal/useImageDetailModal"; -import { useUserCollections } from "@/lib/hooks/query/useUserQuery"; +import { useUserCollections } from "@/client/hooks/query/useUserQuery"; import { SkeletonMasonry } from "../../components/skeletons"; -import type { ApiMyCollectionResponse } from "@/lib/hono/schemas/me.schema"; +import type { ApiMyCollectionResponse } from "@/server/hono/schemas/me.schema"; export const CollectionSection = ({ userId }: { userId: string }) => { const { diff --git a/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx b/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx index 48a5b409..c61a6ed9 100644 --- a/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx +++ b/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx @@ -1,4 +1,4 @@ -import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema"; +import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; import UserProfileImage from "./UserProfileImage"; import UserInfo from "./UserInfo"; diff --git a/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx b/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx index e6e62f41..713c35f1 100644 --- a/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx +++ b/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx @@ -1,5 +1,5 @@ import LevelChip from "@/components/commons/chip/LevelChip"; -import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema"; +import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; interface UserInfoProps { user: ApiMyProfileResponse; diff --git a/src/components/domains/user/sections/ProfileHeaderSection/index.tsx b/src/components/domains/user/sections/ProfileHeaderSection/index.tsx index b0a3ab63..bf369619 100644 --- a/src/components/domains/user/sections/ProfileHeaderSection/index.tsx +++ b/src/components/domains/user/sections/ProfileHeaderSection/index.tsx @@ -2,8 +2,8 @@ import UserHeaderSkeleton from "./components/UserHeaderSkeleton"; import UserHeaderDisplay from "./components/UserHeaderDisplay"; -import { useUserStore } from "@/lib/stores/useUserStore"; -import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema"; +import { useUserStore } from "@/client/stores/useUserStore"; +import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; const UserHeader = ({ user }: { user: ApiMyProfileResponse }) => { const isLoading = useUserStore((state) => state.isLoading); diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx index 3724a1db..a473e92f 100644 --- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx +++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx @@ -1,4 +1,4 @@ -import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema"; +import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; import { Controller, useFormContext } from "react-hook-form"; import { ProfileRow } from "../common/ProfileRow"; import Dropdown from "@/components/commons/dropdown/DropdownBase"; diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx index 313ef135..d1c81900 100644 --- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx +++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx @@ -1,7 +1,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { ProfileRow } from "../common/ProfileRow"; import Dropdown from "@/components/commons/dropdown/DropdownBase"; -import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema"; +import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; export const GenderField = () => { const { control } = useFormContext(); diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx index d932e2a9..3fb8f72a 100644 --- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx +++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx @@ -1,4 +1,4 @@ -import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema"; +import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; import { Controller, useFormContext } from "react-hook-form"; import { ProfileRow } from "../common/ProfileRow"; import InputBase from "@/components/commons/input"; diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx index dd180ee5..2634f532 100644 --- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx +++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx @@ -6,9 +6,9 @@ import { ProfileForm } from "./common/ProfileForm"; import { ApiMyProfileResponse, ApiMyProfileResponseSchema, -} from "@/lib/hono/schemas/me.schema"; +} from "@/server/hono/schemas/me.schema"; import Form from "@/components/commons/form"; -import { useUpdateProfile } from "@/lib/hooks/query/useUserQuery"; +import { useUpdateProfile } from "@/client/hooks/query/useUserQuery"; export const PersonalInfo = (user: ApiMyProfileResponse) => { const { mutateAsync, isPending } = useUpdateProfile(); diff --git a/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx b/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx index d2300081..786ddfa2 100644 --- a/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx +++ b/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; interface ImageDisplayProps { imageUrl?: string | null; previewUrl?: string | null; diff --git a/src/components/domains/user/sections/ProfileSection/index.tsx b/src/components/domains/user/sections/ProfileSection/index.tsx index 203dbca5..398f6861 100644 --- a/src/components/domains/user/sections/ProfileSection/index.tsx +++ b/src/components/domains/user/sections/ProfileSection/index.tsx @@ -3,9 +3,9 @@ import { useState, useCallback, useEffect } from "react"; import { PersonalInfo } from "./components/PersonalInfo"; import { ProfileImage } from "./components/ProfileImage"; -import { useUploadProfileImage, useUserProfile } from "@/lib/hooks/query/useUserQuery"; +import { useUploadProfileImage, useUserProfile } from "@/client/hooks/query/useUserQuery"; import { ProfileSectionSkeleton } from "./components/ProfileSectionSkeleton"; -import { PROFILE_BUCKET_NAME } from "@/lib/constants/buckets"; +import { PROFILE_BUCKET_NAME } from "@/shared/constants/buckets"; export const ProfileSection = () => { const [selectedFile, setSelectedFile] = useState(null); diff --git a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts index 09b58bd4..6b347cd1 100644 --- a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts +++ b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useRecentPerfumesStore } from "@/lib/stores/useRecentPerfumesStore"; +import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; import { useFooterPagination } from "./useFooterPagination"; const PERFUMES_PER_VIEW = 5; diff --git a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts index 3c65b225..5fa68c71 100644 --- a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts +++ b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import { useRecentPostsStore } from "@/lib/stores/useRecentPostsStore"; +import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; import { useFooterPagination } from "./useFooterPagination"; -import type { ApiPostDetailResponse } from "@/lib/hono/schemas/community.schema"; +import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema"; interface PostForCard extends Omit { userId: string; diff --git a/src/components/domains/user/sections/RecentViewSection/index.tsx b/src/components/domains/user/sections/RecentViewSection/index.tsx index 5d41ab90..6c03cd34 100644 --- a/src/components/domains/user/sections/RecentViewSection/index.tsx +++ b/src/components/domains/user/sections/RecentViewSection/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMediaQuery } from "@/lib/hooks/useMediaQuery"; +import { useMediaQuery } from "@/client/hooks/useMediaQuery"; import { RecentPostsSection } from "./components/RecentPostsSection"; import { RecentPerfumesSection } from "./components/RecentPerfumesSection"; diff --git a/src/components/domains/user/sections/sections.type.ts b/src/components/domains/user/sections/sections.type.ts index ad30957f..fb354637 100644 --- a/src/components/domains/user/sections/sections.type.ts +++ b/src/components/domains/user/sections/sections.type.ts @@ -1,11 +1,11 @@ -import type { CommentResponse } from "@/lib/hono/schemas/comment.schema"; -import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema"; -import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema"; +import type { CommentResponse } from "@/server/hono/schemas/comment.schema"; +import type { ApiPostResponse } from "@/server/hono/schemas/community.schema"; +import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; import { ApiMyCollectionResponse, ApiMyProfileResponse, ApiMyBookmarkedPerfumesResponse, -} from "@/lib/hono/schemas/me.schema"; +} from "@/server/hono/schemas/me.schema"; export type BookmarkData = { perfumes: ApiMyBookmarkedPerfumesResponse[]; diff --git a/src/components/domains/user/tabs/MainTabs.tsx b/src/components/domains/user/tabs/MainTabs.tsx index b0aea3f4..eded9491 100644 --- a/src/components/domains/user/tabs/MainTabs.tsx +++ b/src/components/domains/user/tabs/MainTabs.tsx @@ -1,7 +1,7 @@ import React from "react"; import Link from "next/link"; import { getVisibleTabs } from "./tabs.helper"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useUserStore } from "@/client/stores/useUserStore"; import { MobileTabsAccordion } from "./MobileTabsAccordion"; import { FloatingActionButton } from "../components/FAB"; diff --git a/src/components/domains/user/tabs/MobileTabsAccordion.tsx b/src/components/domains/user/tabs/MobileTabsAccordion.tsx index f4c674c7..2cca6f9c 100644 --- a/src/components/domains/user/tabs/MobileTabsAccordion.tsx +++ b/src/components/domains/user/tabs/MobileTabsAccordion.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react"; import Link from "next/link"; import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import ICONS from "@/shared/constants/icons"; +import { useUserStore } from "@/client/stores/useUserStore"; import { TabItem } from "./tabs.type"; interface MobileTabsAccordionProps { diff --git a/src/components/modal/LoginModal.tsx b/src/components/modal/LoginModal.tsx index 92cad592..db1b898f 100644 --- a/src/components/modal/LoginModal.tsx +++ b/src/components/modal/LoginModal.tsx @@ -4,10 +4,10 @@ import { googleLogin, kakaoLogin, naverLogin, -} from "@/lib/database/action/login"; +} from "@/server/database/action/login"; import { ModalContainer } from "./ModalContainer"; import Image from "next/image"; -import IMAGES from "@/lib/constants/images"; +import IMAGES from "@/shared/constants/images"; import { SocialLogoContainer } from "../commons/socialLogo/SocialLogoContainer"; import { usePathname, useSearchParams } from "next/navigation"; diff --git a/src/components/modal/LoginModalProvider.tsx b/src/components/modal/LoginModalProvider.tsx index 0f4eac69..f7309d79 100644 --- a/src/components/modal/LoginModalProvider.tsx +++ b/src/components/modal/LoginModalProvider.tsx @@ -2,9 +2,9 @@ import { useState, useEffect, Suspense } from "react"; import { createPortal } from "react-dom"; -import { useModalStore } from "@/lib/stores/useModalStore"; +import { useModalStore } from "@/client/stores/useModalStore"; import { LoginModal } from "@/components/modal/LoginModal"; -import { MODAL_KEYS } from "@/lib/stores/useModalStore"; +import { MODAL_KEYS } from "@/client/stores/useModalStore"; export default function LoginModalProvider() { const { loginModal, closeModal } = useModalStore(); diff --git a/src/components/modal/ModalContainer.tsx b/src/components/modal/ModalContainer.tsx index 4e6e3a8b..997605ee 100644 --- a/src/components/modal/ModalContainer.tsx +++ b/src/components/modal/ModalContainer.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; import Image from "next/image"; -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; interface ModalProps { children: React.ReactNode; diff --git a/src/components/modal/draftRestoreModal/index.tsx b/src/components/modal/draftRestoreModal/index.tsx index 810706fc..5561f9d3 100644 --- a/src/components/modal/draftRestoreModal/index.tsx +++ b/src/components/modal/draftRestoreModal/index.tsx @@ -1,9 +1,9 @@ "use client"; import { ModalContainer } from "../ModalContainer"; -import { ApiDraftResponse } from "@/lib/hono/schemas/draft.schema"; +import { ApiDraftResponse } from "@/server/hono/schemas/draft.schema"; import { DraftType } from "@prisma/client"; -import { BOARD_OPTIONS } from "@/lib/constants/options"; +import { BOARD_OPTIONS } from "@/shared/constants/options"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import "dayjs/locale/ko"; diff --git a/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx b/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx index df39b17c..890c7a8c 100644 --- a/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx +++ b/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { X } from "lucide-react"; import { useSearchTag } from "./useSearchTag"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import Image from "next/image"; interface PerfumeTaggerProps { diff --git a/src/components/modal/photocollectionUploadModal/index.tsx b/src/components/modal/photocollectionUploadModal/index.tsx index 811b5a1c..cf2364ce 100644 --- a/src/components/modal/photocollectionUploadModal/index.tsx +++ b/src/components/modal/photocollectionUploadModal/index.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect, useState } from "react"; -import ModalPortal from "@/lib/portal/ModalPortal"; +import ModalPortal from "@/client/portal/ModalPortal"; import { ModalContainer } from "../ModalContainer"; import { PhotoDropzone } from "./PhotoDropzone"; import { PerfumeTagger } from "./PerfumeTagger"; -import { fileApi } from "@/lib/utils/api/files.api"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; -import { useCollectionMutation } from "@/lib/hooks/query/useCollectionQuery"; +import { fileApi } from "@/client/utils/api/files.api"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; +import { useCollectionMutation } from "@/client/hooks/query/useCollectionQuery"; interface PhotoCollectionUploadModalProps { isOpen: boolean; diff --git a/src/components/modal/photocollectionUploadModal/useSearchTag.ts b/src/components/modal/photocollectionUploadModal/useSearchTag.ts index 9c411a49..fb1a9dea 100644 --- a/src/components/modal/photocollectionUploadModal/useSearchTag.ts +++ b/src/components/modal/photocollectionUploadModal/useSearchTag.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { useCollectionPerfumeSearch } from "@/lib/hooks/query/useCollectionQuery"; -import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import { useCollectionPerfumeSearch } from "@/client/hooks/query/useCollectionQuery"; +import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; const initialState = { data: [] as ApiPerfumeSimpleResponse[] }; diff --git a/src/components/modal/reviewModal/components/button/ImageButton.tsx b/src/components/modal/reviewModal/components/button/ImageButton.tsx index bfad3af8..47a5b6a7 100644 --- a/src/components/modal/reviewModal/components/button/ImageButton.tsx +++ b/src/components/modal/reviewModal/components/button/ImageButton.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import { ReviewImageType } from "@/lib/types/review.types"; +import { ReviewImageType } from "@/shared/types/review.types"; interface IImageButton { image: ReviewImageType; diff --git a/src/components/modal/reviewModal/components/button/RadioButton.tsx b/src/components/modal/reviewModal/components/button/RadioButton.tsx index 0398cbad..eccf7112 100644 --- a/src/components/modal/reviewModal/components/button/RadioButton.tsx +++ b/src/components/modal/reviewModal/components/button/RadioButton.tsx @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; interface IRadioButton { diff --git a/src/components/modal/reviewModal/components/button/SelectButton.tsx b/src/components/modal/reviewModal/components/button/SelectButton.tsx index 12df0bd0..539b1315 100644 --- a/src/components/modal/reviewModal/components/button/SelectButton.tsx +++ b/src/components/modal/reviewModal/components/button/SelectButton.tsx @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import Image from "next/image"; interface ISelectButton { diff --git a/src/components/modal/reviewModal/components/sections/FeelingSection.tsx b/src/components/modal/reviewModal/components/sections/FeelingSection.tsx index 0e2ae221..935bffd7 100644 --- a/src/components/modal/reviewModal/components/sections/FeelingSection.tsx +++ b/src/components/modal/reviewModal/components/sections/FeelingSection.tsx @@ -3,7 +3,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { ImageButton } from "../button/ImageButton"; import { SubTitle } from "../SubTitle"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { useFormError } from "../../hooks/useFormError"; export const FeelingSection = () => { diff --git a/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx b/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx index 18e85ab0..bc94b254 100644 --- a/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx +++ b/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx @@ -3,7 +3,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { RadioButton } from "../button/RadioButton"; import { SubTitle } from "../SubTitle"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { useFormError } from "../../hooks/useFormError"; export const GenderToneSection = () => { diff --git a/src/components/modal/reviewModal/components/sections/LongevitySection.tsx b/src/components/modal/reviewModal/components/sections/LongevitySection.tsx index 9d701741..ec3e42c3 100644 --- a/src/components/modal/reviewModal/components/sections/LongevitySection.tsx +++ b/src/components/modal/reviewModal/components/sections/LongevitySection.tsx @@ -1,6 +1,6 @@ "use client"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { SelectButton } from "../button/SelectButton"; import { SubTitle } from "../SubTitle"; import { Controller, useFormContext } from "react-hook-form"; diff --git a/src/components/modal/reviewModal/components/sections/PriceSection.tsx b/src/components/modal/reviewModal/components/sections/PriceSection.tsx index 836edd99..b6043b91 100644 --- a/src/components/modal/reviewModal/components/sections/PriceSection.tsx +++ b/src/components/modal/reviewModal/components/sections/PriceSection.tsx @@ -1,7 +1,7 @@ "use client"; import { useFormContext, Controller } from "react-hook-form"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { SubTitle } from "../SubTitle"; import { SelectButton } from "../button/SelectButton"; import { useFormError } from "../../hooks/useFormError"; diff --git a/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx b/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx index cf967f37..818aed65 100644 --- a/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx +++ b/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { SubTitle } from "../SubTitle"; import { RadioButton } from "../button/RadioButton"; import { Controller } from "react-hook-form"; diff --git a/src/components/modal/reviewModal/components/sections/SillageSection.tsx b/src/components/modal/reviewModal/components/sections/SillageSection.tsx index fc3eb98d..f713c99e 100644 --- a/src/components/modal/reviewModal/components/sections/SillageSection.tsx +++ b/src/components/modal/reviewModal/components/sections/SillageSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { SelectButton } from "../button/SelectButton"; import { SubTitle } from "../SubTitle"; import { Controller, useFormContext } from "react-hook-form"; diff --git a/src/components/modal/reviewModal/components/sections/StatusSection.tsx b/src/components/modal/reviewModal/components/sections/StatusSection.tsx index 6567d84a..b5f8ca37 100644 --- a/src/components/modal/reviewModal/components/sections/StatusSection.tsx +++ b/src/components/modal/reviewModal/components/sections/StatusSection.tsx @@ -4,7 +4,7 @@ import { PerfumeUsageStatus } from "@prisma/client"; import { useFormContext, Controller } from "react-hook-form"; import { SelectButton } from "../button/SelectButton"; import { SubTitle } from "../SubTitle"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import type { CreateReviewClientInput } from "../../reviewSchema.client"; export const StatusSection = () => { diff --git a/src/components/modal/reviewModal/components/sections/TimeSection.tsx b/src/components/modal/reviewModal/components/sections/TimeSection.tsx index f60eb1a5..4ef9445b 100644 --- a/src/components/modal/reviewModal/components/sections/TimeSection.tsx +++ b/src/components/modal/reviewModal/components/sections/TimeSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import { SubTitle } from "../SubTitle"; import { RadioButton } from "../button/RadioButton"; import { Controller, useFormContext } from "react-hook-form"; diff --git a/src/components/modal/reviewModal/index.tsx b/src/components/modal/reviewModal/index.tsx index 69d34f16..4dc04507 100644 --- a/src/components/modal/reviewModal/index.tsx +++ b/src/components/modal/reviewModal/index.tsx @@ -14,7 +14,7 @@ import { DetailReviewSection, } from "./components/sections"; import { useReviewSubmit } from "./useReviewSubmit"; -import { useUserStore } from "@/lib/stores/useUserStore"; +import { useUserStore } from "@/client/stores/useUserStore"; import { CreateReviewClientSchema, type CreateReviewClientInput } from "./reviewSchema.client"; import { SubmitButton } from "./components/button/SubmitButton"; import { PerfumeUsageStatus } from "@prisma/client"; diff --git a/src/components/modal/reviewModal/useReviewModal.ts b/src/components/modal/reviewModal/useReviewModal.ts index 18cd2d4c..19bd86c0 100644 --- a/src/components/modal/reviewModal/useReviewModal.ts +++ b/src/components/modal/reviewModal/useReviewModal.ts @@ -1,5 +1,5 @@ import { useFormContext } from "react-hook-form"; -import { REVIEW_OPTIONS } from "@/lib/constants/review"; +import { REVIEW_OPTIONS } from "@/shared/constants/review"; import type { CreateReviewClientInput } from "./reviewSchema.client"; type ReviewOptionKey = keyof typeof REVIEW_OPTIONS; diff --git a/src/components/modal/reviewModal/useReviewSubmit.ts b/src/components/modal/reviewModal/useReviewSubmit.ts index 21e368d8..e8d500d6 100644 --- a/src/components/modal/reviewModal/useReviewSubmit.ts +++ b/src/components/modal/reviewModal/useReviewSubmit.ts @@ -2,8 +2,8 @@ import { useParams } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; -import { reviewApi } from "@/lib/utils/api/reviews.api"; -import { queryKeys } from "@/lib/utils/queryKeys"; +import { reviewApi } from "@/client/utils/api/reviews.api"; +import { queryKeys } from "@/client/utils/queryKeys"; import type { CreateReviewClientInput } from "./reviewSchema.client"; export const useReviewSubmit = (closeModal?: () => void) => { diff --git a/src/lib/database/action/login.ts b/src/server/database/action/login.ts similarity index 100% rename from src/lib/database/action/login.ts rename to src/server/database/action/login.ts diff --git a/src/lib/database/getSession.ts b/src/server/database/getSession.ts similarity index 100% rename from src/lib/database/getSession.ts rename to src/server/database/getSession.ts diff --git a/src/lib/hono/app.ts b/src/server/hono/app.ts similarity index 100% rename from src/lib/hono/app.ts rename to src/server/hono/app.ts diff --git a/src/lib/hono/middleware/auth.middleware.ts b/src/server/hono/middleware/auth.middleware.ts similarity index 100% rename from src/lib/hono/middleware/auth.middleware.ts rename to src/server/hono/middleware/auth.middleware.ts diff --git a/src/lib/hono/routes/v1/brand.handler.ts b/src/server/hono/routes/v1/brand.handler.ts similarity index 89% rename from src/lib/hono/routes/v1/brand.handler.ts rename to src/server/hono/routes/v1/brand.handler.ts index 5139481d..300c9f3d 100644 --- a/src/lib/hono/routes/v1/brand.handler.ts +++ b/src/server/hono/routes/v1/brand.handler.ts @@ -1,14 +1,14 @@ import { createRoute } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi"; -import * as BrandSchemas from "@/lib/hono/schemas/brand.schema"; -import * as BrandServices from "@/lib/hono/services/brand.service"; +import * as BrandSchemas from "@/server/hono/schemas/brand.schema"; +import * as BrandServices from "@/server/hono/services/brand.service"; import { apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/comment.handler.ts b/src/server/hono/routes/v1/comment.handler.ts similarity index 94% rename from src/lib/hono/routes/v1/comment.handler.ts rename to src/server/hono/routes/v1/comment.handler.ts index de02300f..55674c77 100644 --- a/src/lib/hono/routes/v1/comment.handler.ts +++ b/src/server/hono/routes/v1/comment.handler.ts @@ -1,6 +1,6 @@ import { createRoute } from "@hono/zod-openapi"; -import * as CommentSchemas from "@/lib/hono/schemas/comment.schema"; -import * as CommentServices from "@/lib/hono/services/comment.service"; +import * as CommentSchemas from "@/server/hono/schemas/comment.schema"; +import * as CommentServices from "@/server/hono/services/comment.service"; import { apiInternalError, apiSuccess, @@ -9,10 +9,10 @@ import { apiBadRequest, apiCreated, apiForbidden, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/community.handler.ts b/src/server/hono/routes/v1/community.handler.ts similarity index 96% rename from src/lib/hono/routes/v1/community.handler.ts rename to src/server/hono/routes/v1/community.handler.ts index 37297509..b01f2a27 100644 --- a/src/lib/hono/routes/v1/community.handler.ts +++ b/src/server/hono/routes/v1/community.handler.ts @@ -1,7 +1,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { PostCategory } from "@prisma/client"; -import * as CommunitySchemas from "@/lib/hono/schemas/community.schema"; -import * as CommunityServices from "@/lib/hono/services/community.service"; +import * as CommunitySchemas from "@/server/hono/schemas/community.schema"; +import * as CommunityServices from "@/server/hono/services/community.service"; import { apiBadRequest, apiNotFound, @@ -9,10 +9,10 @@ import { apiInternalError, apiCreated, apiForbidden, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/draft.handler.ts b/src/server/hono/routes/v1/draft.handler.ts similarity index 92% rename from src/lib/hono/routes/v1/draft.handler.ts rename to src/server/hono/routes/v1/draft.handler.ts index f91a81f3..a3c453f2 100644 --- a/src/lib/hono/routes/v1/draft.handler.ts +++ b/src/server/hono/routes/v1/draft.handler.ts @@ -1,7 +1,7 @@ import { createRoute } from "@hono/zod-openapi"; import { z } from "zod"; -import * as DraftSchemas from "@/lib/hono/schemas/draft.schema"; -import * as DraftServices from "@/lib/hono/services/draft.service"; +import * as DraftSchemas from "@/server/hono/schemas/draft.schema"; +import * as DraftServices from "@/server/hono/services/draft.service"; import { apiBadRequest, apiCreated, @@ -9,10 +9,10 @@ import { apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/file.handler.ts b/src/server/hono/routes/v1/file.handler.ts similarity index 79% rename from src/lib/hono/routes/v1/file.handler.ts rename to src/server/hono/routes/v1/file.handler.ts index 93126840..bfd46ae3 100644 --- a/src/lib/hono/routes/v1/file.handler.ts +++ b/src/server/hono/routes/v1/file.handler.ts @@ -1,14 +1,14 @@ import { createRoute } from "@hono/zod-openapi"; -import { UploadedImageInfoSchema } from "@/lib/hono/schemas/common.schema"; -import * as FileServices from "@/lib/hono/services/file.service"; +import { UploadedImageInfoSchema } from "@/server/hono/schemas/common.schema"; +import * as FileServices from "@/server/hono/services/file.service"; import { apiBadRequest, apiInternalError, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/filter.handler.ts b/src/server/hono/routes/v1/filter.handler.ts similarity index 84% rename from src/lib/hono/routes/v1/filter.handler.ts rename to src/server/hono/routes/v1/filter.handler.ts index b73c1e3f..dcd8092f 100644 --- a/src/lib/hono/routes/v1/filter.handler.ts +++ b/src/server/hono/routes/v1/filter.handler.ts @@ -1,12 +1,12 @@ import { createRoute } from "@hono/zod-openapi"; -import * as FilterSchemas from "@/lib/hono/schemas/filter.schema"; +import * as FilterSchemas from "@/server/hono/schemas/filter.schema"; import { getAvailableFiltersService, getAvailableFiltersTotalService, -} from "@/lib/hono/services/filter.service"; -import { apiInternalError, apiSuccess } from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; +} from "@/server/hono/services/filter.service"; +import { apiInternalError, apiSuccess } from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/me.handler.ts b/src/server/hono/routes/v1/me.handler.ts similarity index 97% rename from src/lib/hono/routes/v1/me.handler.ts rename to src/server/hono/routes/v1/me.handler.ts index 272df72b..421875fa 100644 --- a/src/lib/hono/routes/v1/me.handler.ts +++ b/src/server/hono/routes/v1/me.handler.ts @@ -1,16 +1,16 @@ import { createRoute, z } from "@hono/zod-openapi"; -import * as MeSchemas from "@/lib/hono/schemas/me.schema"; -import * as MeServices from "@/lib/hono/services/me.service"; +import * as MeSchemas from "@/server/hono/schemas/me.schema"; +import * as MeServices from "@/server/hono/services/me.service"; import { apiBadRequest, apiForbidden, apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/perfume.handler.ts b/src/server/hono/routes/v1/perfume.handler.ts similarity index 93% rename from src/lib/hono/routes/v1/perfume.handler.ts rename to src/server/hono/routes/v1/perfume.handler.ts index e5c40997..6f3798a1 100644 --- a/src/lib/hono/routes/v1/perfume.handler.ts +++ b/src/server/hono/routes/v1/perfume.handler.ts @@ -1,19 +1,19 @@ import { createRoute, z } from "@hono/zod-openapi"; -import { ApiPostResponseSchema } from "@/lib/hono/schemas/community.schema"; -import * as CommonSchemas from "@/lib/hono/schemas/common.schema"; -import * as PerfumeSchemas from "@/lib/hono/schemas/perfume.schema"; +import { ApiPostResponseSchema } from "@/server/hono/schemas/community.schema"; +import * as CommonSchemas from "@/server/hono/schemas/common.schema"; +import * as PerfumeSchemas from "@/server/hono/schemas/perfume.schema"; -import * as PerfumeServices from "@/lib/hono/services/perfume.service"; +import * as PerfumeServices from "@/server/hono/services/perfume.service"; import { apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/point.handler.ts b/src/server/hono/routes/v1/point.handler.ts similarity index 92% rename from src/lib/hono/routes/v1/point.handler.ts rename to src/server/hono/routes/v1/point.handler.ts index 67015086..030c2dbb 100644 --- a/src/lib/hono/routes/v1/point.handler.ts +++ b/src/server/hono/routes/v1/point.handler.ts @@ -1,15 +1,15 @@ import { createRoute } from "@hono/zod-openapi"; -import * as PointSchemas from "@/lib/hono/schemas/point.schema"; -import * as PointServices from "@/lib/hono/services/point.service"; +import * as PointSchemas from "@/server/hono/schemas/point.schema"; +import * as PointServices from "@/server/hono/services/point.service"; import { apiBadRequest, apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/review.handler.ts b/src/server/hono/routes/v1/review.handler.ts similarity index 92% rename from src/lib/hono/routes/v1/review.handler.ts rename to src/server/hono/routes/v1/review.handler.ts index aac748da..2925c05a 100644 --- a/src/lib/hono/routes/v1/review.handler.ts +++ b/src/server/hono/routes/v1/review.handler.ts @@ -1,9 +1,9 @@ import { createRoute, z } from "@hono/zod-openapi"; -import * as CommonSchemas from "@/lib/hono/schemas/common.schema"; -import * as ReviewSchemas from "@/lib/hono/schemas/review.schema"; +import * as CommonSchemas from "@/server/hono/schemas/common.schema"; +import * as ReviewSchemas from "@/server/hono/schemas/review.schema"; -import * as ReviewServices from "@/lib/hono/services/review.service"; +import * as ReviewServices from "@/server/hono/services/review.service"; import { apiBadRequest, @@ -12,10 +12,10 @@ import { apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; -import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; +import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/search.handler.ts b/src/server/hono/routes/v1/search.handler.ts similarity index 83% rename from src/lib/hono/routes/v1/search.handler.ts rename to src/server/hono/routes/v1/search.handler.ts index 3647b9eb..dd34cb90 100644 --- a/src/lib/hono/routes/v1/search.handler.ts +++ b/src/server/hono/routes/v1/search.handler.ts @@ -1,9 +1,9 @@ import { createRoute } from "@hono/zod-openapi"; -import * as SearchSchemas from "@/lib/hono/schemas/search.schema"; -import { searchPerfumesService } from "@/lib/hono/services/search.service"; -import { apiInternalError, apiSuccess } from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; +import * as SearchSchemas from "@/server/hono/schemas/search.schema"; +import { searchPerfumesService } from "@/server/hono/services/search.service"; +import { apiInternalError, apiSuccess } from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; const routers = createDomainRouters(); diff --git a/src/lib/hono/routes/v1/user.handler.ts b/src/server/hono/routes/v1/user.handler.ts similarity index 86% rename from src/lib/hono/routes/v1/user.handler.ts rename to src/server/hono/routes/v1/user.handler.ts index f61ba03d..388e5dca 100644 --- a/src/lib/hono/routes/v1/user.handler.ts +++ b/src/server/hono/routes/v1/user.handler.ts @@ -1,15 +1,15 @@ import { createRoute, z } from "@hono/zod-openapi"; -import { ApiMyCollectionResponseSchema } from "@/lib/hono/schemas/me.schema"; -import { ApiPerfumeSimpleResponseSchema } from "@/lib/hono/schemas/perfume.schema"; -import * as UserSchemas from "@/lib/hono/schemas/user.schema"; -import * as UserServices from "@/lib/hono/services/user.service"; +import { ApiMyCollectionResponseSchema } from "@/server/hono/schemas/me.schema"; +import { ApiPerfumeSimpleResponseSchema } from "@/server/hono/schemas/perfume.schema"; +import * as UserSchemas from "@/server/hono/schemas/user.schema"; +import * as UserServices from "@/server/hono/services/user.service"; import { apiInternalError, apiNotFound, apiSuccess, -} from "@/lib/hono/utils/api.utils"; -import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema"; -import { createDomainRouters } from "@/lib/hono/utils/router"; +} from "@/server/hono/utils/api.utils"; +import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; +import { createDomainRouters } from "@/server/hono/utils/router"; const routers = createDomainRouters(); diff --git a/src/lib/hono/schemas/brand.schema.ts b/src/server/hono/schemas/brand.schema.ts similarity index 100% rename from src/lib/hono/schemas/brand.schema.ts rename to src/server/hono/schemas/brand.schema.ts diff --git a/src/lib/hono/schemas/comment.schema.ts b/src/server/hono/schemas/comment.schema.ts similarity index 100% rename from src/lib/hono/schemas/comment.schema.ts rename to src/server/hono/schemas/comment.schema.ts diff --git a/src/lib/hono/schemas/common.schema.ts b/src/server/hono/schemas/common.schema.ts similarity index 100% rename from src/lib/hono/schemas/common.schema.ts rename to src/server/hono/schemas/common.schema.ts diff --git a/src/lib/hono/schemas/community.schema.ts b/src/server/hono/schemas/community.schema.ts similarity index 100% rename from src/lib/hono/schemas/community.schema.ts rename to src/server/hono/schemas/community.schema.ts diff --git a/src/lib/hono/schemas/draft.schema.ts b/src/server/hono/schemas/draft.schema.ts similarity index 100% rename from src/lib/hono/schemas/draft.schema.ts rename to src/server/hono/schemas/draft.schema.ts diff --git a/src/lib/hono/schemas/filter.schema.ts b/src/server/hono/schemas/filter.schema.ts similarity index 96% rename from src/lib/hono/schemas/filter.schema.ts rename to src/server/hono/schemas/filter.schema.ts index 78cb01fe..d6442bce 100644 --- a/src/lib/hono/schemas/filter.schema.ts +++ b/src/server/hono/schemas/filter.schema.ts @@ -1,5 +1,5 @@ import { z } from "@hono/zod-openapi"; -import { SearchPostBodySchema } from "@/lib/hono/schemas/search.schema"; +import { SearchPostBodySchema } from "@/server/hono/schemas/search.schema"; /** * 데이터베이스 필터 옵션 스키마 diff --git a/src/lib/hono/schemas/me.schema.ts b/src/server/hono/schemas/me.schema.ts similarity index 100% rename from src/lib/hono/schemas/me.schema.ts rename to src/server/hono/schemas/me.schema.ts diff --git a/src/lib/hono/schemas/perfume.schema.ts b/src/server/hono/schemas/perfume.schema.ts similarity index 100% rename from src/lib/hono/schemas/perfume.schema.ts rename to src/server/hono/schemas/perfume.schema.ts diff --git a/src/lib/hono/schemas/point.schema.ts b/src/server/hono/schemas/point.schema.ts similarity index 100% rename from src/lib/hono/schemas/point.schema.ts rename to src/server/hono/schemas/point.schema.ts diff --git a/src/lib/hono/schemas/review.schema.ts b/src/server/hono/schemas/review.schema.ts similarity index 100% rename from src/lib/hono/schemas/review.schema.ts rename to src/server/hono/schemas/review.schema.ts diff --git a/src/lib/hono/schemas/search.schema.ts b/src/server/hono/schemas/search.schema.ts similarity index 100% rename from src/lib/hono/schemas/search.schema.ts rename to src/server/hono/schemas/search.schema.ts diff --git a/src/lib/hono/schemas/user.schema.ts b/src/server/hono/schemas/user.schema.ts similarity index 100% rename from src/lib/hono/schemas/user.schema.ts rename to src/server/hono/schemas/user.schema.ts diff --git a/src/lib/hono/services/__tests__/brand.service.test.ts b/src/server/hono/services/__tests__/brand.service.test.ts similarity index 98% rename from src/lib/hono/services/__tests__/brand.service.test.ts rename to src/server/hono/services/__tests__/brand.service.test.ts index f34550bf..2e143399 100644 --- a/src/lib/hono/services/__tests__/brand.service.test.ts +++ b/src/server/hono/services/__tests__/brand.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { getAllBrandsService, getBrandByIdService, @@ -22,7 +22,7 @@ import { */ // Mock Prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { brand: { findMany: vi.fn(), diff --git a/src/lib/hono/services/__tests__/comment.service.test.ts b/src/server/hono/services/__tests__/comment.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/comment.service.test.ts rename to src/server/hono/services/__tests__/comment.service.test.ts index e0273e7d..719836c3 100644 --- a/src/lib/hono/services/__tests__/comment.service.test.ts +++ b/src/server/hono/services/__tests__/comment.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import type { PrismaClient } from "@prisma/client"; import { createCommentService, @@ -27,7 +27,7 @@ type PrismaTransactionClient = Omit< */ // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/__tests__/community.service.test.ts b/src/server/hono/services/__tests__/community.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/community.service.test.ts rename to src/server/hono/services/__tests__/community.service.test.ts index 50ae6540..d3733766 100644 --- a/src/lib/hono/services/__tests__/community.service.test.ts +++ b/src/server/hono/services/__tests__/community.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import type { PrismaClient, PostCategory } from "@prisma/client"; import { createPostService, @@ -29,7 +29,7 @@ type PrismaTransactionClient = Omit< */ // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/__tests__/consecutiveLogin.service.test.ts b/src/server/hono/services/__tests__/consecutiveLogin.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/consecutiveLogin.service.test.ts rename to src/server/hono/services/__tests__/consecutiveLogin.service.test.ts index da2a8469..bef975ce 100644 --- a/src/lib/hono/services/__tests__/consecutiveLogin.service.test.ts +++ b/src/server/hono/services/__tests__/consecutiveLogin.service.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { processConsecutiveLoginService } from "../point.service"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import dayjs from "dayjs"; /** @@ -34,7 +34,7 @@ const getTestDates = (baseDate = "2025-11-15") => ({ }); // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/__tests__/earnPoints.service.test.ts b/src/server/hono/services/__tests__/earnPoints.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/earnPoints.service.test.ts rename to src/server/hono/services/__tests__/earnPoints.service.test.ts index f7dfbce8..ecec22cb 100644 --- a/src/lib/hono/services/__tests__/earnPoints.service.test.ts +++ b/src/server/hono/services/__tests__/earnPoints.service.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { earnPointsService } from "../point.service"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { PointActivityType } from "@prisma/client"; /** @@ -29,7 +29,7 @@ interface PointHistoryCreateArgs { } // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/__tests__/file.service.test.ts b/src/server/hono/services/__tests__/file.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/file.service.test.ts rename to src/server/hono/services/__tests__/file.service.test.ts index c8d95303..2055db67 100644 --- a/src/lib/hono/services/__tests__/file.service.test.ts +++ b/src/server/hono/services/__tests__/file.service.test.ts @@ -23,7 +23,7 @@ const mockUpload = vi.fn(); const mockRemove = vi.fn(); const mockGetPublicUrl = vi.fn(); -vi.mock("@/lib/supabase/server", () => ({ +vi.mock("@/server/supabase/server", () => ({ supabaseAdmin: { storage: { from: vi.fn(() => ({ diff --git a/src/lib/hono/services/__tests__/filter.service.test.ts b/src/server/hono/services/__tests__/filter.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/filter.service.test.ts rename to src/server/hono/services/__tests__/filter.service.test.ts index 2d401a14..dbb8d58c 100644 --- a/src/lib/hono/services/__tests__/filter.service.test.ts +++ b/src/server/hono/services/__tests__/filter.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { supabase } from "@/lib/supabase/init"; +import { supabase } from "@/server/supabase/init"; import { getAvailableFiltersService, getAvailableFiltersTotalService, @@ -25,7 +25,7 @@ import { getTestData } from "./helpers/filter.test.helpers"; */ // Mock supabase -vi.mock("@/lib/supabase/init", () => ({ +vi.mock("@/server/supabase/init", () => ({ supabase: { rpc: vi.fn(), }, diff --git a/src/lib/hono/services/__tests__/helpers/comment.test.helpers.ts b/src/server/hono/services/__tests__/helpers/comment.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/comment.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/comment.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/helpers/community.test.helpers.ts b/src/server/hono/services/__tests__/helpers/community.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/community.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/community.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/helpers/filter.test.helpers.ts b/src/server/hono/services/__tests__/helpers/filter.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/filter.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/filter.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/helpers/me.test.helpers.ts b/src/server/hono/services/__tests__/helpers/me.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/me.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/me.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/helpers/perfume.test.helpers.ts b/src/server/hono/services/__tests__/helpers/perfume.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/perfume.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/perfume.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/helpers/search.test.helpers.ts b/src/server/hono/services/__tests__/helpers/search.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/search.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/search.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/helpers/user.test.helpers.ts b/src/server/hono/services/__tests__/helpers/user.test.helpers.ts similarity index 100% rename from src/lib/hono/services/__tests__/helpers/user.test.helpers.ts rename to src/server/hono/services/__tests__/helpers/user.test.helpers.ts diff --git a/src/lib/hono/services/__tests__/level.utils.test.ts b/src/server/hono/services/__tests__/level.utils.test.ts similarity index 98% rename from src/lib/hono/services/__tests__/level.utils.test.ts rename to src/server/hono/services/__tests__/level.utils.test.ts index ae08d8c1..e61d9892 100644 --- a/src/lib/hono/services/__tests__/level.utils.test.ts +++ b/src/server/hono/services/__tests__/level.utils.test.ts @@ -5,7 +5,7 @@ import { getLevelProgress, getPointsForLevel, POINTS_PER_LEVEL, -} from "../../../utils/level.utils"; +} from "@/shared/utils/level.utils"; /** * 레벨 유틸리티 함수 테스트 diff --git a/src/lib/hono/services/__tests__/me.service.test.ts b/src/server/hono/services/__tests__/me.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/me.service.test.ts rename to src/server/hono/services/__tests__/me.service.test.ts index ae8bed72..7a6b3879 100644 --- a/src/lib/hono/services/__tests__/me.service.test.ts +++ b/src/server/hono/services/__tests__/me.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { getMyProfileService, updateMyProfileService, @@ -41,7 +41,7 @@ import { getTestData } from "./helpers/me.test.helpers"; */ // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/__tests__/perfume.service.test.ts b/src/server/hono/services/__tests__/perfume.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/perfume.service.test.ts rename to src/server/hono/services/__tests__/perfume.service.test.ts index ddcd9b01..6c931672 100644 --- a/src/lib/hono/services/__tests__/perfume.service.test.ts +++ b/src/server/hono/services/__tests__/perfume.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { getPerfumesListService, getPerfumesListByThemeService, @@ -32,7 +32,7 @@ import { getTestData } from "./helpers/perfume.test.helpers"; */ // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { perfume: { findMany: vi.fn(), diff --git a/src/lib/hono/services/__tests__/point.service.test.ts b/src/server/hono/services/__tests__/point.service.test.ts similarity index 100% rename from src/lib/hono/services/__tests__/point.service.test.ts rename to src/server/hono/services/__tests__/point.service.test.ts diff --git a/src/lib/hono/services/__tests__/review.service.test.ts b/src/server/hono/services/__tests__/review.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/review.service.test.ts rename to src/server/hono/services/__tests__/review.service.test.ts index 4be8cc1b..ea95bfa4 100644 --- a/src/lib/hono/services/__tests__/review.service.test.ts +++ b/src/server/hono/services/__tests__/review.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import type { PrismaClient } from "@prisma/client"; import { createReviewService, @@ -159,7 +159,7 @@ const getTestData = () => { }; // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/__tests__/search.service.test.ts b/src/server/hono/services/__tests__/search.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/search.service.test.ts rename to src/server/hono/services/__tests__/search.service.test.ts index cf28b714..47dc495f 100644 --- a/src/lib/hono/services/__tests__/search.service.test.ts +++ b/src/server/hono/services/__tests__/search.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { supabase } from "@/lib/supabase/init"; +import { supabase } from "@/server/supabase/init"; import { searchPerfumesService } from "../search.service"; import type { SearchGetQuery, @@ -28,7 +28,7 @@ import { getTestData } from "./helpers/search.test.helpers"; */ // Mock supabase -vi.mock("@/lib/supabase/init", () => ({ +vi.mock("@/server/supabase/init", () => ({ supabase: { rpc: vi.fn(), }, diff --git a/src/lib/hono/services/__tests__/user.service.test.ts b/src/server/hono/services/__tests__/user.service.test.ts similarity index 99% rename from src/lib/hono/services/__tests__/user.service.test.ts rename to src/server/hono/services/__tests__/user.service.test.ts index 790b303f..7be8a441 100644 --- a/src/lib/hono/services/__tests__/user.service.test.ts +++ b/src/server/hono/services/__tests__/user.service.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { getUserProfileService, getUserPostsService, @@ -26,7 +26,7 @@ import { getTestData } from "./helpers/user.test.helpers"; */ // Mock prisma -vi.mock("@/lib/prisma", () => ({ +vi.mock("@/server/prisma", () => ({ prisma: { user: { findUnique: vi.fn(), diff --git a/src/lib/hono/services/brand.service.ts b/src/server/hono/services/brand.service.ts similarity index 98% rename from src/lib/hono/services/brand.service.ts rename to src/server/hono/services/brand.service.ts index 908b791d..6d36bf61 100644 --- a/src/lib/hono/services/brand.service.ts +++ b/src/server/hono/services/brand.service.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { serviceInternalError, serviceNotFound, diff --git a/src/lib/hono/services/comment.service.ts b/src/server/hono/services/comment.service.ts similarity index 99% rename from src/lib/hono/services/comment.service.ts rename to src/server/hono/services/comment.service.ts index 3728e956..59794c11 100644 --- a/src/lib/hono/services/comment.service.ts +++ b/src/server/hono/services/comment.service.ts @@ -1,5 +1,5 @@ import { Prisma, PointActivityType } from "@prisma/client"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { checkResourceExists, serviceBadRequest, diff --git a/src/lib/hono/services/community.service.ts b/src/server/hono/services/community.service.ts similarity index 99% rename from src/lib/hono/services/community.service.ts rename to src/server/hono/services/community.service.ts index 86898b07..3690fe44 100644 --- a/src/lib/hono/services/community.service.ts +++ b/src/server/hono/services/community.service.ts @@ -1,5 +1,5 @@ import { PostCategory, Prisma, PointActivityType } from "@prisma/client"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import type { ApiPostDetailCategoryPostResponse, ApiPostDetailResponse, diff --git a/src/lib/hono/services/draft.service.ts b/src/server/hono/services/draft.service.ts similarity index 94% rename from src/lib/hono/services/draft.service.ts rename to src/server/hono/services/draft.service.ts index ee819ca4..0847a4a1 100644 --- a/src/lib/hono/services/draft.service.ts +++ b/src/server/hono/services/draft.service.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { checkResourceExists, serviceBadRequest, @@ -244,20 +244,11 @@ export const listDraftsService = async ( // perfumeIds 순서대로 향수 정보 조회 const perfumes = draft.perfumeIds .map((id) => perfumeMap.get(id)) - .filter((p) => p !== undefined); + .filter((p): p is NonNullable => p !== undefined); return { - id: draft.id, - userId: draft.userId, - type: draft.type, - title: draft.title, - content: draft.content, - contentText: draft.contentText, - category: draft.category, - thumbnailUrl: draft.thumbnailUrl, - perfumeIds: draft.perfumeIds, - perfumes: perfumes, - postId: draft.postId, + ...draft, + perfumes, createdAt: draft.createdAt.toISOString(), updatedAt: draft.updatedAt.toISOString(), }; diff --git a/src/lib/hono/services/file.service.ts b/src/server/hono/services/file.service.ts similarity index 98% rename from src/lib/hono/services/file.service.ts rename to src/server/hono/services/file.service.ts index 6a5d02ff..c6274b2d 100644 --- a/src/lib/hono/services/file.service.ts +++ b/src/server/hono/services/file.service.ts @@ -6,7 +6,7 @@ import { serviceSuccess, } from "../utils/service.utils"; import { getImageFormat } from "../utils/service.utils"; -import { supabaseAdmin } from "@/lib/supabase/server"; +import { supabaseAdmin } from "@/server/supabase/server"; export interface UploadedImageInfo { imageUrl: string; diff --git a/src/lib/hono/services/filter.service.ts b/src/server/hono/services/filter.service.ts similarity index 98% rename from src/lib/hono/services/filter.service.ts rename to src/server/hono/services/filter.service.ts index c090c010..a45f1b48 100644 --- a/src/lib/hono/services/filter.service.ts +++ b/src/server/hono/services/filter.service.ts @@ -1,4 +1,4 @@ -import { supabase } from "@/lib/supabase/init"; +import { supabase } from "@/server/supabase/init"; import { serviceInternalError, ServiceResult, diff --git a/src/lib/hono/services/me.service.ts b/src/server/hono/services/me.service.ts similarity index 98% rename from src/lib/hono/services/me.service.ts rename to src/server/hono/services/me.service.ts index e2f273f5..bcee720c 100644 --- a/src/lib/hono/services/me.service.ts +++ b/src/server/hono/services/me.service.ts @@ -1,5 +1,5 @@ import { ImageFormat } from "@prisma/client"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { ServiceResult, serviceSuccess, @@ -15,7 +15,7 @@ import { import { COLLECTION_BUCKET_NAME, PROFILE_BUCKET_NAME, -} from "@/lib/constants/buckets"; +} from "@/shared/constants/buckets"; import { PaginatedResponse } from "../schemas/common.schema"; import { deleteImageByUrl } from "./file.service"; import { @@ -30,7 +30,7 @@ import { myCommentInclude, reviewIncludeArgs, } from "../utils/prisma.utils"; -import { calculateLevel, getPointsForNextLevel } from "../../utils/level.utils"; +import { calculateLevel, getPointsForNextLevel } from "@/shared/utils/level.utils"; const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; diff --git a/src/lib/hono/services/perfume.service.ts b/src/server/hono/services/perfume.service.ts similarity index 99% rename from src/lib/hono/services/perfume.service.ts rename to src/server/hono/services/perfume.service.ts index c33054df..314818b5 100644 --- a/src/lib/hono/services/perfume.service.ts +++ b/src/server/hono/services/perfume.service.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { serviceInternalError, serviceNotFound, diff --git a/src/lib/hono/services/point.service.ts b/src/server/hono/services/point.service.ts similarity index 98% rename from src/lib/hono/services/point.service.ts rename to src/server/hono/services/point.service.ts index 6f592432..18b3bacf 100644 --- a/src/lib/hono/services/point.service.ts +++ b/src/server/hono/services/point.service.ts @@ -1,5 +1,5 @@ import { PointActivityType } from "@prisma/client"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { serviceInternalError, serviceNotFound, @@ -7,13 +7,13 @@ import { serviceSuccess, serviceError, } from "../utils/service.utils"; -import { POINT_POLICY, POINT_DESCRIPTIONS } from "@/lib/constants/point"; +import { POINT_POLICY, POINT_DESCRIPTIONS } from "@/shared/constants/point"; import type { PointEarnData, PointHistoryData, UserPointsData, PointStatisticsData, -} from "@/lib/types/point.types"; +} from "@/shared/types/point.types"; import dayjs from "dayjs"; /** diff --git a/src/lib/hono/services/review.service.ts b/src/server/hono/services/review.service.ts similarity index 99% rename from src/lib/hono/services/review.service.ts rename to src/server/hono/services/review.service.ts index 080613f6..1fe2574f 100644 --- a/src/lib/hono/services/review.service.ts +++ b/src/server/hono/services/review.service.ts @@ -1,5 +1,5 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import type { CreateReviewInput, UpdateReviewInput, diff --git a/src/lib/hono/services/search.service.ts b/src/server/hono/services/search.service.ts similarity index 98% rename from src/lib/hono/services/search.service.ts rename to src/server/hono/services/search.service.ts index 16b46e60..c8a14cd0 100644 --- a/src/lib/hono/services/search.service.ts +++ b/src/server/hono/services/search.service.ts @@ -1,4 +1,4 @@ -import { supabase } from "@/lib/supabase/init"; +import { supabase } from "@/server/supabase/init"; import type { SearchGetQuery, SearchPostBody, diff --git a/src/lib/hono/services/user.service.ts b/src/server/hono/services/user.service.ts similarity index 96% rename from src/lib/hono/services/user.service.ts rename to src/server/hono/services/user.service.ts index efbdfe3b..6cb43656 100644 --- a/src/lib/hono/services/user.service.ts +++ b/src/server/hono/services/user.service.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { serviceInternalError, serviceNotFound, @@ -12,7 +12,7 @@ import { perfumeBaseInclude, UserCollectionWithRelations, } from "../utils/prisma.utils"; -import { calculateLevel } from "../../utils/level.utils"; +import { calculateLevel } from "@/shared/utils/level.utils"; import { ApiUserProfileResponse } from "../schemas/user.schema"; export async function getUserProfileService( diff --git a/src/lib/hono/utils/__tests__/level.utils.test.ts b/src/server/hono/utils/__tests__/level.utils.test.ts similarity index 98% rename from src/lib/hono/utils/__tests__/level.utils.test.ts rename to src/server/hono/utils/__tests__/level.utils.test.ts index ae08d8c1..e61d9892 100644 --- a/src/lib/hono/utils/__tests__/level.utils.test.ts +++ b/src/server/hono/utils/__tests__/level.utils.test.ts @@ -5,7 +5,7 @@ import { getLevelProgress, getPointsForLevel, POINTS_PER_LEVEL, -} from "../../../utils/level.utils"; +} from "@/shared/utils/level.utils"; /** * 레벨 유틸리티 함수 테스트 diff --git a/src/lib/hono/utils/api.utils.ts b/src/server/hono/utils/api.utils.ts similarity index 100% rename from src/lib/hono/utils/api.utils.ts rename to src/server/hono/utils/api.utils.ts diff --git a/src/lib/hono/utils/http.constants.ts b/src/server/hono/utils/http.constants.ts similarity index 100% rename from src/lib/hono/utils/http.constants.ts rename to src/server/hono/utils/http.constants.ts diff --git a/src/lib/hono/utils/openapi.schema.ts b/src/server/hono/utils/openapi.schema.ts similarity index 95% rename from src/lib/hono/utils/openapi.schema.ts rename to src/server/hono/utils/openapi.schema.ts index fbbb7597..20a56593 100644 --- a/src/lib/hono/utils/openapi.schema.ts +++ b/src/server/hono/utils/openapi.schema.ts @@ -1,5 +1,5 @@ import { z } from "@hono/zod-openapi"; -import * as CommonSchemas from "@/lib/hono/schemas/common.schema"; +import * as CommonSchemas from "@/server/hono/schemas/common.schema"; const getErrorResponseConfig = { "400": CommonSchemas.BadRequestResponse, diff --git a/src/lib/hono/utils/pagination.utils.ts b/src/server/hono/utils/pagination.utils.ts similarity index 100% rename from src/lib/hono/utils/pagination.utils.ts rename to src/server/hono/utils/pagination.utils.ts diff --git a/src/lib/hono/utils/prisma.utils.ts b/src/server/hono/utils/prisma.utils.ts similarity index 100% rename from src/lib/hono/utils/prisma.utils.ts rename to src/server/hono/utils/prisma.utils.ts diff --git a/src/lib/hono/utils/router.ts b/src/server/hono/utils/router.ts similarity index 100% rename from src/lib/hono/utils/router.ts rename to src/server/hono/utils/router.ts diff --git a/src/lib/hono/utils/sanitize.utils.ts b/src/server/hono/utils/sanitize.utils.ts similarity index 100% rename from src/lib/hono/utils/sanitize.utils.ts rename to src/server/hono/utils/sanitize.utils.ts diff --git a/src/lib/hono/utils/service.utils.ts b/src/server/hono/utils/service.utils.ts similarity index 99% rename from src/lib/hono/utils/service.utils.ts rename to src/server/hono/utils/service.utils.ts index ef954152..e1bf4087 100644 --- a/src/lib/hono/utils/service.utils.ts +++ b/src/server/hono/utils/service.utils.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/lib/prisma"; +import { prisma } from "@/server/prisma"; import { z } from "zod"; import { Context } from "hono"; import { ImageFormat } from "@prisma/client"; diff --git a/src/lib/prisma/index.ts b/src/server/prisma/index.ts similarity index 100% rename from src/lib/prisma/index.ts rename to src/server/prisma/index.ts diff --git a/src/lib/supabase/init.ts b/src/server/supabase/init.ts similarity index 100% rename from src/lib/supabase/init.ts rename to src/server/supabase/init.ts diff --git a/src/lib/supabase/server.ts b/src/server/supabase/server.ts similarity index 100% rename from src/lib/supabase/server.ts rename to src/server/supabase/server.ts diff --git a/src/lib/constants/accords.ts b/src/shared/constants/accords.ts similarity index 100% rename from src/lib/constants/accords.ts rename to src/shared/constants/accords.ts diff --git a/src/lib/constants/buckets.ts b/src/shared/constants/buckets.ts similarity index 100% rename from src/lib/constants/buckets.ts rename to src/shared/constants/buckets.ts diff --git a/src/lib/constants/colors.ts b/src/shared/constants/colors.ts similarity index 100% rename from src/lib/constants/colors.ts rename to src/shared/constants/colors.ts diff --git a/src/lib/constants/communityBoard.ts b/src/shared/constants/communityBoard.ts similarity index 100% rename from src/lib/constants/communityBoard.ts rename to src/shared/constants/communityBoard.ts diff --git a/src/lib/constants/icons/index.ts b/src/shared/constants/icons/index.ts similarity index 100% rename from src/lib/constants/icons/index.ts rename to src/shared/constants/icons/index.ts diff --git a/src/lib/constants/icons/navBar.ts b/src/shared/constants/icons/navBar.ts similarity index 100% rename from src/lib/constants/icons/navBar.ts rename to src/shared/constants/icons/navBar.ts diff --git a/src/lib/constants/images.ts b/src/shared/constants/images.ts similarity index 100% rename from src/lib/constants/images.ts rename to src/shared/constants/images.ts diff --git a/src/lib/constants/main.ts b/src/shared/constants/main.ts similarity index 92% rename from src/lib/constants/main.ts rename to src/shared/constants/main.ts index aa02022c..e63955a4 100644 --- a/src/lib/constants/main.ts +++ b/src/shared/constants/main.ts @@ -1,5 +1,5 @@ import type { StaticImageData } from "next/image"; -import IMAGES from "@/lib/constants/images"; +import IMAGES from "@/shared/constants/images"; interface MainButton { link: string; diff --git a/src/lib/constants/options.ts b/src/shared/constants/options.ts similarity index 100% rename from src/lib/constants/options.ts rename to src/shared/constants/options.ts diff --git a/src/lib/constants/point.ts b/src/shared/constants/point.ts similarity index 100% rename from src/lib/constants/point.ts rename to src/shared/constants/point.ts diff --git a/src/lib/constants/post.ts b/src/shared/constants/post.ts similarity index 100% rename from src/lib/constants/post.ts rename to src/shared/constants/post.ts diff --git a/src/lib/constants/review.ts b/src/shared/constants/review.ts similarity index 99% rename from src/lib/constants/review.ts rename to src/shared/constants/review.ts index b06e2490..a6a0b306 100644 --- a/src/lib/constants/review.ts +++ b/src/shared/constants/review.ts @@ -1,4 +1,4 @@ -import ICONS from "@/lib/constants/icons"; +import ICONS from "@/shared/constants/icons"; import { ReviewImageType, ReviewCategory, diff --git a/src/lib/mocks/communityCard.ts b/src/shared/mocks/communityCard.ts similarity index 99% rename from src/lib/mocks/communityCard.ts rename to src/shared/mocks/communityCard.ts index 9fe1e087..d1eedfbf 100644 --- a/src/lib/mocks/communityCard.ts +++ b/src/shared/mocks/communityCard.ts @@ -1,5 +1,5 @@ import { PostMetaItem } from "@/components/commons/author/author.types"; -import { CategoryType } from "@/lib/constants/post"; +import { CategoryType } from "@/shared/constants/post"; export type TCommunityPost = { id: string; diff --git a/src/lib/mocks/communityPosts.ts b/src/shared/mocks/communityPosts.ts similarity index 100% rename from src/lib/mocks/communityPosts.ts rename to src/shared/mocks/communityPosts.ts diff --git a/src/lib/mocks/fetchUser.ts b/src/shared/mocks/fetchUser.ts similarity index 100% rename from src/lib/mocks/fetchUser.ts rename to src/shared/mocks/fetchUser.ts diff --git a/src/lib/mocks/perfumeCard.ts b/src/shared/mocks/perfumeCard.ts similarity index 100% rename from src/lib/mocks/perfumeCard.ts rename to src/shared/mocks/perfumeCard.ts diff --git a/src/lib/mocks/postCard.ts b/src/shared/mocks/postCard.ts similarity index 100% rename from src/lib/mocks/postCard.ts rename to src/shared/mocks/postCard.ts diff --git a/src/lib/mocks/reply.ts b/src/shared/mocks/reply.ts similarity index 100% rename from src/lib/mocks/reply.ts rename to src/shared/mocks/reply.ts diff --git a/src/lib/mocks/reviewCard.ts b/src/shared/mocks/reviewCard.ts similarity index 100% rename from src/lib/mocks/reviewCard.ts rename to src/shared/mocks/reviewCard.ts diff --git a/src/lib/types/perfumeDetail.d.ts b/src/shared/types/perfumeDetail.d.ts similarity index 100% rename from src/lib/types/perfumeDetail.d.ts rename to src/shared/types/perfumeDetail.d.ts diff --git a/src/lib/types/perfumeReview.d.ts b/src/shared/types/perfumeReview.d.ts similarity index 100% rename from src/lib/types/perfumeReview.d.ts rename to src/shared/types/perfumeReview.d.ts diff --git a/src/lib/types/point.types.ts b/src/shared/types/point.types.ts similarity index 100% rename from src/lib/types/point.types.ts rename to src/shared/types/point.types.ts diff --git a/src/lib/types/postDraft.ts b/src/shared/types/postDraft.ts similarity index 100% rename from src/lib/types/postDraft.ts rename to src/shared/types/postDraft.ts diff --git a/src/lib/types/review.types.ts b/src/shared/types/review.types.ts similarity index 79% rename from src/lib/types/review.types.ts rename to src/shared/types/review.types.ts index 002ecf70..13e31100 100644 --- a/src/lib/types/review.types.ts +++ b/src/shared/types/review.types.ts @@ -1,4 +1,4 @@ -import { REVIEW_CONFIG } from "@/lib/constants/review"; +import { REVIEW_CONFIG } from "@/shared/constants/review"; export type ReviewCategory = keyof typeof REVIEW_CONFIG; export type ReviewOptionKey = diff --git a/src/lib/types/search.types.ts b/src/shared/types/search.types.ts similarity index 100% rename from src/lib/types/search.types.ts rename to src/shared/types/search.types.ts diff --git a/src/lib/types/swagger-ui-react.d.ts b/src/shared/types/swagger-ui-react.d.ts similarity index 100% rename from src/lib/types/swagger-ui-react.d.ts rename to src/shared/types/swagger-ui-react.d.ts diff --git a/src/lib/utils/cn.ts b/src/shared/utils/cn.ts similarity index 100% rename from src/lib/utils/cn.ts rename to src/shared/utils/cn.ts diff --git a/src/lib/utils/extractFirstImageSrc.ts b/src/shared/utils/extractFirstImageSrc.ts similarity index 100% rename from src/lib/utils/extractFirstImageSrc.ts rename to src/shared/utils/extractFirstImageSrc.ts diff --git a/src/lib/utils/generateSeo.ts b/src/shared/utils/generateSeo.ts similarity index 100% rename from src/lib/utils/generateSeo.ts rename to src/shared/utils/generateSeo.ts diff --git a/src/lib/utils/getPlainText.ts b/src/shared/utils/getPlainText.ts similarity index 100% rename from src/lib/utils/getPlainText.ts rename to src/shared/utils/getPlainText.ts diff --git a/src/lib/utils/getUserSessionInfo.ts b/src/shared/utils/getUserSessionInfo.ts similarity index 75% rename from src/lib/utils/getUserSessionInfo.ts rename to src/shared/utils/getUserSessionInfo.ts index 03d72044..94f42a89 100644 --- a/src/lib/utils/getUserSessionInfo.ts +++ b/src/shared/utils/getUserSessionInfo.ts @@ -1,4 +1,4 @@ -import { getSession } from "@/lib/database/getSession"; +import { getSession } from "@/server/database/getSession"; export async function getUserSessionInfo(pageOwnerId: string) { const session = await getSession(); diff --git a/src/lib/utils/highlightText.tsx b/src/shared/utils/highlightText.tsx similarity index 100% rename from src/lib/utils/highlightText.tsx rename to src/shared/utils/highlightText.tsx diff --git a/src/lib/utils/level.utils.ts b/src/shared/utils/level.utils.ts similarity index 100% rename from src/lib/utils/level.utils.ts rename to src/shared/utils/level.utils.ts diff --git a/src/lib/utils/postDraft.ts b/src/shared/utils/postDraft.ts similarity index 86% rename from src/lib/utils/postDraft.ts rename to src/shared/utils/postDraft.ts index 2e4ea920..0b5a9aca 100644 --- a/src/lib/utils/postDraft.ts +++ b/src/shared/utils/postDraft.ts @@ -1,6 +1,6 @@ -import { PostDraftInput, SaveDraftResponse } from "@/lib/types/postDraft"; -import { draftApi } from "@/lib/utils/api/draft.api"; -import { CreateDraftBody } from "@/lib/hono/schemas/draft.schema"; +import { PostDraftInput, SaveDraftResponse } from "@/shared/types/postDraft"; +import { draftApi } from "@/client/utils/api/draft.api"; +import { CreateDraftBody } from "@/server/hono/schemas/draft.schema"; /** * Draft 저장 함수 diff --git a/src/lib/utils/review.helpers.ts b/src/shared/utils/review.helpers.ts similarity index 100% rename from src/lib/utils/review.helpers.ts rename to src/shared/utils/review.helpers.ts diff --git a/src/lib/utils/stableStringify.ts b/src/shared/utils/stableStringify.ts similarity index 100% rename from src/lib/utils/stableStringify.ts rename to src/shared/utils/stableStringify.ts From ae8de09aaa90535ec5c9757cad5549f31c2916ec Mon Sep 17 00:00:00 2001 From: kuan Date: Wed, 25 Mar 2026 09:39:04 +0900 Subject: [PATCH 02/39] =?UTF-8?q?chore=20:=20prisma.utils.ts=20=EB=B6=84?= =?UTF-8?q?=ED=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainContentCommunityList.tsx | 2 +- .../hono/repositories/brand.repository.ts | 64 ++++++ .../hono/repositories/comment.repository.ts | 22 ++ .../hono/repositories/community.repository.ts | 33 +++ .../hono/repositories/perfume.repository.ts | 34 +++ .../hono/repositories/review.repository.ts | 30 +++ .../hono/repositories/user.repository.ts | 31 +++ src/server/hono/services/brand.service.ts | 2 +- src/server/hono/services/comment.service.ts | 2 +- src/server/hono/services/community.service.ts | 2 +- src/server/hono/services/me.service.ts | 18 +- src/server/hono/services/perfume.service.ts | 4 +- src/server/hono/services/review.service.ts | 2 +- src/server/hono/services/user.service.ts | 6 +- src/server/hono/utils/prisma.utils.ts | 214 ------------------ 15 files changed, 236 insertions(+), 230 deletions(-) create mode 100644 src/server/hono/repositories/brand.repository.ts create mode 100644 src/server/hono/repositories/comment.repository.ts create mode 100644 src/server/hono/repositories/community.repository.ts create mode 100644 src/server/hono/repositories/perfume.repository.ts create mode 100644 src/server/hono/repositories/review.repository.ts create mode 100644 src/server/hono/repositories/user.repository.ts delete mode 100644 src/server/hono/utils/prisma.utils.ts diff --git a/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx b/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx index 67df5cdb..c4037212 100644 --- a/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx +++ b/src/components/domains/main/mainContent/mainContentCommunity/MainContentCommunityList.tsx @@ -9,7 +9,7 @@ import Link from "next/link"; import { useCommunityPosts } from "@/client/hooks/query/useCommunityQuery"; import { PostCategory } from "@prisma/client"; import { Indicator } from "@/components/commons/loading/Indicator"; -import { BasePost } from "@/server/hono/utils/prisma.utils"; +import { BasePost } from "@/server/hono/repositories/community.repository"; interface IMainContentCommunityList { size: "s" | "m"; diff --git a/src/server/hono/repositories/brand.repository.ts b/src/server/hono/repositories/brand.repository.ts new file mode 100644 index 00000000..a9021457 --- /dev/null +++ b/src/server/hono/repositories/brand.repository.ts @@ -0,0 +1,64 @@ +import { Prisma } from "@prisma/client"; + +export const brandSelect = { + nameEn: true, + nameKo: true, + brandUrl: true, +} satisfies Prisma.BrandSelect; + +export const brandSimpleSelect = { + nameEn: true, + nameKo: true, +} satisfies Prisma.BrandSelect; + +export const brandDetailSelect = { + id: true, + nameEn: true, + nameKo: true, + description: true, + imageUrl: true, + brandUrl: true, + mapLocation: true, +} satisfies Prisma.BrandSelect; + +export const brandWithPerfumesInclude = { + perfumes: { + include: { + brand: { select: brandSelect }, + perfumeImage: { select: { imageUrl: true } }, + }, + orderBy: { nameKo: "asc" as const }, + }, +} satisfies Prisma.BrandInclude; + +export type SimpleBrand = Prisma.BrandGetPayload<{ + select: typeof brandSimpleSelect; +}>; + +export type DetailBrand = Prisma.BrandGetPayload<{ + select: typeof brandDetailSelect; +}>; + +export type BrandWithPerfumes = Prisma.BrandGetPayload<{ + include: typeof brandWithPerfumesInclude; +}>; + +export function parseMapLocation( + value: Prisma.JsonValue +): { latitude: number; longitude: number } | null { + if (value === null) return null; + + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + const obj = value as Record; + + if (typeof obj.latitude === "number" && typeof obj.longitude === "number") { + return { + latitude: obj.latitude, + longitude: obj.longitude, + }; + } + } + + console.warn("Invalid mapLocation format:", value); + return null; +} diff --git a/src/server/hono/repositories/comment.repository.ts b/src/server/hono/repositories/comment.repository.ts new file mode 100644 index 00000000..3cc68a99 --- /dev/null +++ b/src/server/hono/repositories/comment.repository.ts @@ -0,0 +1,22 @@ +import { Prisma } from "@prisma/client"; +import { authorSelect } from "./user.repository"; + +export const commentIncludeArgs = { + author: { + select: authorSelect, + }, + replies: { + include: { + author: { + select: authorSelect, + }, + }, + orderBy: { + createdAt: "asc" as const, + }, + }, +} satisfies Prisma.CommentInclude; + +export type CommentWithReplies = Prisma.CommentGetPayload<{ + include: typeof commentIncludeArgs; +}>; diff --git a/src/server/hono/repositories/community.repository.ts b/src/server/hono/repositories/community.repository.ts new file mode 100644 index 00000000..0bb0e11f --- /dev/null +++ b/src/server/hono/repositories/community.repository.ts @@ -0,0 +1,33 @@ +import { Prisma } from "@prisma/client"; +import { authorSelect } from "./user.repository"; +import { perfumeBaseInclude } from "./perfume.repository"; + +export const postIncludeArgs = { + author: { + select: authorSelect, + }, +} satisfies Prisma.PostInclude; + +export const postDetailIncludeArgs = { + ...postIncludeArgs, + perfumeMappings: { + select: { + perfume: { + select: { + id: true, + nameEn: true, + nameKo: true, + ...perfumeBaseInclude, + }, + }, + }, + }, +} satisfies Prisma.PostInclude; + +export type BasePost = Prisma.PostGetPayload<{ + include: typeof postIncludeArgs; +}>; + +export type FullPost = Prisma.PostGetPayload<{ + include: typeof postDetailIncludeArgs; +}>; diff --git a/src/server/hono/repositories/perfume.repository.ts b/src/server/hono/repositories/perfume.repository.ts new file mode 100644 index 00000000..cf2bfd2f --- /dev/null +++ b/src/server/hono/repositories/perfume.repository.ts @@ -0,0 +1,34 @@ +import { Prisma } from "@prisma/client"; +import { brandSelect } from "./brand.repository"; +import { authorSelect } from "./user.repository"; + +export const perfumeBaseInclude = { + brand: { select: brandSelect }, + perfumeImage: { select: { imageUrl: true } }, +} satisfies Prisma.PerfumeInclude; + +export const perfumeDetailInclude = { + ...perfumeBaseInclude, + accordMappings: { select: { accord: true } }, + noteMappings: { select: { note: true, noteStage: true } }, + reviews: { + select: { + id: true, + content: true, + author: { select: authorSelect }, + }, + orderBy: { createdAt: "desc" as const }, + take: 5, + }, + _count: { + select: { bookmarks: true, reviews: true, collectedByUsers: true }, + }, +} satisfies Prisma.PerfumeInclude; + +export type BasePerfume = Prisma.PerfumeGetPayload<{ + include: typeof perfumeBaseInclude; +}>; + +export type FullPerfume = Prisma.PerfumeGetPayload<{ + include: typeof perfumeDetailInclude; +}>; diff --git a/src/server/hono/repositories/review.repository.ts b/src/server/hono/repositories/review.repository.ts new file mode 100644 index 00000000..dd7b71db --- /dev/null +++ b/src/server/hono/repositories/review.repository.ts @@ -0,0 +1,30 @@ +import { Prisma } from "@prisma/client"; +import { authorSelect } from "./user.repository"; +import { perfumeBaseInclude } from "./perfume.repository"; + +export const reviewIncludeArgs = { + author: { + select: authorSelect, + }, + perfume: { + select: { + id: true, + nameKo: true, + nameEn: true, + ...perfumeBaseInclude, + }, + }, + attributeSelections: { + include: { + option: { + include: { + attribute: true, + }, + }, + }, + }, +} satisfies Prisma.ReviewInclude; + +export type FullReview = Prisma.ReviewGetPayload<{ + include: typeof reviewIncludeArgs; +}>; diff --git a/src/server/hono/repositories/user.repository.ts b/src/server/hono/repositories/user.repository.ts new file mode 100644 index 00000000..112fcefd --- /dev/null +++ b/src/server/hono/repositories/user.repository.ts @@ -0,0 +1,31 @@ +import { Prisma } from "@prisma/client"; + +export const authorSelect = { + id: true, + nickname: true, + imageUrl: true, +} satisfies Prisma.UserSelect; + +export const myCollectionInclude = { + image: true, +} satisfies Prisma.UserCollectionInclude; + +export const myCommentInclude = { + post: { select: { id: true, title: true } }, +} satisfies Prisma.CommentInclude; + +export type UserProfile = Prisma.UserGetPayload<{ + select: typeof authorSelect; +}>; + +export type MyCollection = Prisma.UserCollectionGetPayload<{ + include: typeof myCollectionInclude; +}>; + +export type MyComment = Prisma.CommentGetPayload<{ + include: typeof myCommentInclude; +}>; + +export type UserCollectionWithRelations = Prisma.UserCollectionGetPayload<{ + include: { image: true; perfume: true }; +}>; diff --git a/src/server/hono/services/brand.service.ts b/src/server/hono/services/brand.service.ts index 6d36bf61..ef324a52 100644 --- a/src/server/hono/services/brand.service.ts +++ b/src/server/hono/services/brand.service.ts @@ -9,7 +9,7 @@ import { ApiBrandDetailResponse, ApiBrandSimpleResponse, } from "../schemas/brand.schema"; -import { brandDetailSelect, parseMapLocation } from "../utils/prisma.utils"; +import { brandDetailSelect, parseMapLocation } from "../repositories/brand.repository"; /** * 모든 브랜드 목록 조회 diff --git a/src/server/hono/services/comment.service.ts b/src/server/hono/services/comment.service.ts index 59794c11..73d34145 100644 --- a/src/server/hono/services/comment.service.ts +++ b/src/server/hono/services/comment.service.ts @@ -19,7 +19,7 @@ import { UpdateCommentPayload, } from "../schemas/comment.schema"; import { createCursorPaginationResult } from "../utils/pagination.utils"; -import { commentIncludeArgs, CommentWithReplies } from "../utils/prisma.utils"; +import { commentIncludeArgs, CommentWithReplies } from "../repositories/comment.repository"; import { earnPointsService } from "./point.service"; const commentWithRepliesArgs = { include: commentIncludeArgs }; diff --git a/src/server/hono/services/community.service.ts b/src/server/hono/services/community.service.ts index 3690fe44..687a446d 100644 --- a/src/server/hono/services/community.service.ts +++ b/src/server/hono/services/community.service.ts @@ -26,7 +26,7 @@ import { postIncludeArgs, postDetailIncludeArgs, BasePost, -} from "../utils/prisma.utils"; +} from "../repositories/community.repository"; import { earnPointsService } from "./point.service"; import { sanitizeHtml } from "../utils/sanitize.utils"; diff --git a/src/server/hono/services/me.service.ts b/src/server/hono/services/me.service.ts index bcee720c..356bc6a6 100644 --- a/src/server/hono/services/me.service.ts +++ b/src/server/hono/services/me.service.ts @@ -19,17 +19,23 @@ import { import { PaginatedResponse } from "../schemas/common.schema"; import { deleteImageByUrl } from "./file.service"; import { - BasePost, BasePerfume, - FullReview, - MyCollection, - MyComment, perfumeBaseInclude, +} from "../repositories/perfume.repository"; +import { + BasePost, postIncludeArgs, +} from "../repositories/community.repository"; +import { + FullReview, + reviewIncludeArgs, +} from "../repositories/review.repository"; +import { + MyCollection, myCollectionInclude, + MyComment, myCommentInclude, - reviewIncludeArgs, -} from "../utils/prisma.utils"; +} from "../repositories/user.repository"; import { calculateLevel, getPointsForNextLevel } from "@/shared/utils/level.utils"; const UUID_REGEX = diff --git a/src/server/hono/services/perfume.service.ts b/src/server/hono/services/perfume.service.ts index 314818b5..655624d3 100644 --- a/src/server/hono/services/perfume.service.ts +++ b/src/server/hono/services/perfume.service.ts @@ -11,8 +11,8 @@ import { perfumeDetailInclude, BasePerfume, FullPerfume, - BasePost, -} from "../utils/prisma.utils"; +} from "../repositories/perfume.repository"; +import { BasePost } from "../repositories/community.repository"; import { ApiPerfumeNoteResponse, ApiPerfumeAccordResponse, diff --git a/src/server/hono/services/review.service.ts b/src/server/hono/services/review.service.ts index 1fe2574f..d727c458 100644 --- a/src/server/hono/services/review.service.ts +++ b/src/server/hono/services/review.service.ts @@ -13,7 +13,7 @@ import { } from "../utils/service.utils"; import { checkResourceExists, validateUuid } from "../utils/service.utils"; import { createCursorPaginationResult } from "../utils/pagination.utils"; -import { reviewIncludeArgs, FullReview } from "../utils/prisma.utils"; +import { reviewIncludeArgs, FullReview } from "../repositories/review.repository"; const POPULAR_REVIEW_LIMIT = 5; const POPULAR_REVIEW_POOL_SIZE = 20; diff --git a/src/server/hono/services/user.service.ts b/src/server/hono/services/user.service.ts index 6cb43656..a752649d 100644 --- a/src/server/hono/services/user.service.ts +++ b/src/server/hono/services/user.service.ts @@ -7,11 +7,11 @@ import { } from "../utils/service.utils"; import { checkResourceExists } from "../utils/service.utils"; import { - BasePost, BasePerfume, perfumeBaseInclude, - UserCollectionWithRelations, -} from "../utils/prisma.utils"; +} from "../repositories/perfume.repository"; +import { BasePost } from "../repositories/community.repository"; +import { UserCollectionWithRelations } from "../repositories/user.repository"; import { calculateLevel } from "@/shared/utils/level.utils"; import { ApiUserProfileResponse } from "../schemas/user.schema"; diff --git a/src/server/hono/utils/prisma.utils.ts b/src/server/hono/utils/prisma.utils.ts deleted file mode 100644 index 54749b37..00000000 --- a/src/server/hono/utils/prisma.utils.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Prisma } from "@prisma/client"; - -export const brandSelect = { - nameEn: true, - nameKo: true, - brandUrl: true, -} satisfies Prisma.BrandSelect; - -export const authorSelect = { - id: true, - nickname: true, - imageUrl: true, -} satisfies Prisma.UserSelect; - -// Perfume Includes -export const perfumeBaseInclude = { - brand: { select: brandSelect }, - perfumeImage: { select: { imageUrl: true } }, -} satisfies Prisma.PerfumeInclude; - -export const perfumeDetailInclude = { - ...perfumeBaseInclude, - accordMappings: { select: { accord: true } }, - noteMappings: { select: { note: true, noteStage: true } }, - reviews: { - select: { - id: true, - content: true, - author: { select: authorSelect }, - }, - orderBy: { createdAt: "desc" as const }, - take: 5, - }, - _count: { - select: { bookmarks: true, reviews: true, collectedByUsers: true }, - }, -} satisfies Prisma.PerfumeInclude; - -// Brand Includes -export const brandSimpleSelect = { - nameEn: true, - nameKo: true, -} satisfies Prisma.BrandSelect; - -export const brandDetailSelect = { - id: true, - nameEn: true, - nameKo: true, - description: true, - imageUrl: true, - brandUrl: true, - mapLocation: true, -} satisfies Prisma.BrandSelect; - -export const brandWithPerfumesInclude = { - perfumes: { - include: perfumeBaseInclude, - orderBy: { nameKo: "asc" as const }, - }, -} satisfies Prisma.BrandInclude; - -// Post Includes -export const postIncludeArgs = { - author: { - select: authorSelect, - }, -} satisfies Prisma.PostInclude; - -export const postDetailIncludeArgs = { - ...postIncludeArgs, - perfumeMappings: { - select: { - perfume: { - select: { - id: true, - nameEn: true, - nameKo: true, - ...perfumeBaseInclude, - }, - }, - }, - }, -} satisfies Prisma.PostInclude; - -// Review Includes -export const reviewIncludeArgs = { - author: { - select: authorSelect, - }, - perfume: { - select: { - id: true, - nameKo: true, - nameEn: true, - ...perfumeBaseInclude, - }, - }, - attributeSelections: { - include: { - option: { - include: { - attribute: true, - }, - }, - }, - }, -} satisfies Prisma.ReviewInclude; - -// Comment Includes -export const commentIncludeArgs = { - author: { - select: authorSelect, - }, - replies: { - include: { - author: { - select: authorSelect, - }, - }, - orderBy: { - createdAt: "asc" as const, - }, - }, -} satisfies Prisma.CommentInclude; - -// Collection Includes -export const myCollectionInclude = { - image: true, -} satisfies Prisma.UserCollectionInclude; - -export const myCommentInclude = { - post: { select: { id: true, title: true } }, -} satisfies Prisma.CommentInclude; - -export type BasePerfume = Prisma.PerfumeGetPayload<{ - include: typeof perfumeBaseInclude; -}>; - -export type FullPerfume = Prisma.PerfumeGetPayload<{ - include: typeof perfumeDetailInclude; -}>; - -export type UserProfile = Prisma.UserGetPayload<{ - select: typeof authorSelect; -}>; - -export type BasePost = Prisma.PostGetPayload<{ - include: typeof postIncludeArgs; -}>; - -export type FullPost = Prisma.PostGetPayload<{ - include: typeof postDetailIncludeArgs; -}>; - -export type FullReview = Prisma.ReviewGetPayload<{ - include: typeof reviewIncludeArgs; -}>; - -export type CommentWithReplies = Prisma.CommentGetPayload<{ - include: typeof commentIncludeArgs; -}>; - -export type MyCollection = Prisma.UserCollectionGetPayload<{ - include: typeof myCollectionInclude; -}>; - -export type MyComment = Prisma.CommentGetPayload<{ - include: typeof myCommentInclude; -}>; - -// Brand Types -export type SimpleBrand = Prisma.BrandGetPayload<{ - select: typeof brandSimpleSelect; -}>; - -export type DetailBrand = Prisma.BrandGetPayload<{ - select: typeof brandDetailSelect; -}>; - -export type BrandWithPerfumes = Prisma.BrandGetPayload<{ - include: typeof brandWithPerfumesInclude; -}>; - -// User Collection Types -export type UserCollectionWithRelations = Prisma.UserCollectionGetPayload<{ - include: { image: true; perfume: true }; -}>; - -// ==================== Type Transform Utilities ==================== - -/** - * Prisma JsonValue를 MapLocation으로 변환 - * - 유효한 형식이면 { latitude, longitude } 반환 - * - 유효하지 않으면 null 반환 - */ -export function parseMapLocation( - value: Prisma.JsonValue -): { latitude: number; longitude: number } | null { - if (value === null) return null; - - if (typeof value === "object" && value !== null && !Array.isArray(value)) { - const obj = value as Record; - - if (typeof obj.latitude === "number" && typeof obj.longitude === "number") { - return { - latitude: obj.latitude, - longitude: obj.longitude, - }; - } - } - - console.warn("Invalid mapLocation format:", value); - return null; -} From 7c016853f729fbc9e3dd8aa0e9ce3e0d31a67162 Mon Sep 17 00:00:00 2001 From: kuan Date: Thu, 26 Mar 2026 01:15:51 +0900 Subject: [PATCH 03/39] =?UTF-8?q?chore:=20service.utils.ts=20=EA=B4=80?= =?UTF-8?q?=EC=8B=AC=EC=82=AC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/hono/app.ts | 4 +- .../hono/repositories/base.repository.ts | 37 ++++ src/server/hono/routes/v1/comment.handler.ts | 11 +- .../hono/routes/v1/community.handler.ts | 17 +- src/server/hono/routes/v1/draft.handler.ts | 14 +- src/server/hono/routes/v1/file.handler.ts | 5 +- src/server/hono/routes/v1/me.handler.ts | 47 +++-- src/server/hono/routes/v1/perfume.handler.ts | 8 +- src/server/hono/routes/v1/point.handler.ts | 14 +- src/server/hono/routes/v1/review.handler.ts | 8 +- src/server/hono/services/brand.service.ts | 2 +- src/server/hono/services/comment.service.ts | 6 +- src/server/hono/services/community.service.ts | 6 +- src/server/hono/services/draft.service.ts | 4 +- src/server/hono/services/file.service.ts | 4 +- src/server/hono/services/filter.service.ts | 2 +- src/server/hono/services/me.service.ts | 2 +- src/server/hono/services/perfume.service.ts | 4 +- src/server/hono/services/point.service.ts | 2 +- src/server/hono/services/review.service.ts | 5 +- src/server/hono/services/search.service.ts | 2 +- src/server/hono/services/user.service.ts | 4 +- src/server/hono/utils/service.utils.ts | 161 ------------------ src/server/result.ts | 66 +++++++ src/server/utils/image.utils.ts | 20 +++ src/shared/utils/validate.utils.ts | 26 +++ 26 files changed, 252 insertions(+), 229 deletions(-) create mode 100644 src/server/hono/repositories/base.repository.ts delete mode 100644 src/server/hono/utils/service.utils.ts create mode 100644 src/server/result.ts create mode 100644 src/server/utils/image.utils.ts create mode 100644 src/shared/utils/validate.utils.ts diff --git a/src/server/hono/app.ts b/src/server/hono/app.ts index 25535263..421780c4 100644 --- a/src/server/hono/app.ts +++ b/src/server/hono/app.ts @@ -15,11 +15,9 @@ import pointApi from "./routes/v1/point.handler"; import draftsApi from "./routes/v1/draft.handler"; import { handleApiError } from "./utils/api.utils"; -export type AuthenticatedUser = User; - export type AppContext = { Variables: { - user?: AuthenticatedUser; + user?: User; session?: Session; }; }; diff --git a/src/server/hono/repositories/base.repository.ts b/src/server/hono/repositories/base.repository.ts new file mode 100644 index 00000000..1d977e14 --- /dev/null +++ b/src/server/hono/repositories/base.repository.ts @@ -0,0 +1,37 @@ +import { prisma } from "@/server/prisma"; +import { + serviceNotFound, + serviceSuccess, + ServiceResult, +} from "@/server/result"; + +type FindUniqueMethod = { + findUnique: (args: { + where: { id: string }; + select: { id: boolean }; + }) => Promise<{ id: string } | null>; +}; + +/** + * 특정 리소스가 데이터베이스에 존재하는지 확인합니다. + * @param model - Prisma 모델 이름 (예: "perfume", "user") + * @param id - 확인할 리소스의 ID + * @param resourceName - 오류 메시지에 사용할 리소스의 한글 이름 (예: "향수") + */ +export async function checkResourceExists( + model: keyof typeof prisma, + id: string, + resourceName: string +): Promise> { + const resource = await ( + prisma[model] as unknown as FindUniqueMethod + ).findUnique({ + where: { id }, + select: { id: true }, + }); + + if (!resource) { + return serviceNotFound(`${resourceName}을(를) 찾을 수 없습니다.`); + } + return serviceSuccess(true); +} diff --git a/src/server/hono/routes/v1/comment.handler.ts b/src/server/hono/routes/v1/comment.handler.ts index 55674c77..ed4eefbe 100644 --- a/src/server/hono/routes/v1/comment.handler.ts +++ b/src/server/hono/routes/v1/comment.handler.ts @@ -12,7 +12,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -127,7 +127,8 @@ const createCommentRoute = createRoute({ routers.authenticated.openapi(createCommentRoute, async (c) => { const { postId } = c.req.valid("param"); const body = c.req.valid("json"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const payload: CommentSchemas.CreateCommentPayload = { ...body, @@ -188,7 +189,8 @@ const editCommentRoute = createRoute({ routers.authenticated.openapi(editCommentRoute, async (c) => { const { commentId } = c.req.valid("param"); const body = c.req.valid("json"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const payload = { id: commentId, authorId: user.id, @@ -231,7 +233,8 @@ const deleteCommentRoute = createRoute({ routers.authenticated.openapi(deleteCommentRoute, async (c) => { const { commentId } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const payload: CommentSchemas.DeleteCommentPayload = { id: commentId, authorId: user.id, diff --git a/src/server/hono/routes/v1/community.handler.ts b/src/server/hono/routes/v1/community.handler.ts index b01f2a27..3dc1ae6d 100644 --- a/src/server/hono/routes/v1/community.handler.ts +++ b/src/server/hono/routes/v1/community.handler.ts @@ -12,7 +12,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -198,7 +198,8 @@ const createPostRoute = createRoute({ }); routers.authenticated.openapi(createPostRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const body = c.req.valid("json"); const result = await CommunityServices.createPostService({ @@ -242,7 +243,8 @@ const editPostRoute = createRoute({ routers.authenticated.openapi(editPostRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await CommunityServices.updatePostService( id, @@ -287,7 +289,8 @@ const deletePostRoute = createRoute({ routers.authenticated.openapi(deletePostRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await CommunityServices.deletePostService(id, user.id); if (!result.success) { @@ -322,7 +325,8 @@ const likePostRoute = createRoute({ routers.authenticated.openapi(likePostRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await CommunityServices.togglePostLikeService(id, user.id); if (!result.success) { @@ -357,7 +361,8 @@ const bookmarkPostRoute = createRoute({ routers.authenticated.openapi(bookmarkPostRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await CommunityServices.togglePostBookmarkService(id, user.id); if (!result.success) { diff --git a/src/server/hono/routes/v1/draft.handler.ts b/src/server/hono/routes/v1/draft.handler.ts index a3c453f2..0e4c0103 100644 --- a/src/server/hono/routes/v1/draft.handler.ts +++ b/src/server/hono/routes/v1/draft.handler.ts @@ -12,7 +12,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -43,7 +43,8 @@ const createOrUpdateDraftRoute = createRoute({ }); routers.authenticated.openapi(createOrUpdateDraftRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const body = c.req.valid("json"); const payload: DraftSchemas.CreateDraftPayload = { @@ -83,7 +84,8 @@ const listDraftsRoute = createRoute({ }); routers.authenticated.openapi(listDraftsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await DraftServices.listDraftsService(user.id); @@ -115,7 +117,8 @@ const getDraftRoute = createRoute({ routers.authenticated.openapi(getDraftRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await DraftServices.getDraftService(id, user.id); @@ -153,7 +156,8 @@ const deleteDraftRoute = createRoute({ routers.authenticated.openapi(deleteDraftRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await DraftServices.deleteDraftService(id, user.id); diff --git a/src/server/hono/routes/v1/file.handler.ts b/src/server/hono/routes/v1/file.handler.ts index bfd46ae3..8b92d041 100644 --- a/src/server/hono/routes/v1/file.handler.ts +++ b/src/server/hono/routes/v1/file.handler.ts @@ -8,7 +8,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -43,7 +43,8 @@ const uploadRoute = createRoute({ }); routers.authenticated.openapi(uploadRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const formData = await c.req.formData(); const file = formData.get("file"); const bucketName = formData.get("bucketName"); diff --git a/src/server/hono/routes/v1/me.handler.ts b/src/server/hono/routes/v1/me.handler.ts index 421875fa..962f0042 100644 --- a/src/server/hono/routes/v1/me.handler.ts +++ b/src/server/hono/routes/v1/me.handler.ts @@ -10,7 +10,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -32,7 +32,8 @@ const getMyBookmarkedPostsRoute = createRoute({ }); routers.authenticated.openapi(getMyBookmarkedPostsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getMyBookmarkedPostsService(user.id); if (!result.success) { @@ -61,7 +62,8 @@ const getMyBookmarkedPerfumesRoute = createRoute({ }); routers.authenticated.openapi(getMyBookmarkedPerfumesRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getMyBookmarkedPerfumesService(user.id); @@ -100,7 +102,8 @@ const postPhotoCollectionRoute = createRoute({ }); routers.authenticated.openapi(postPhotoCollectionRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const body = c.req.valid("json"); const data = { @@ -140,7 +143,8 @@ const deletePhotoCollectionRoute = createRoute({ }); routers.authenticated.openapi(deletePhotoCollectionRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const { collectionId } = c.req.valid("param"); const result = await MeServices.deletePhotoCollectionService({ @@ -183,7 +187,8 @@ const getMyReviewsRoute = createRoute({ }); routers.authenticated.openapi(getMyReviewsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const options = c.req.valid("query"); const result = await MeServices.getMyReviewsService(user.id, options); @@ -220,7 +225,8 @@ const getMyPostsRoute = createRoute({ }); routers.authenticated.openapi(getMyPostsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const options = c.req.valid("query"); const result = await MeServices.getMyPostsService(user.id, options); @@ -257,7 +263,8 @@ const getMyCommentsRoute = createRoute({ }); routers.authenticated.openapi(getMyCommentsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const options = c.req.valid("query"); const result = await MeServices.getMyCommentsService(user.id, options); @@ -288,7 +295,8 @@ const getMyLikedPerfumesRoute = createRoute({ }); routers.authenticated.openapi(getMyLikedPerfumesRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getMyLikedPerfumesService(user.id); @@ -318,7 +326,8 @@ const getMyLikedPostsRoute = createRoute({ }); routers.authenticated.openapi(getMyLikedPostsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getMyLikedPostsService(user.id); @@ -348,7 +357,8 @@ const getMyProfileRoute = createRoute({ }); routers.authenticated.openapi(getMyProfileRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getMyProfileService(user.id); @@ -386,7 +396,8 @@ const patchMyProfileRoute = createRoute({ }); routers.authenticated.openapi(patchMyProfileRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const formData = c.req.valid("json"); const result = await MeServices.updateMyProfileService({ id: user.id, @@ -427,7 +438,8 @@ const postRecentPerfumesRoute = createRoute({ }); routers.authenticated.openapi(postRecentPerfumesRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const formData = c.req.valid("json"); // DB에 최근 본 향수 기록 동기화 const result = await MeServices.syncRecentPerfumesService({ @@ -460,7 +472,8 @@ const getRecentPerfumesRoute = createRoute({ }); routers.authenticated.openapi(getRecentPerfumesRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getRecentPerfumesService(user.id); if (!result.success) { return apiInternalError(c, result.message); @@ -500,7 +513,8 @@ const postRecentPostsRoute = createRoute({ }); routers.authenticated.openapi(postRecentPostsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const formData = c.req.valid("json"); const result = await MeServices.syncRecentPostsService({ userId: user.id, @@ -532,7 +546,8 @@ const getRecentPostsRoute = createRoute({ }); routers.authenticated.openapi(getRecentPostsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await MeServices.getRecentPostsService(user.id); if (!result.success) { return apiInternalError(c, result.message); diff --git a/src/server/hono/routes/v1/perfume.handler.ts b/src/server/hono/routes/v1/perfume.handler.ts index 6f3798a1..e1409d39 100644 --- a/src/server/hono/routes/v1/perfume.handler.ts +++ b/src/server/hono/routes/v1/perfume.handler.ts @@ -13,7 +13,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -212,7 +212,8 @@ const toggleBookmarkRoute = createRoute({ routers.authenticated.openapi(toggleBookmarkRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await PerfumeServices.togglePerfumeBookmarkService( id, user.id @@ -250,7 +251,8 @@ const toggleLikeRoute = createRoute({ routers.authenticated.openapi(toggleLikeRoute, async (c) => { const { id } = c.req.valid("param"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await PerfumeServices.togglePerfumeLikeService(id, user.id); if (!result.success) { diff --git a/src/server/hono/routes/v1/point.handler.ts b/src/server/hono/routes/v1/point.handler.ts index 030c2dbb..04f3cf08 100644 --- a/src/server/hono/routes/v1/point.handler.ts +++ b/src/server/hono/routes/v1/point.handler.ts @@ -9,7 +9,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -30,7 +30,8 @@ const getUserPointsRoute = createRoute({ }); routers.authenticated.openapi(getUserPointsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await PointServices.getUserPointsService(user.id); if (!result.success) { @@ -64,7 +65,8 @@ const getPointHistoryRoute = createRoute({ }); routers.authenticated.openapi(getPointHistoryRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const query = c.req.valid("query"); const result = await PointServices.getPointHistoryService(user.id, { @@ -100,7 +102,8 @@ const getPointStatisticsRoute = createRoute({ }); routers.authenticated.openapi(getPointStatisticsRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await PointServices.getPointStatisticsService(user.id); if (!result.success) { @@ -139,7 +142,8 @@ const processLoginRoute = createRoute({ }); routers.authenticated.openapi(processLoginRoute, async (c) => { - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const body = c.req.valid("json"); // 클라이언트 타임스탬프 검증 (5분 이내의 요청만 허용) diff --git a/src/server/hono/routes/v1/review.handler.ts b/src/server/hono/routes/v1/review.handler.ts index 2925c05a..48df5758 100644 --- a/src/server/hono/routes/v1/review.handler.ts +++ b/src/server/hono/routes/v1/review.handler.ts @@ -15,7 +15,7 @@ import { } from "@/server/hono/utils/api.utils"; import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; -import { getAuthenticatedUser } from "@/server/hono/utils/service.utils"; + const routers = createDomainRouters(); @@ -144,7 +144,8 @@ const createReviewRoute = createRoute({ routers.authenticated.openapi(createReviewRoute, async (c) => { const { perfumeId } = c.req.param(); const validatedData = c.req.valid("json"); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const payload = { ...validatedData, perfumeId, authorId: user.id }; @@ -188,7 +189,8 @@ const toggleLikeRoute = createRoute({ routers.authenticated.openapi(toggleLikeRoute, async (c) => { const { reviewId } = c.req.param(); - const user = getAuthenticatedUser(c); + const user = c.get("user"); + if (!user?.id) throw new Error("인증되지 않은 사용자입니다."); const result = await ReviewServices.toggleLikeService(reviewId, user.id); diff --git a/src/server/hono/services/brand.service.ts b/src/server/hono/services/brand.service.ts index ef324a52..7c9792c4 100644 --- a/src/server/hono/services/brand.service.ts +++ b/src/server/hono/services/brand.service.ts @@ -4,7 +4,7 @@ import { serviceNotFound, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; +} from "@/server/result"; import { ApiBrandDetailResponse, ApiBrandSimpleResponse, diff --git a/src/server/hono/services/comment.service.ts b/src/server/hono/services/comment.service.ts index 73d34145..746d8201 100644 --- a/src/server/hono/services/comment.service.ts +++ b/src/server/hono/services/comment.service.ts @@ -1,15 +1,15 @@ import { Prisma, PointActivityType } from "@prisma/client"; import { prisma } from "@/server/prisma"; import { - checkResourceExists, serviceBadRequest, serviceForbidden, serviceInternalError, serviceNotFound, ServiceResult, serviceSuccess, - validateUuid, -} from "../utils/service.utils"; +} from "@/server/result"; +import { checkResourceExists } from "../repositories/base.repository"; +import { validateUuid } from "@/shared/utils/validate.utils"; import { CreateCommentPayload, DeleteCommentPayload, diff --git a/src/server/hono/services/community.service.ts b/src/server/hono/services/community.service.ts index 687a446d..8c69fa52 100644 --- a/src/server/hono/services/community.service.ts +++ b/src/server/hono/services/community.service.ts @@ -13,15 +13,15 @@ import { PaginationResult, } from "../utils/pagination.utils"; import { - checkResourceExists, serviceBadRequest, serviceForbidden, serviceInternalError, serviceNotFound, ServiceResult, serviceSuccess, - validateUuid, -} from "../utils/service.utils"; +} from "@/server/result"; +import { checkResourceExists } from "../repositories/base.repository"; +import { validateUuid } from "@/shared/utils/validate.utils"; import { postIncludeArgs, postDetailIncludeArgs, diff --git a/src/server/hono/services/draft.service.ts b/src/server/hono/services/draft.service.ts index 0847a4a1..85cc7ac9 100644 --- a/src/server/hono/services/draft.service.ts +++ b/src/server/hono/services/draft.service.ts @@ -1,13 +1,13 @@ import { prisma } from "@/server/prisma"; import { - checkResourceExists, serviceBadRequest, serviceForbidden, serviceInternalError, serviceNotFound, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; +} from "@/server/result"; +import { checkResourceExists } from "../repositories/base.repository"; import { ApiDraftListResponse, ApiDraftResponse, diff --git a/src/server/hono/services/file.service.ts b/src/server/hono/services/file.service.ts index c6274b2d..03129763 100644 --- a/src/server/hono/services/file.service.ts +++ b/src/server/hono/services/file.service.ts @@ -4,8 +4,8 @@ import { serviceInternalError, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; -import { getImageFormat } from "../utils/service.utils"; +} from "@/server/result"; +import { getImageFormat } from "@/server/utils/image.utils"; import { supabaseAdmin } from "@/server/supabase/server"; export interface UploadedImageInfo { diff --git a/src/server/hono/services/filter.service.ts b/src/server/hono/services/filter.service.ts index a45f1b48..9fa1a892 100644 --- a/src/server/hono/services/filter.service.ts +++ b/src/server/hono/services/filter.service.ts @@ -3,7 +3,7 @@ import { serviceInternalError, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; +} from "@/server/result"; import type { AvailableFiltersResponse, AvailableFiltersTotalResponse, diff --git a/src/server/hono/services/me.service.ts b/src/server/hono/services/me.service.ts index 356bc6a6..5802d9f8 100644 --- a/src/server/hono/services/me.service.ts +++ b/src/server/hono/services/me.service.ts @@ -6,7 +6,7 @@ import { serviceInternalError, serviceNotFound, serviceForbidden, -} from "../utils/service.utils"; +} from "@/server/result"; import { ApiMyProfileResponse, ApiUpdateMyProfileRequest, diff --git a/src/server/hono/services/perfume.service.ts b/src/server/hono/services/perfume.service.ts index 655624d3..99905405 100644 --- a/src/server/hono/services/perfume.service.ts +++ b/src/server/hono/services/perfume.service.ts @@ -4,8 +4,8 @@ import { serviceNotFound, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; -import { checkResourceExists } from "../utils/service.utils"; +} from "@/server/result"; +import { checkResourceExists } from "../repositories/base.repository"; import { perfumeBaseInclude, perfumeDetailInclude, diff --git a/src/server/hono/services/point.service.ts b/src/server/hono/services/point.service.ts index 18b3bacf..e85ab82e 100644 --- a/src/server/hono/services/point.service.ts +++ b/src/server/hono/services/point.service.ts @@ -6,7 +6,7 @@ import { ServiceResult, serviceSuccess, serviceError, -} from "../utils/service.utils"; +} from "@/server/result"; import { POINT_POLICY, POINT_DESCRIPTIONS } from "@/shared/constants/point"; import type { PointEarnData, diff --git a/src/server/hono/services/review.service.ts b/src/server/hono/services/review.service.ts index d727c458..0f73fe1f 100644 --- a/src/server/hono/services/review.service.ts +++ b/src/server/hono/services/review.service.ts @@ -10,8 +10,9 @@ import { serviceNotFound, serviceAlreadyExists, serviceInternalError, -} from "../utils/service.utils"; -import { checkResourceExists, validateUuid } from "../utils/service.utils"; +} from "@/server/result"; +import { checkResourceExists } from "../repositories/base.repository"; +import { validateUuid } from "@/shared/utils/validate.utils"; import { createCursorPaginationResult } from "../utils/pagination.utils"; import { reviewIncludeArgs, FullReview } from "../repositories/review.repository"; diff --git a/src/server/hono/services/search.service.ts b/src/server/hono/services/search.service.ts index c8a14cd0..b29450a7 100644 --- a/src/server/hono/services/search.service.ts +++ b/src/server/hono/services/search.service.ts @@ -10,7 +10,7 @@ import { serviceInternalError, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; +} from "@/server/result"; import { createCursorPaginationResult } from "../utils/pagination.utils"; function transformSupabasePerfume( diff --git a/src/server/hono/services/user.service.ts b/src/server/hono/services/user.service.ts index a752649d..e82abfbe 100644 --- a/src/server/hono/services/user.service.ts +++ b/src/server/hono/services/user.service.ts @@ -4,8 +4,8 @@ import { serviceNotFound, ServiceResult, serviceSuccess, -} from "../utils/service.utils"; -import { checkResourceExists } from "../utils/service.utils"; +} from "@/server/result"; +import { checkResourceExists } from "../repositories/base.repository"; import { BasePerfume, perfumeBaseInclude, diff --git a/src/server/hono/utils/service.utils.ts b/src/server/hono/utils/service.utils.ts deleted file mode 100644 index e1bf4087..00000000 --- a/src/server/hono/utils/service.utils.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { prisma } from "@/server/prisma"; -import { z } from "zod"; -import { Context } from "hono"; -import { ImageFormat } from "@prisma/client"; -import { User } from "next-auth"; - -const IMAGE_FORMAT_MAP: Record = { - JPEG: ImageFormat.JPEG, - JPG: ImageFormat.JPEG, - PNG: ImageFormat.PNG, - WEBP: ImageFormat.WEBP, - HEIF: ImageFormat.HEIC, - HEIC: ImageFormat.HEIC, -} as const; - -/** - * 인증된 사용자 타입 - id가 반드시 존재함 - */ -export type AuthenticatedUser = Required> & Omit; - -export type ServiceErrorCode = - | "NOT_FOUND" - | "ALREADY_EXISTS" - | "BAD_REQUEST" - | "INTERNAL_ERROR" - | "FORBIDDEN"; - -export type ServiceResult = - | { success: true; data: T } - | { success: false; error: E; message?: string }; - -/** - * 성공적인 서비스 결과 객체를 생성합니다. - * @param data - 성공 시 반환할 데이터 - */ -export function serviceSuccess(data: T): ServiceResult { - return { success: true, data }; -} - -/** - * 실패 서비스 결과 객체를 생성합니다. - * @param error - 에러를 식별하는 코드 또는 문자열 - * @param message - 클라이언트에게 보여줄 수 있는 에러 메시지 - */ -export function serviceError( - error: E, - message?: string -): ServiceResult { - return { success: false, error, message }; -} - -/** - * 'NOT_FOUND' 에러 결과 객체를 생성합니다. - */ -export const serviceNotFound = (message = "리소스를 찾을 수 없습니다.") => - serviceError("NOT_FOUND", message); - -/** - * 'ALREADY_EXISTS' 에러 결과 객체를 생성합니다. - */ -export const serviceAlreadyExists = (message = "이미 존재하는 리소스입니다.") => - serviceError("ALREADY_EXISTS", message); - -/** - * 'BAD_REQUEST' 또는 유효성 검사 에러 결과 객체를 생성합니다. - */ -export const serviceBadRequest = (message = "잘못된 요청입니다.") => - serviceError("BAD_REQUEST", message); - -/** - * 'FORBIDDEN' 에러 결과 객체를 생성합니다. - */ -export const serviceForbidden = (message = "요청을 수행할 권한이 없습니다.") => - serviceError("FORBIDDEN", message); - -/** - * 예기치 않은 내부 서버 오류 결과 객체를 생성합니다. - */ -export function serviceInternalError( - error: unknown -): ServiceResult { - const err = error instanceof Error ? error : new Error(String(error)); - const message = err.message; - console.error("[INTERNAL_ERROR]", err); - return { success: false, error: "INTERNAL_ERROR", message }; -} - -type FindUniqueMethod = { - findUnique: (args: { - where: { id: string }; - select: { id: boolean }; - }) => Promise<{ id: string } | null>; -}; - -/** - * 특정 리소스가 데이터베이스에 존재하는지 확인합니다. - * @param model - Prisma 모델 이름 (예: "perfume", "user") - * @param id - 확인할 리소스의 ID - * @param resourceName - 오류 메시지에 사용할 리소스의 한글 이름 (예: "향수") - */ -export async function checkResourceExists( - model: keyof typeof prisma, - id: string, - resourceName: string -): Promise> { - const resource = await ( - prisma[model] as unknown as FindUniqueMethod - ).findUnique({ - where: { id }, - select: { id: true }, - }); - - if (!resource) { - return serviceNotFound(`${resourceName}을(를) 찾을 수 없습니다.`); - } - return serviceSuccess(true); -} - -/** - * UUID 형식이 유효한지 Zod를 사용하여 검사합니다. - * @param id - 검사할 ID - * @param resourceName - 오류 메시지에 사용할 리소스의 이름 - */ -export function validateUuid( - id: string, - resourceName: string -): ServiceResult { - const uuidSchema = z - .string() - .uuid({ message: `유효하지 않은 ${resourceName} ID 형식입니다.` }); - const result = uuidSchema.safeParse(id); - - if (!result.success) { - return serviceBadRequest(result.error.flatten().formErrors[0]); - } - return serviceSuccess(result.data); -} - -/** - * 인증된 사용자 정보를 가져옵니다. - * @param c - Hono 컨텍스트 - * @returns 인증된 사용자 정보 (id가 보장됨) - * @throws 인증되지 않은 사용자이거나 id가 없는 경우 에러를 throw합니다 - */ -export function getAuthenticatedUser(c: Context): AuthenticatedUser { - const user = c.get("user"); - if (!user || !user.id) { - throw new Error("인증되지 않은 사용자입니다."); - } - return user as AuthenticatedUser; -} -/** - * 이미지 형식을 가져옵니다. - * @param formatString - 이미지 형식 문자열 - * @returns 이미지 형식 - */ - -export function getImageFormat(formatString?: string): ImageFormat { - if (!formatString) return ImageFormat.UNKNOWN; - return IMAGE_FORMAT_MAP[formatString.toUpperCase()] ?? ImageFormat.UNKNOWN; -} diff --git a/src/server/result.ts b/src/server/result.ts new file mode 100644 index 00000000..2a9459a4 --- /dev/null +++ b/src/server/result.ts @@ -0,0 +1,66 @@ +export type ServiceErrorCode = + | "NOT_FOUND" + | "ALREADY_EXISTS" + | "BAD_REQUEST" + | "INTERNAL_ERROR" + | "FORBIDDEN"; + +export type ServiceResult = + | { success: true; data: T } + | { success: false; error: E; message?: string }; + +/** + * 성공적인 서비스 결과 객체를 생성합니다. + * @param data - 성공 시 반환할 데이터 + */ +export function serviceSuccess(data: T): ServiceResult { + return { success: true, data }; +} + +/** + * 실패 서비스 결과 객체를 생성합니다. + * @param error - 에러를 식별하는 코드 또는 문자열 + * @param message - 클라이언트에게 보여줄 수 있는 에러 메시지 + */ +export function serviceError( + error: E, + message?: string +): ServiceResult { + return { success: false, error, message }; +} + +/** + * 'NOT_FOUND' 에러 결과 객체를 생성합니다. + */ +export const serviceNotFound = (message = "리소스를 찾을 수 없습니다.") => + serviceError("NOT_FOUND", message); + +/** + * 'ALREADY_EXISTS' 에러 결과 객체를 생성합니다. + */ +export const serviceAlreadyExists = (message = "이미 존재하는 리소스입니다.") => + serviceError("ALREADY_EXISTS", message); + +/** + * 'BAD_REQUEST' 또는 유효성 검사 에러 결과 객체를 생성합니다. + */ +export const serviceBadRequest = (message = "잘못된 요청입니다.") => + serviceError("BAD_REQUEST", message); + +/** + * 'FORBIDDEN' 에러 결과 객체를 생성합니다. + */ +export const serviceForbidden = (message = "요청을 수행할 권한이 없습니다.") => + serviceError("FORBIDDEN", message); + +/** + * 예기치 않은 내부 서버 오류 결과 객체를 생성합니다. + */ +export function serviceInternalError( + error: unknown +): ServiceResult { + const err = error instanceof Error ? error : new Error(String(error)); + const message = err.message; + console.error("[INTERNAL_ERROR]", err); + return { success: false, error: "INTERNAL_ERROR", message }; +} diff --git a/src/server/utils/image.utils.ts b/src/server/utils/image.utils.ts new file mode 100644 index 00000000..78be0656 --- /dev/null +++ b/src/server/utils/image.utils.ts @@ -0,0 +1,20 @@ +import { ImageFormat } from "@prisma/client"; + +const IMAGE_FORMAT_MAP: Record = { + JPEG: ImageFormat.JPEG, + JPG: ImageFormat.JPEG, + PNG: ImageFormat.PNG, + WEBP: ImageFormat.WEBP, + HEIF: ImageFormat.HEIC, + HEIC: ImageFormat.HEIC, +} as const; + +/** + * 이미지 형식을 가져옵니다. + * @param formatString - 이미지 형식 문자열 + * @returns 이미지 형식 + */ +export function getImageFormat(formatString?: string): ImageFormat { + if (!formatString) return ImageFormat.UNKNOWN; + return IMAGE_FORMAT_MAP[formatString.toUpperCase()] ?? ImageFormat.UNKNOWN; +} diff --git a/src/shared/utils/validate.utils.ts b/src/shared/utils/validate.utils.ts new file mode 100644 index 00000000..3007579b --- /dev/null +++ b/src/shared/utils/validate.utils.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { + serviceBadRequest, + serviceSuccess, + ServiceResult, +} from "@/server/result"; + +/** + * UUID 형식이 유효한지 Zod를 사용하여 검사합니다. + * @param id - 검사할 ID + * @param resourceName - 오류 메시지에 사용할 리소스의 이름 + */ +export function validateUuid( + id: string, + resourceName: string +): ServiceResult { + const uuidSchema = z + .string() + .uuid({ message: `유효하지 않은 ${resourceName} ID 형식입니다.` }); + const result = uuidSchema.safeParse(id); + + if (!result.success) { + return serviceBadRequest(result.error.flatten().formErrors[0]); + } + return serviceSuccess(result.data); +} From f30e633b1c81cc09f4414d1adfc646e5a9633761 Mon Sep 17 00:00:00 2001 From: kuan Date: Thu, 26 Mar 2026 20:09:30 +0900 Subject: [PATCH 04/39] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 2 - package.json | 7 +- src/app/api-doc/layout.tsx | 3 +- src/app/api-doc/page.tsx | 1 + src/app/api/[[...route]]/route.ts | 12 - src/app/api/brands/[name]/route.ts | 212 ------------------ src/app/login-test/page.tsx | 28 --- src/client/utils/core-request/core.ts | 17 +- src/client/utils/core-request/token.ts | 3 +- .../domains/brandDetail/map/index.tsx | 2 +- src/server/hono/routes/v1/brand.handler.ts | 25 +++ src/server/hono/schemas/brand.schema.ts | 37 +++ .../services/__tests__/level.utils.test.ts | 84 ------- src/server/hono/services/brand.service.ts | 91 ++++++++ src/server/hono/services/review.service.ts | 43 ++-- yarn.lock | 120 +--------- 16 files changed, 186 insertions(+), 501 deletions(-) delete mode 100644 src/app/api/brands/[name]/route.ts delete mode 100644 src/app/login-test/page.tsx delete mode 100644 src/server/hono/services/__tests__/level.utils.test.ts diff --git a/next.config.ts b/next.config.ts index 9708020c..898d5463 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,8 +11,6 @@ const nextConfig: NextConfig = { serverExternalPackages: ["jsdom"], webpack: (config, { dev }) => { if (!dev) { - config.resolve.alias["@hono/swagger-ui"] = false; - config.resolve.alias["swagger-ui-dist"] = false; config.resolve.alias["swagger-ui-react"] = false; diff --git a/package.json b/package.json index 6e8c4da1..891f1e13 100644 --- a/package.json +++ b/package.json @@ -19,18 +19,14 @@ "enabled": false }, "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.3.4", "@auth/supabase-adapter": "^1.8.0", "@ckeditor/ckeditor5-react": "^9.5.0", - "@hono/node-server": "^1.14.4", - "@hono/swagger-ui": "^0.5.2", "@hono/zod-openapi": "^0.19.9", "@hono/zod-validator": "^0.4.3", "@hookform/resolvers": "^5.1.1", "@prisma/client": "6.19.0", "@supabase/supabase-js": "^2.46.2", "@tanstack/react-query": "^5.87.4", - "@tanstack/react-query-devtools": "^5.87.4", "chart.js": "^4.4.9", "chartjs-plugin-datalabels": "^2.2.0", "ckeditor5": "^45.2.0", @@ -39,7 +35,6 @@ "dayjs": "^1.11.13", "dompurify": "^3.3.1", "hono": "^4.8.3", - "hono-openapi": "^0.4.8", "jsdom": "^28.1.0", "lucide-react": "^0.462.0", "next": "15.0.5", @@ -58,7 +53,6 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "zod": "^3.25.67", - "zod-openapi": "^4.2.4", "zustand": "^5.0.1" }, "devDependencies": { @@ -72,6 +66,7 @@ "@storybook/react": "^8.5.8", "@storybook/test": "^8.5.8", "@svgr/webpack": "^8.1.0", + "@tanstack/react-query-devtools": "^5.87.4", "@types/dompurify": "^3.2.0", "@types/jsdom": "^27.0.0", "@types/navermaps": "^3.9.1", diff --git a/src/app/api-doc/layout.tsx b/src/app/api-doc/layout.tsx index dd030a7d..59a9448e 100644 --- a/src/app/api-doc/layout.tsx +++ b/src/app/api-doc/layout.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { notFound } from "next/navigation"; export default function ApiDocLayout({ children, @@ -6,7 +7,7 @@ export default function ApiDocLayout({ children: React.ReactNode; }) { if (process.env.NODE_ENV === "production") { - return null; + notFound(); } return <>{children}; } diff --git a/src/app/api-doc/page.tsx b/src/app/api-doc/page.tsx index bd56fdda..39bc851d 100644 --- a/src/app/api-doc/page.tsx +++ b/src/app/api-doc/page.tsx @@ -1,4 +1,5 @@ import SwaggerUI from "swagger-ui-react"; +import "swagger-ui-react/swagger-ui.css"; export default function Page() { return ( diff --git a/src/app/api/[[...route]]/route.ts b/src/app/api/[[...route]]/route.ts index 2d8ca568..81a88d11 100644 --- a/src/app/api/[[...route]]/route.ts +++ b/src/app/api/[[...route]]/route.ts @@ -3,18 +3,6 @@ import { handle } from "hono/vercel"; // export const runtime = "edge"; -if (process.env.NODE_ENV !== "production") { - try { - const { swaggerUI } = await import("@hono/swagger-ui"); - app.get("/swagger-ui", swaggerUI({ url: "/api/doc" })); - } catch (_) { - console.warn( - "Swagger UI dependencies not found in production environment.", - _, - ); - } -} - export const GET = handle(app); export const POST = handle(app); export const PUT = handle(app); diff --git a/src/app/api/brands/[name]/route.ts b/src/app/api/brands/[name]/route.ts deleted file mode 100644 index 528f2a8f..00000000 --- a/src/app/api/brands/[name]/route.ts +++ /dev/null @@ -1,212 +0,0 @@ -// app/api/brands/[name]/route.ts -import { Store } from "@/components/domains/brandDetail/map"; -import { NextRequest, NextResponse } from "next/server"; - -interface NaverLocalItem { - title: string; - address: string; - roadAddress: string; - telephone: string; - mapx: string; - mapy: string; - category: string; - link: string; -} - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ name: string }> } -) { - try { - // Next.js 15: params를 await - const { name } = await params; - - const searchParams = request.nextUrl.searchParams; - const x = searchParams.get("x"); - const y = searchParams.get("y"); - - if (!name) { - return NextResponse.json( - { error: "Brand name is required" }, - { status: 400 } - ); - } - - if (!process.env.NAVER_CLIENT_ID || !process.env.NAVER_CLIENT_SECRET) { - return NextResponse.json( - { - error: "Server configuration error", - details: "Naver API credentials are missing in .env.local", - }, - { status: 500 } - ); - } - - // API URL 구성 - const apiUrl = `https://openapi.naver.com/v1/search/local.json?query=${encodeURIComponent( - name - )}&display=20&sort=random`; - - const response = await fetch(apiUrl, { - method: "GET", - headers: { - "X-Naver-Client-Id": process.env.NAVER_CLIENT_ID!, - "X-Naver-Client-Secret": process.env.NAVER_CLIENT_SECRET!, - }, - }); - - // 에러 응답 상세 처리 - if (!response.ok) { - const errorBody = await response.text(); - - // 네이버 API 에러 메시지 파싱 시도 - let errorMessage = `Naver API error: ${response.status}`; - let errorDetails = errorBody; - - try { - const errorJson = JSON.parse(errorBody); - errorMessage = errorJson.errorMessage || errorMessage; - errorDetails = errorJson.errorCode - ? `${errorJson.errorCode}: ${errorJson.errorMessage}` - : errorBody; - } catch { - // JSON 파싱 실패 시 원본 텍스트 사용 - } - - // 상태 코드별 구체적인 에러 메시지 - if (response.status === 400) { - return NextResponse.json( - { - error: "Bad Request", - details: "Invalid request parameters", - naverError: errorDetails, - }, - { status: 400 } - ); - } else if (response.status === 401) { - return NextResponse.json( - { - error: "Authentication Failed", - details: - "Invalid Naver API credentials. Please check your Client ID and Secret in .env.local", - naverError: errorDetails, - }, - { status: 401 } - ); - } else if (response.status === 403) { - return NextResponse.json( - { - error: "Forbidden", - details: - "API access denied. Check if the Search API is enabled in your Naver app", - naverError: errorDetails, - }, - { status: 403 } - ); - } else if (response.status === 429) { - return NextResponse.json( - { - error: "Rate Limit Exceeded", - details: "Too many requests. Please try again later", - naverError: errorDetails, - }, - { status: 429 } - ); - } else { - return NextResponse.json( - { - error: errorMessage, - details: errorDetails, - status: response.status, - }, - { status: response.status } - ); - } - } - - // 성공 응답 처리 - const data = await response.json(); - - // 데이터가 없는 경우 - if (!data.items || data.items.length === 0) { - return NextResponse.json({ - success: true, - stores: [], - total: 0, - message: `No stores found for "${name}"`, - }); - } - - // 데이터 정제 - const stores = data.items.map((item: NaverLocalItem, index: number) => { - console.log(` - Item ${index + 1}:`, item.title?.substring(0, 30)); - return { - name: item.title?.replace(/<[^>]*>/g, "") || "Unknown", - address: item.address || "", - roadAddress: item.roadAddress || "", - telephone: item.telephone || "", - x: item.mapx || "0", - y: item.mapy || "0", - category: item.category || "", - link: item.link || "", - }; - }); - - // 거리 계산 - if (x && y) { - const userLat = parseFloat(y); - const userLng = parseFloat(x); - - stores.forEach((store: Store) => { - const storeLat = parseInt(store.y) / 10000000; - const storeLng = parseInt(store.x) / 10000000; - - const R = 6371; // km - const dLat = ((storeLat - userLat) * Math.PI) / 180; - const dLng = ((storeLng - userLng) * Math.PI) / 180; - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos((userLat * Math.PI) / 180) * - Math.cos((storeLat * Math.PI) / 180) * - Math.sin(dLng / 2) * - Math.sin(dLng / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - store.distance = Math.round(R * c * 1000); // meters - }); - - stores.sort( - (a: Store, b: Store) => (a.distance || 0) - (b.distance || 0) - ); - } - - return NextResponse.json({ - success: true, - stores, - total: data.total, - }); - } catch (error) { - console.error("=== API Route Error ==="); - console.error( - "Error type:", - error instanceof Error ? error.constructor.name : typeof error - ); - console.error( - "Error message:", - error instanceof Error ? error.message : String(error) - ); - console.error( - "Error stack:", - error instanceof Error ? error.stack : "No stack" - ); - console.error("======================="); - - return NextResponse.json( - { - error: "Internal server error", - message: error instanceof Error ? error.message : "Unknown error", - type: error instanceof Error ? error.constructor.name : typeof error, - }, - { status: 500 } - ); - } -} diff --git a/src/app/login-test/page.tsx b/src/app/login-test/page.tsx deleted file mode 100644 index 260b51d6..00000000 --- a/src/app/login-test/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { LoginModal } from "@/components/modal/LoginModal"; -import { logout } from "@/server/database/action/login"; -import { useState } from "react"; - -export default function LoginTestPage() { - const [openModal, setOpenModal] = useState(false); - - const handleOpenModal = () => { - setOpenModal((prev) => !prev); - }; - return ( - <> - - {openModal && ( - { - setOpenModal(false); - }} - /> - )} -
- -
- - ); -} diff --git a/src/client/utils/core-request/core.ts b/src/client/utils/core-request/core.ts index a91c0a27..1de47f34 100644 --- a/src/client/utils/core-request/core.ts +++ b/src/client/utils/core-request/core.ts @@ -7,12 +7,14 @@ export interface CustomRequestInit extends RequestInit { } type RequestInterceptor = ( - config: CustomRequestInit + config: CustomRequestInit, ) => Promise | CustomRequestInit; type ResponseInterceptor = (response: Response) => Promise | Response; -export interface RequestOptions - extends CustomRequestInit { +export interface RequestOptions< + ResponseData, + TransformedData, +> extends CustomRequestInit { url: string; isRetry?: boolean; transformResponse?: (response: ResponseData) => TransformedData; @@ -23,7 +25,7 @@ export class APIError extends Error { constructor( public status: number, public message: string, - public errorData?: unknown + public errorData?: unknown, ) { super(message); this.name = "APIError"; @@ -47,7 +49,7 @@ export function createCoreClient(config: LibraryConfig) { // HTTP 요청을 수행하는 함수 const request = async ( - originalOptions: RequestOptions + originalOptions: RequestOptions, ): Promise => { const { url, transformResponse, ...options } = originalOptions; @@ -63,7 +65,7 @@ export function createCoreClient(config: LibraryConfig) { try { console.log( `[Request] Fetch options:`, - JSON.stringify(reqConfig, null, 2) + JSON.stringify(reqConfig, null, 2), ); } catch { console.log(`[Request] Fetch options:`, reqConfig); @@ -73,8 +75,7 @@ export function createCoreClient(config: LibraryConfig) { let response = await fetch(url, { ...reqConfig, headers, - cache: reqConfig.cache || "force-cache", - next: { revalidate: 300 }, + cache: reqConfig.cache, }); if (response.status === 401 && !options.isRetry && config.refreshToken) { diff --git a/src/client/utils/core-request/token.ts b/src/client/utils/core-request/token.ts index 15d90d21..0b556c10 100644 --- a/src/client/utils/core-request/token.ts +++ b/src/client/utils/core-request/token.ts @@ -13,8 +13,9 @@ export function manageTokenRefresh(refreshToken: RefreshFn): Promise { isRefreshing = true; refreshPromise = refreshToken().finally(() => { isRefreshing = false; + refreshPromise = null; }); } - return refreshPromise!; + return refreshPromise ?? Promise.resolve(); } diff --git a/src/components/domains/brandDetail/map/index.tsx b/src/components/domains/brandDetail/map/index.tsx index 665eae83..a0f37c60 100644 --- a/src/components/domains/brandDetail/map/index.tsx +++ b/src/components/domains/brandDetail/map/index.tsx @@ -63,7 +63,7 @@ export const BrandMap = () => { y: userLocation.lat.toString(), }); - const response = await fetch(`/api/brands/${brandName}?${params}`); + const response = await fetch(`/api/v1/brands/stores/${brandName}?${params}`); if (!response.ok) { throw new Error("Search failed"); diff --git a/src/server/hono/routes/v1/brand.handler.ts b/src/server/hono/routes/v1/brand.handler.ts index 300c9f3d..f2d9aa9a 100644 --- a/src/server/hono/routes/v1/brand.handler.ts +++ b/src/server/hono/routes/v1/brand.handler.ts @@ -11,6 +11,7 @@ import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema"; import { createDomainRouters } from "@/server/hono/utils/router"; const routers = createDomainRouters(); +const publicRouter = routers.public; /** * @method GET @@ -104,4 +105,28 @@ routers.optionalAuth.openapi(getBrandByNameRoute, async (c) => { return apiSuccess(c, result.data, "브랜드 정보를 성공적으로 불러왔습니다."); }); +/** + * @method GET + * @path /stores/:name + * @description 네이버 지역 검색 API를 통해 브랜드 매장 목록을 조회합니다. + * @summary 브랜드 매장 검색 + */ +publicRouter.get("/stores/:name", async (c) => { + const name = c.req.param("name"); + const x = c.req.query("x"); + const y = c.req.query("y"); + + const coords = x && y ? { x, y } : undefined; + const result = await BrandServices.getStoresByNameService(name, coords); + + if (!result.success) { + return apiInternalError(c, result.message); + } + + return c.json( + { success: true, stores: result.data.stores, total: result.data.total }, + 200 + ); +}); + export default routers.merge(); diff --git a/src/server/hono/schemas/brand.schema.ts b/src/server/hono/schemas/brand.schema.ts index aa9b4fa8..f5201452 100644 --- a/src/server/hono/schemas/brand.schema.ts +++ b/src/server/hono/schemas/brand.schema.ts @@ -62,6 +62,41 @@ export const ApiBrandForEmbeddingResponseSchema = BrandSchema.pick({ brandUrl: true, }); +export const StoreSchema = z + .object({ + name: z.string(), + address: z.string(), + roadAddress: z.string().optional(), + telephone: z.string().optional(), + x: z.string(), + y: z.string(), + category: z.string().optional(), + link: z.string().optional(), + distance: z.number().optional(), + }) + .openapi("Store"); + +export const BrandStoreNameParamSchema = z.object({ + name: z.string().openapi({ + param: { name: "name", in: "path" }, + example: "딥티크", + description: "검색할 브랜드 이름", + }), +}); + +export const BrandStoreQuerySchema = z.object({ + x: z.string().optional().openapi({ description: "사용자 경도" }), + y: z.string().optional().openapi({ description: "사용자 위도" }), +}); + +export const BrandStoresResponseSchema = z + .object({ + success: z.literal(true), + stores: z.array(StoreSchema), + total: z.number(), + }) + .openapi("BrandStoresResponse"); + export type ApiBrandSimpleResponse = z.infer< typeof ApiBrandSimpleResponseSchema >; @@ -69,3 +104,5 @@ export type ApiBrandSimpleResponse = z.infer< export type ApiBrandDetailResponse = z.infer< typeof ApiBrandDetailResponseSchema >; + +export type Store = z.infer; diff --git a/src/server/hono/services/__tests__/level.utils.test.ts b/src/server/hono/services/__tests__/level.utils.test.ts deleted file mode 100644 index e61d9892..00000000 --- a/src/server/hono/services/__tests__/level.utils.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - calculateLevel, - getPointsForNextLevel, - getLevelProgress, - getPointsForLevel, - POINTS_PER_LEVEL, -} from "@/shared/utils/level.utils"; - -/** - * 레벨 유틸리티 함수 테스트 - * - * 테스트 전략: - * - 경계값을 중심으로 3개씩 간결하게 테스트 - * - 최소값, 경계값, 일반값으로 구성 - */ - -describe("Level Utils - calculateLevel", () => { - it("경계값 테스트: 0, 99, 100", () => { - expect(calculateLevel(0)).toBe(0); // 최소값 - expect(calculateLevel(99)).toBe(0); // 레벨 올리기 직전 - expect(calculateLevel(100)).toBe(1); // 레벨 업 - }); - - it("일반값 테스트: 150, 1000, 음수", () => { - expect(calculateLevel(150)).toBe(1); // 중간값 - expect(calculateLevel(1000)).toBe(10); // 큰 값 - expect(calculateLevel(-100)).toBe(0); // 음수 - }); -}); - -describe("Level Utils - getPointsForNextLevel", () => { - it("경계값 테스트: 0, 99, 100", () => { - expect(getPointsForNextLevel(0)).toBe(100); // 시작점 - expect(getPointsForNextLevel(99)).toBe(1); // 레벨 업 직전 - expect(getPointsForNextLevel(100)).toBe(100); // 레벨 시작점 - }); - - it("일반값 테스트: 50, 150, 음수", () => { - expect(getPointsForNextLevel(50)).toBe(50); // 중간 - expect(getPointsForNextLevel(150)).toBe(50); // 다음 레벨 중간 - expect(getPointsForNextLevel(-100)).toBe(100); // 음수 - }); -}); - -describe("Level Utils - getLevelProgress", () => { - it("경계값 테스트: 0, 99, 100", () => { - expect(getLevelProgress(0)).toBe(0); // 시작점 - expect(getLevelProgress(99)).toBe(99); // 거의 끝 - expect(getLevelProgress(100)).toBe(0); // 다음 레벨 시작 - }); - - it("일반값 테스트: 50, 150, 음수", () => { - expect(getLevelProgress(50)).toBe(50); // 중간 - expect(getLevelProgress(150)).toBe(50); // 다음 레벨 중간 - expect(getLevelProgress(-100)).toBe(0); // 음수 - }); -}); - -describe("Level Utils - getPointsForLevel", () => { - it("경계값 테스트: 0, 1, 10", () => { - expect(getPointsForLevel(0)).toBe(0); // 최소 레벨 - expect(getPointsForLevel(1)).toBe(100); // 첫 레벨 - expect(getPointsForLevel(10)).toBe(1000); // 일반 레벨 - }); - - it("특수값 테스트: 100, -1", () => { - expect(getPointsForLevel(100)).toBe(10000); // 큰 레벨 - expect(getPointsForLevel(-1)).toBe(0); // 음수 레벨 - }); -}); - -describe("Level Utils - 통합 시나리오", () => { - it("포인트 1050일 때 모든 함수가 일관되게 동작", () => { - const points = 1050; - expect(calculateLevel(points)).toBe(10); - expect(getPointsForNextLevel(points)).toBe(50); - expect(getLevelProgress(points)).toBe(50); - }); - - it("상수 값 검증", () => { - expect(POINTS_PER_LEVEL).toBe(100); - }); -}); diff --git a/src/server/hono/services/brand.service.ts b/src/server/hono/services/brand.service.ts index 7c9792c4..72c12825 100644 --- a/src/server/hono/services/brand.service.ts +++ b/src/server/hono/services/brand.service.ts @@ -8,9 +8,21 @@ import { import { ApiBrandDetailResponse, ApiBrandSimpleResponse, + Store, } from "../schemas/brand.schema"; import { brandDetailSelect, parseMapLocation } from "../repositories/brand.repository"; +interface NaverLocalItem { + title: string; + address: string; + roadAddress: string; + telephone: string; + mapx: string; + mapy: string; + category: string; + link: string; +} + /** * 모든 브랜드 목록 조회 * - 브랜드 필터링, 검색 등에 사용 @@ -113,3 +125,82 @@ export async function getBrandByNameService( return serviceInternalError(error); } } + +/** + * 네이버 지역 검색 API를 통해 브랜드 매장 목록 조회 + * @param name 검색할 브랜드 이름 + * @param coords 사용자 위치 좌표 (거리순 정렬에 사용) + */ +export async function getStoresByNameService( + name: string, + coords?: { x: string; y: string } +): Promise> { + try { + if (!process.env.NAVER_CLIENT_ID || !process.env.NAVER_CLIENT_SECRET) { + return serviceInternalError( + new Error("Naver API credentials are missing") + ); + } + + const apiUrl = `https://openapi.naver.com/v1/search/local.json?query=${encodeURIComponent(name)}&display=20&sort=random`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + "X-Naver-Client-Id": process.env.NAVER_CLIENT_ID, + "X-Naver-Client-Secret": process.env.NAVER_CLIENT_SECRET, + }, + }); + + if (!response.ok) { + return serviceInternalError( + new Error(`Naver API error: ${response.status}`) + ); + } + + const data = await response.json(); + + if (!data.items || data.items.length === 0) { + return serviceSuccess({ stores: [], total: 0 }); + } + + const stores: Store[] = data.items.map((item: NaverLocalItem) => ({ + name: item.title?.replace(/<[^>]*>/g, "") || "Unknown", + address: item.address || "", + roadAddress: item.roadAddress || "", + telephone: item.telephone || "", + x: item.mapx || "0", + y: item.mapy || "0", + category: item.category || "", + link: item.link || "", + })); + + if (coords) { + const userLat = parseFloat(coords.y); + const userLng = parseFloat(coords.x); + + stores.forEach((store) => { + const storeLat = parseInt(store.y) / 10000000; + const storeLng = parseInt(store.x) / 10000000; + + const R = 6371; + const dLat = ((storeLat - userLat) * Math.PI) / 180; + const dLng = ((storeLng - userLng) * Math.PI) / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos((userLat * Math.PI) / 180) * + Math.cos((storeLat * Math.PI) / 180) * + Math.sin(dLng / 2) * + Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + store.distance = Math.round(R * c * 1000); + }); + + stores.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0)); + } + + return serviceSuccess({ stores, total: data.total }); + } catch (error) { + return serviceInternalError(error); + } +} diff --git a/src/server/hono/services/review.service.ts b/src/server/hono/services/review.service.ts index 0f73fe1f..1550f3bd 100644 --- a/src/server/hono/services/review.service.ts +++ b/src/server/hono/services/review.service.ts @@ -18,32 +18,20 @@ import { reviewIncludeArgs, FullReview } from "../repositories/review.repository const POPULAR_REVIEW_LIMIT = 5; const POPULAR_REVIEW_POOL_SIZE = 20; - -let optionMap = new Map(); - -async function initializeOptionMap() { - try { - const allOptions = await prisma.attributeOption.findMany({ - include: { attribute: true }, - }); - const newMap = new Map(); - allOptions.forEach((opt) => { - const key = `${opt.attribute.key}-${opt.value}`; - newMap.set(key, opt.id); - }); - optionMap = newMap; - } catch (error) { - console.error("Failed to initialize AttributeOption map:", error); - } -} -// 서버 시작 시 한번만 실행되도록 호출 -initializeOptionMap(); - const DEFAULT_REVIEW_LIMIT = 12; -function getOptionIdsFromAttributes( +async function getOptionIdsFromAttributes( attributes: CreateReviewInput["attributes"] -): number[] { +): Promise { + const allOptions = await prisma.attributeOption.findMany({ + include: { attribute: true }, + }); + const optionMap = new Map(); + allOptions.forEach((opt) => { + const key = `${opt.attribute.key}-${opt.value}`; + optionMap.set(key, opt.id); + }); + const selectionIds: number[] = []; for (const [key, value] of Object.entries(attributes)) { if (!value) continue; @@ -162,7 +150,7 @@ export async function createReviewService( return serviceAlreadyExists("이미 이 향수에 대한 리뷰를 작성하셨습니다."); } - const selectionIds = getOptionIdsFromAttributes(attributes); + const selectionIds = await getOptionIdsFromAttributes(attributes); // 트랜잭션 시작 const newReview = await prisma.$transaction(async (tx) => { @@ -210,6 +198,10 @@ export async function updateReviewService( const { content, usageStatus, attributes } = updateData; + const selectionIds = attributes + ? await getOptionIdsFromAttributes(attributes as CreateReviewInput["attributes"]) + : []; + const updatedReview = await prisma.$transaction(async (tx) => { if (content || usageStatus) { await tx.review.update({ @@ -223,9 +215,6 @@ export async function updateReviewService( if (attributes) { await tx.reviewAttributeSelection.deleteMany({ where: { reviewId } }); - const selectionIds = getOptionIdsFromAttributes( - attributes as CreateReviewInput["attributes"] - ); if (selectionIds.length > 0) { await tx.reviewAttributeSelection.createMany({ diff --git a/yarn.lock b/yarn.lock index a1db8b9f..c388ccf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,17 +19,6 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^11.1.0": - version: 11.9.3 - resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" - dependencies: - "@jsdevtools/ono": "npm:^7.1.3" - "@types/json-schema": "npm:^7.0.15" - js-yaml: "npm:^4.1.0" - checksum: 10/3d3618dbb611d1296b99bdee4ff0dde664dad47632d30e0310c6d10de8081f6378ccb58329ea4e03103eca9347d5143671d03f0527b1c3f0916d95f8c09215e2 - languageName: node - linkType: hard - "@asamuzakjp/css-color@npm:^3.2.0": version: 3.2.0 resolution: "@asamuzakjp/css-color@npm:3.2.0" @@ -43,7 +32,7 @@ __metadata: languageName: node linkType: hard -"@asteasolutions/zod-to-openapi@npm:^7.3.0, @asteasolutions/zod-to-openapi@npm:^7.3.4": +"@asteasolutions/zod-to-openapi@npm:^7.3.0": version: 7.3.4 resolution: "@asteasolutions/zod-to-openapi@npm:7.3.4" dependencies: @@ -3623,24 +3612,6 @@ __metadata: languageName: node linkType: hard -"@hono/node-server@npm:^1.14.4": - version: 1.19.4 - resolution: "@hono/node-server@npm:1.19.4" - peerDependencies: - hono: ^4 - checksum: 10/65ef565c60cf1fa5670d37282f209f956d86ee04f48199b7e92c17159d62621e874a06f768484bac901307a1f68f2eba613d2d37dba6040e6f5f74e5be714242 - languageName: node - linkType: hard - -"@hono/swagger-ui@npm:^0.5.2": - version: 0.5.2 - resolution: "@hono/swagger-ui@npm:0.5.2" - peerDependencies: - hono: "*" - checksum: 10/4afb4cf2611ec28e07ee813e3b4d7707e32e78823ea10b4a2fe93f351b2e70789f7834d0c7948b771c4ca9b0a8763fa5025b984f808bce61b70dc149e6976516 - languageName: node - linkType: hard - "@hono/zod-openapi@npm:^0.19.9": version: 0.19.10 resolution: "@hono/zod-openapi@npm:0.19.10" @@ -3973,13 +3944,6 @@ __metadata: languageName: node linkType: hard -"@jsdevtools/ono@npm:^7.1.3": - version: 7.1.3 - resolution: "@jsdevtools/ono@npm:7.1.3" - checksum: 10/d4a036ccb9d2b21b7e4cec077c59a5a83fad58adacbce89e7e6b77a703050481ff5b6d813aef7f5ff0a8347a85a0eedf599e2e6bb5784a971a93e53e43b10157 - languageName: node - linkType: hard - "@kurkle/color@npm:^0.3.0": version: 0.3.4 resolution: "@kurkle/color@npm:0.3.4" @@ -8146,13 +8110,6 @@ __metadata: languageName: node linkType: hard -"clone@npm:^2.1.2": - version: 2.1.2 - resolution: "clone@npm:2.1.2" - checksum: 10/d9c79efba655f0bf601ab299c57eb54cbaa9860fb011aee9d89ed5ac0d12df1660ab7642fddaabb9a26b7eff0e117d4520512cb70798319ff5d30a111b5310c2 - languageName: node - linkType: hard - "clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" @@ -10684,57 +10641,6 @@ __metadata: languageName: node linkType: hard -"hono-openapi@npm:^0.4.8": - version: 0.4.8 - resolution: "hono-openapi@npm:0.4.8" - dependencies: - json-schema-walker: "npm:^2.0.0" - peerDependencies: - "@hono/arktype-validator": ^2.0.0 - "@hono/effect-validator": ^1.2.0 - "@hono/typebox-validator": ^0.2.0 || ^0.3.0 - "@hono/valibot-validator": ^0.5.1 - "@hono/zod-validator": ^0.4.1 - "@sinclair/typebox": ^0.34.9 - "@valibot/to-json-schema": ^1.0.0-beta.3 - arktype: ^2.0.0 - effect: ^3.11.3 - hono: ^4.6.13 - openapi-types: ^12.1.3 - valibot: ^1.0.0-beta.9 - zod: ^3.23.8 - zod-openapi: ^4.0.0 - peerDependenciesMeta: - "@hono/arktype-validator": - optional: true - "@hono/effect-validator": - optional: true - "@hono/typebox-validator": - optional: true - "@hono/valibot-validator": - optional: true - "@hono/zod-validator": - optional: true - "@sinclair/typebox": - optional: true - "@valibot/to-json-schema": - optional: true - arktype: - optional: true - effect: - optional: true - hono: - optional: true - valibot: - optional: true - zod: - optional: true - zod-openapi: - optional: true - checksum: 10/560203307a6cbd8159a7607dc49de3bc845f23883ea4e161f2762414d4ba424c52089f711798af5e2c801b643c64b27c98722e2db49a0e54b921cd63b75dcc09 - languageName: node - linkType: hard - "hono@npm:^4.8.3": version: 4.9.8 resolution: "hono@npm:4.9.8" @@ -11561,16 +11467,6 @@ __metadata: languageName: node linkType: hard -"json-schema-walker@npm:^2.0.0": - version: 2.0.0 - resolution: "json-schema-walker@npm:2.0.0" - dependencies: - "@apidevtools/json-schema-ref-parser": "npm:^11.1.0" - clone: "npm:^2.1.2" - checksum: 10/4b26aa732918f6c458646aeeb757f6b6b82329ac20c758fc6bba178129c1b41f07db0cac8d56d6d3fcaa6202f0931de4b632ad240d1f1699dc631bee7f0c9d92 - languageName: node - linkType: hard - "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -13000,12 +12896,9 @@ __metadata: version: 0.0.0-use.local resolution: "perscent@workspace:." dependencies: - "@asteasolutions/zod-to-openapi": "npm:^7.3.4" "@auth/supabase-adapter": "npm:^1.8.0" "@chromatic-com/storybook": "npm:^3.2.4" "@ckeditor/ckeditor5-react": "npm:^9.5.0" - "@hono/node-server": "npm:^1.14.4" - "@hono/swagger-ui": "npm:^0.5.2" "@hono/zod-openapi": "npm:^0.19.9" "@hono/zod-validator": "npm:^0.4.3" "@hookform/resolvers": "npm:^5.1.1" @@ -13043,7 +12936,6 @@ __metadata: eslint-config-next: "npm:15.0.3" eslint-plugin-storybook: "npm:^0.11.3" hono: "npm:^4.8.3" - hono-openapi: "npm:^0.4.8" jsdom: "npm:^28.1.0" lucide-react: "npm:^0.462.0" next: "npm:15.0.5" @@ -13070,7 +12962,6 @@ __metadata: uuid: "npm:^11.1.0" vitest: "npm:^4.0.9" zod: "npm:^3.25.67" - zod-openapi: "npm:^4.2.4" zod-prisma-types: "npm:^3.2.4" zustand: "npm:^5.0.1" languageName: unknown @@ -16899,15 +16790,6 @@ __metadata: languageName: node linkType: hard -"zod-openapi@npm:^4.2.4": - version: 4.2.4 - resolution: "zod-openapi@npm:4.2.4" - peerDependencies: - zod: ^3.21.4 - checksum: 10/526c6713f05212cd170e449cd224276c97deb7a9e4d3a5649adc534c5a30b72f952456aac35f2bea3943da861d52c90feb0a90c1f63423ad039de3bef7c7c2b1 - languageName: node - linkType: hard - "zod-prisma-types@npm:^3.2.4": version: 3.2.4 resolution: "zod-prisma-types@npm:3.2.4" From c162dc896d699065adb4fb29948c8f47d81be0cc Mon Sep 17 00:00:00 2001 From: kuan Date: Thu, 26 Mar 2026 20:35:18 +0900 Subject: [PATCH 05/39] =?UTF-8?q?refactor:=20GlobalStateSync=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=ED=9B=84=20useSession=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 8 +- src/client/stores/useUserStore.ts | 28 +++++-- .../commons/Provider/GlobalStateSync.tsx | 73 ------------------- .../commons/Provider/RecentSyncManager.tsx | 27 +++++++ .../commons/Provider/TanstackProvider.tsx | 11 ++- src/components/commons/bottomNavBar/index.tsx | 2 +- .../sections/ProfileHeaderSection/index.tsx | 2 +- src/components/domains/user/tabs/MainTabs.tsx | 2 +- .../domains/user/tabs/MobileTabsAccordion.tsx | 2 +- 9 files changed, 59 insertions(+), 96 deletions(-) delete mode 100644 src/components/commons/Provider/GlobalStateSync.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c2a0ffe1..31267d9a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,8 @@ import type { Metadata, Viewport } from "next"; -import { auth } from "@/auth"; import Providers from "@/components/commons/Provider/TanstackProvider"; import { BodyWrapper } from "@/components/commons/wrapper/BodyWrapper"; import { NavBarWrapper } from "@/components/commons/navBar/Wrapper"; import LoginModalProvider from "@/components/modal/LoginModalProvider"; -import GlobalStateSync from "@/components/commons/Provider/GlobalStateSync"; import RecentSyncManager from "@/components/commons/Provider/RecentSyncManager"; import "./globals.css"; @@ -80,14 +78,11 @@ export const metadata: Metadata = { }, }; -export default async function RootLayout({ +export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const session = await auth(); - const isAuthenticated = !!session?.user; - return ( @@ -118,7 +113,6 @@ export default async function RootLayout({ - {children} diff --git a/src/client/stores/useUserStore.ts b/src/client/stores/useUserStore.ts index 8173ee3a..de751fd7 100644 --- a/src/client/stores/useUserStore.ts +++ b/src/client/stores/useUserStore.ts @@ -1,16 +1,28 @@ -import { create } from "zustand"; +"use client"; + +import { useSession } from "next-auth/react"; +import { useUserProfile } from "@/client/hooks/query/useUserQuery"; import { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; interface UserState { user: ApiMyProfileResponse | null; isLoading: boolean; - setUser: (user: ApiMyProfileResponse | null) => void; reset: () => void; } -export const useUserStore = create((set) => ({ - user: null, - isLoading: true, - setUser: (user) => set({ user, isLoading: false }), - reset: () => set({ user: null, isLoading: false }), -})); +export const useUserStore = (): UserState => { + const { status } = useSession(); + const isAuthenticated = status === "authenticated"; + + const { data: userProfile, isFetched } = useUserProfile({ + enabled: isAuthenticated, + }); + + const isLoading = status === "loading" || (isAuthenticated && !isFetched); + const user = isAuthenticated && isFetched ? (userProfile ?? null) : null; + + // 로그아웃 후 window.location.reload()가 호출되므로 no-op + const reset = () => {}; + + return { user, isLoading, reset }; +}; diff --git a/src/components/commons/Provider/GlobalStateSync.tsx b/src/components/commons/Provider/GlobalStateSync.tsx deleted file mode 100644 index 46d1dcaa..00000000 --- a/src/components/commons/Provider/GlobalStateSync.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useUserProfile } from "@/client/hooks/query/useUserQuery"; -import { useUserStore } from "@/client/stores/useUserStore"; -import { pointApi } from "@/client/utils/api/points.api"; - -interface GlobalStateSyncProps { - isAuthenticated: boolean; -} - -//전역으로 사용하는 상태 동기화 -export default function GlobalStateSync({ isAuthenticated }: GlobalStateSyncProps) { - const { setUser, reset } = useUserStore(); - - // 서버에서 인증된 경우에만 프로필 조회 - const { data: userProfile, isFetched } = useUserProfile({ - enabled: isAuthenticated, - }); - - // 인증되지 않은 경우 즉시 reset - useEffect(() => { - if (!isAuthenticated) { - reset(); - } - }, [isAuthenticated, reset]); - - // 사용자 프로필 동기화 - useEffect(() => { - // 인증된 경우에만 프로필 동기화 - if (isAuthenticated && isFetched) { - if (userProfile) { - setUser(userProfile); - } else { - reset(); - } - } - }, [isAuthenticated, isFetched, userProfile, setUser, reset]); - - // 일일 로그인 체크 - useEffect(() => { - // 사용자가 로그인하지 않았거나 프로필이 없으면 스킵 - if (!isFetched || !userProfile) { - return; - } - - const checkDailyLogin = async () => { - try { - const lastCheck = localStorage.getItem("lastLoginCheck"); - const today = new Date().toDateString(); - - // 오늘 이미 체크했다면 스킵 - if (lastCheck === today) { - return; - } - - // 서버에 로그인 처리 요청 - const response = await pointApi.processLogin(); - if (response?.success) { - localStorage.setItem("lastLoginCheck", today); - } - } catch (error) { - if (process.env.NODE_ENV === "development") { - console.error("❌ [Daily Login] Failed:", error); - } - } - }; - - checkDailyLogin(); - }, [isFetched, userProfile]); - - return null; -} diff --git a/src/components/commons/Provider/RecentSyncManager.tsx b/src/components/commons/Provider/RecentSyncManager.tsx index be740af3..11007a37 100644 --- a/src/components/commons/Provider/RecentSyncManager.tsx +++ b/src/components/commons/Provider/RecentSyncManager.tsx @@ -1,9 +1,11 @@ "use client"; +import { useEffect } from "react"; import { useRecentItemsSync } from "@/client/hooks/useRecentItemsSync"; import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; import { useUserStore } from "@/client/stores/useUserStore"; +import { pointApi } from "@/client/utils/api/points.api"; function RecentSyncManager() { const { user, isLoading } = useUserStore(); @@ -24,6 +26,31 @@ function RecentSyncManager() { enabled: shouldSync, }); + // 일일 로그인 체크 + useEffect(() => { + if (!shouldSync) return; + + const checkDailyLogin = async () => { + try { + const lastCheck = localStorage.getItem("lastLoginCheck"); + const today = new Date().toDateString(); + + if (lastCheck === today) return; + + const response = await pointApi.processLogin(); + if (response?.success) { + localStorage.setItem("lastLoginCheck", today); + } + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error("❌ [Daily Login] Failed:", error); + } + } + }; + + checkDailyLogin(); + }, [shouldSync]); + return null; } diff --git a/src/components/commons/Provider/TanstackProvider.tsx b/src/components/commons/Provider/TanstackProvider.tsx index d2945d61..c418a916 100644 --- a/src/components/commons/Provider/TanstackProvider.tsx +++ b/src/components/commons/Provider/TanstackProvider.tsx @@ -1,6 +1,7 @@ "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { SessionProvider } from "next-auth/react"; import dynamic from "next/dynamic"; import { useState } from "react"; @@ -38,9 +39,11 @@ export default function Providers({ children }: { children: React.ReactNode }) { ); return ( - - {children} - - + + + {children} + + + ); } diff --git a/src/components/commons/bottomNavBar/index.tsx b/src/components/commons/bottomNavBar/index.tsx index 10077917..3b05ef4a 100644 --- a/src/components/commons/bottomNavBar/index.tsx +++ b/src/components/commons/bottomNavBar/index.tsx @@ -7,7 +7,7 @@ import { usePathname } from "next/navigation"; export const BottomNavBar = () => { const pathname = usePathname(); - const user = useUserStore((state) => state.user); + const { user } = useUserStore(); return (
diff --git a/src/components/domains/user/sections/ProfileHeaderSection/index.tsx b/src/components/domains/user/sections/ProfileHeaderSection/index.tsx index bf369619..b079118f 100644 --- a/src/components/domains/user/sections/ProfileHeaderSection/index.tsx +++ b/src/components/domains/user/sections/ProfileHeaderSection/index.tsx @@ -6,7 +6,7 @@ import { useUserStore } from "@/client/stores/useUserStore"; import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema"; const UserHeader = ({ user }: { user: ApiMyProfileResponse }) => { - const isLoading = useUserStore((state) => state.isLoading); + const { isLoading } = useUserStore(); if (!user || isLoading) { return ; diff --git a/src/components/domains/user/tabs/MainTabs.tsx b/src/components/domains/user/tabs/MainTabs.tsx index eded9491..a5146fcb 100644 --- a/src/components/domains/user/tabs/MainTabs.tsx +++ b/src/components/domains/user/tabs/MainTabs.tsx @@ -18,7 +18,7 @@ const MainTabs = ({ onAddPhotoClick: () => void; children?: React.ReactNode; }) => { - const user = useUserStore((state) => state.user); + const { user } = useUserStore(); const tabItems = getVisibleTabs(isMe, user?.nickname); const isCollectionTab = isMe && tab === "collection"; diff --git a/src/components/domains/user/tabs/MobileTabsAccordion.tsx b/src/components/domains/user/tabs/MobileTabsAccordion.tsx index 2cca6f9c..f1be62d0 100644 --- a/src/components/domains/user/tabs/MobileTabsAccordion.tsx +++ b/src/components/domains/user/tabs/MobileTabsAccordion.tsx @@ -18,7 +18,7 @@ export const MobileTabsAccordion = ({ currentTab, children, }: MobileTabsAccordionProps) => { - const user = useUserStore((state) => state.user); + const { user } = useUserStore(); const [isOpen, setIsOpen] = useState(true); From d56bb8d967eff995330c695eabc3ad888903ee6e Mon Sep 17 00:00:00 2001 From: kuan Date: Fri, 27 Mar 2026 00:43:06 +0900 Subject: [PATCH 06/39] =?UTF-8?q?perf=20:=20jsodom=20+=20dompurify=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20sanitizeHtml=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 9 +- src/server/hono/utils/sanitize.utils.ts | 28 +- yarn.lock | 414 ++++-------------------- 3 files changed, 74 insertions(+), 377 deletions(-) diff --git a/package.json b/package.json index 891f1e13..0416391a 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", - "dompurify": "^3.3.1", "hono": "^4.8.3", - "jsdom": "^28.1.0", "lucide-react": "^0.462.0", "next": "15.0.5", "next-auth": "^5.0.0-beta.25", @@ -45,6 +43,7 @@ "react-dropzone": "^14.3.8", "react-hook-form": "^7.53.2", "react-naver-maps": "^0.1.4", + "sanitize-html": "^2.17.2", "sharp": "^0.33.5", "swagger-ui-react": "^5.25.3", "swiper": "^11.2.7", @@ -67,12 +66,11 @@ "@storybook/test": "^8.5.8", "@svgr/webpack": "^8.1.0", "@tanstack/react-query-devtools": "^5.87.4", - "@types/dompurify": "^3.2.0", - "@types/jsdom": "^27.0.0", "@types/navermaps": "^3.9.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/sanitize-html": "^2.16.1", "@types/swagger-ui-react": "^5.18.0", "@vitejs/plugin-react": "^5.1.1", "@vitest/ui": "^4.0.9", @@ -91,8 +89,7 @@ "zod-prisma-types": "^3.2.4" }, "resolutions": { - "sharp": "0.33.5", - "jsdom": "25.0.1" + "sharp": "0.33.5" }, "browserslist": [ "chrome >= 90", diff --git a/src/server/hono/utils/sanitize.utils.ts b/src/server/hono/utils/sanitize.utils.ts index 3ed3d0d7..a85b13ac 100644 --- a/src/server/hono/utils/sanitize.utils.ts +++ b/src/server/hono/utils/sanitize.utils.ts @@ -1,5 +1,4 @@ -import DOMPurify from "dompurify"; -import { JSDOM } from "jsdom"; +import _sanitize from "sanitize-html"; /** * 서버 사이드에서 HTML을 sanitize하고 이미지를 최적화합니다. @@ -12,13 +11,8 @@ import { JSDOM } from "jsdom"; export function sanitizeHtml(html: string): string { if (!html) return ""; - // JSDOM을 사용하여 서버 환경에서 DOM API 제공 - const window = new JSDOM("").window; - const purify = DOMPurify(window); - - // Sanitize 옵션 설정 - const cleanHtml = purify.sanitize(html, { - ALLOWED_TAGS: [ + const cleanHtml = _sanitize(html, { + allowedTags: [ // 텍스트 포맷팅 "p", "br", "strong", "em", "u", "s", "sub", "sup", // 제목 @@ -32,16 +26,12 @@ export function sanitizeHtml(html: string): string { // 기타 "blockquote", "code", "pre", "span", "div", ], - ALLOWED_ATTR: [ - // 링크 속성 - "href", "target", "rel", - // 이미지 속성 - "src", "alt", "width", "height", "loading", "decoding", "fetchpriority", - // 스타일 및 기타 - "class", "style", "data-optimized", - ], - // 외부 링크에 자동으로 rel="noopener noreferrer" 추가 - ALLOW_UNKNOWN_PROTOCOLS: false, + allowedAttributes: { + "a": ["href", "target", "rel"], + "img": ["src", "alt", "width", "height", "loading", "decoding", "fetchpriority", "data-optimized"], + "*": ["class", "style"], + }, + allowedSchemes: ["http", "https", "mailto"], }); // 이미지 최적화 적용 diff --git a/yarn.lock b/yarn.lock index c388ccf5..bd959fcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,19 +19,6 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^3.2.0": - version: 3.2.0 - resolution: "@asamuzakjp/css-color@npm:3.2.0" - dependencies: - "@csstools/css-calc": "npm:^2.1.3" - "@csstools/css-color-parser": "npm:^3.0.9" - "@csstools/css-parser-algorithms": "npm:^3.0.4" - "@csstools/css-tokenizer": "npm:^3.0.3" - lru-cache: "npm:^10.4.3" - checksum: 10/870f661460173174fef8bfebea0799ba26566f3aa7b307e5adabb7aae84fed2da68e40080104ed0c83b43c5be632ee409e65396af13bfe948a3ef4c2c729ecd9 - languageName: node - linkType: hard - "@asteasolutions/zod-to-openapi@npm:^7.3.0": version: 7.3.4 resolution: "@asteasolutions/zod-to-openapi@npm:7.3.4" @@ -3118,52 +3105,6 @@ __metadata: languageName: node linkType: hard -"@csstools/color-helpers@npm:^5.1.0": - version: 5.1.0 - resolution: "@csstools/color-helpers@npm:5.1.0" - checksum: 10/0138b3d5ccbe77aeccf6721fd008a53523c70e932f0c82dca24a1277ca780447e1d8357da47512ebf96358476f8764de57002f3e491920d67e69202f5a74c383 - languageName: node - linkType: hard - -"@csstools/css-calc@npm:^2.1.3, @csstools/css-calc@npm:^2.1.4": - version: 2.1.4 - resolution: "@csstools/css-calc@npm:2.1.4" - peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10/06975b650c0f44c60eeb7afdb3fd236f2dd607b2c622e0bc908d3f54de39eb84e0692833320d03dac04bd6c1ab0154aa3fa0dd442bd9e5f917cf14d8e2ba8d74 - languageName: node - linkType: hard - -"@csstools/css-color-parser@npm:^3.0.9": - version: 3.1.0 - resolution: "@csstools/css-color-parser@npm:3.1.0" - dependencies: - "@csstools/color-helpers": "npm:^5.1.0" - "@csstools/css-calc": "npm:^2.1.4" - peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10/4741095fdc4501e8e7ada4ed14fbf9dbbe6fea9b989818790ebca15657c29c62defbebacf18592cde2aa638a1d098bbe86d742d2c84ba932fbc00fac51cb8805 - languageName: node - linkType: hard - -"@csstools/css-parser-algorithms@npm:^3.0.4": - version: 3.0.5 - resolution: "@csstools/css-parser-algorithms@npm:3.0.5" - peerDependencies: - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10/e93083b5cb36a3c1e7a47ce10cf62961d05bd1e4c608bb3ee50186ff740157ab0ec16a3956f7b86251efd10703034d849693201eea858ae904848c68d2d46ada - languageName: node - linkType: hard - -"@csstools/css-tokenizer@npm:^3.0.3": - version: 3.0.4 - resolution: "@csstools/css-tokenizer@npm:3.0.4" - checksum: 10/eb6c84c086312f6bb8758dfe2c85addd7475b0927333c5e39a4d59fb210b9810f8c346972046f95e60a721329cffe98895abe451e51de753ad1ca7a8c24ec65f - languageName: node - linkType: hard - "@discoveryjs/json-ext@npm:0.5.7": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -5985,15 +5926,6 @@ __metadata: languageName: node linkType: hard -"@types/dompurify@npm:^3.2.0": - version: 3.2.0 - resolution: "@types/dompurify@npm:3.2.0" - dependencies: - dompurify: "npm:*" - checksum: 10/0194dd9024e76e856f3373e8b080c7a5ac9910543ac1ee85db45277efbbea820b2f27fe22c8a6f02bb51833e0a8540d732be0d11d156efd16cb0b97f88e41ebf - languageName: node - linkType: hard - "@types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -6051,17 +5983,6 @@ __metadata: languageName: node linkType: hard -"@types/jsdom@npm:^27.0.0": - version: 27.0.0 - resolution: "@types/jsdom@npm:27.0.0" - dependencies: - "@types/node": "npm:*" - "@types/tough-cookie": "npm:*" - parse5: "npm:^7.0.0" - checksum: 10/67dc284f74960e1ba0787599dfb0a46ca94ea4443242d3fc1e513b0c5313d2d4136176764acce2b03b8c66dec942ff4ab15322208ccc17b7c1dccc8cd01d4cc2 - languageName: node - linkType: hard - "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -6182,6 +6103,15 @@ __metadata: languageName: node linkType: hard +"@types/sanitize-html@npm:^2.16.1": + version: 2.16.1 + resolution: "@types/sanitize-html@npm:2.16.1" + dependencies: + htmlparser2: "npm:^10.1" + checksum: 10/fba65c1b612f60cba07af2bb22791997f02b6774a2b68f16a40cb73e92b5f7086684fa6a947b5a24d200aae3607236eb24bfa3f2c56c40cd0e5209baf50bb12a + languageName: node + linkType: hard + "@types/semver@npm:^7.3.4": version: 7.7.1 resolution: "@types/semver@npm:7.7.1" @@ -6198,13 +6128,6 @@ __metadata: languageName: node linkType: hard -"@types/tough-cookie@npm:*": - version: 4.0.5 - resolution: "@types/tough-cookie@npm:4.0.5" - checksum: 10/01fd82efc8202670865928629697b62fe9bf0c0dcbc5b1c115831caeb073a2c0abb871ff393d7df1ae94ea41e256cb87d2a5a91fd03cdb1b0b4384e08d4ee482 - languageName: node - linkType: hard - "@types/trusted-types@npm:^2.0.7": version: 2.0.7 resolution: "@types/trusted-types@npm:2.0.7" @@ -8580,16 +8503,6 @@ __metadata: languageName: node linkType: hard -"cssstyle@npm:^4.1.0": - version: 4.6.0 - resolution: "cssstyle@npm:4.6.0" - dependencies: - "@asamuzakjp/css-color": "npm:^3.2.0" - rrweb-cssom: "npm:^0.8.0" - checksum: 10/1cb25c9d66b87adb165f978b75cdeb6f225d7e31ba30a8934666046a0be037e4e7200d359bfa79d4f1a4aef1083ea09633b81bcdb36a2f2ac888e8c73ea3a289 - languageName: node - linkType: hard - "csstype@npm:^3.0.2, csstype@npm:^3.1.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -8604,16 +8517,6 @@ __metadata: languageName: node linkType: hard -"data-urls@npm:^5.0.0": - version: 5.0.0 - resolution: "data-urls@npm:5.0.0" - dependencies: - whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^14.0.0" - checksum: 10/5c40568c31b02641a70204ff233bc4e42d33717485d074244a98661e5f2a1e80e38fe05a5755dfaf2ee549f2ab509d6a3af2a85f4b2ad2c984e5d176695eaf46 - languageName: node - linkType: hard - "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -8682,13 +8585,6 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.3": - version: 10.6.0 - resolution: "decimal.js@npm:10.6.0" - checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69 - languageName: node - linkType: hard - "dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" @@ -8932,18 +8828,6 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:*, dompurify@npm:^3.3.1": - version: 3.3.1 - resolution: "dompurify@npm:3.3.1" - dependencies: - "@types/trusted-types": "npm:^2.0.7" - dependenciesMeta: - "@types/trusted-types": - optional: true - checksum: 10/f71cca489e628591165d16e8cf4fa4f0d3e2ee48db4d73e9d2c5bedc6f915c92f9e9f101f8c4ba790bec0cdffe7f4e1747f5e31c69dc53ce7ae20a81ff6b0022 - languageName: node - linkType: hard - "dompurify@npm:=3.2.4": version: 3.2.4 resolution: "dompurify@npm:3.2.4" @@ -8967,7 +8851,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^3.0.1": +"domutils@npm:^3.0.1, domutils@npm:^3.2.2": version: 3.2.2 resolution: "domutils@npm:3.2.2" dependencies: @@ -9138,10 +9022,10 @@ __metadata: languageName: node linkType: hard -"entities@npm:^6.0.0": - version: 6.0.1 - resolution: "entities@npm:6.0.1" - checksum: 10/62af1307202884349d2867f0aac5c60d8b57102ea0b0e768b16246099512c28e239254ad772d6834e7e14cb1b6f153fc3d0c031934e3183b086c86d3838d874a +"entities@npm:^7.0.1": + version: 7.0.1 + resolution: "entities@npm:7.0.1" + checksum: 10/3c0c58d869c45148463e96d21dee2d1b801bd3fe4cf47aa470cd26dfe81d59e9e0a9be92ae083fa02fa441283c883a471486e94538dcfb8544428aa80a55271b languageName: node linkType: hard @@ -10202,19 +10086,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.5 - resolution: "form-data@npm:4.0.5" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.12" - checksum: 10/52ecd6e927c8c4e215e68a7ad5e0f7c1031397439672fd9741654b4a94722c4182e74cc815b225dcb5be3f4180f36428f67c6dd39eaa98af0dcfdd26c00c19cd - languageName: node - linkType: hard - "form-data@npm:^4.0.4": version: 4.0.4 resolution: "form-data@npm:4.0.4" @@ -10648,15 +10519,6 @@ __metadata: languageName: node linkType: hard -"html-encoding-sniffer@npm:^4.0.0": - version: 4.0.0 - resolution: "html-encoding-sniffer@npm:4.0.0" - dependencies: - whatwg-encoding: "npm:^3.1.1" - checksum: 10/e86efd493293a5671b8239bd099d42128433bb3c7b0fdc7819282ef8e118a21f5dead0ad6f358e024a4e5c84f17ebb7a9b36075220fac0a6222b207248bede6f - languageName: node - linkType: hard - "html-entities@npm:^2.1.0": version: 2.6.0 resolution: "html-entities@npm:2.6.0" @@ -10709,6 +10571,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^10.1, htmlparser2@npm:^10.1.0": + version: 10.1.0 + resolution: "htmlparser2@npm:10.1.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.2.2" + entities: "npm:^7.0.1" + checksum: 10/660fb094a53fb77a3c771db969778b58af0e8a572a1bdc8e5952a4241e4b04e0a6063b16f6422e22c821441081c8de339e3f06ddda362ac2a42c8767d5e5ad53 + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -10728,7 +10602,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": +"http-proxy-agent@npm:^7.0.0": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -10745,7 +10619,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5": +"https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -10762,7 +10636,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -11157,13 +11031,6 @@ __metadata: languageName: node linkType: hard -"is-potential-custom-element-name@npm:^1.0.1": - version: 1.0.1 - resolution: "is-potential-custom-element-name@npm:1.0.1" - checksum: 10/ced7bbbb6433a5b684af581872afe0e1767e2d1146b2207ca0068a648fb5cab9d898495d1ac0583524faaf24ca98176a7d9876363097c2d14fee6dd324f3a1ab - languageName: node - linkType: hard - "is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -11396,40 +11263,6 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:25.0.1": - version: 25.0.1 - resolution: "jsdom@npm:25.0.1" - dependencies: - cssstyle: "npm:^4.1.0" - data-urls: "npm:^5.0.0" - decimal.js: "npm:^10.4.3" - form-data: "npm:^4.0.0" - html-encoding-sniffer: "npm:^4.0.0" - http-proxy-agent: "npm:^7.0.2" - https-proxy-agent: "npm:^7.0.5" - is-potential-custom-element-name: "npm:^1.0.1" - nwsapi: "npm:^2.2.12" - parse5: "npm:^7.1.2" - rrweb-cssom: "npm:^0.7.1" - saxes: "npm:^6.0.0" - symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^5.0.0" - w3c-xmlserializer: "npm:^5.0.0" - webidl-conversions: "npm:^7.0.0" - whatwg-encoding: "npm:^3.1.1" - whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^14.0.0" - ws: "npm:^8.18.0" - xml-name-validator: "npm:^5.0.0" - peerDependencies: - canvas: ^2.11.2 - peerDependenciesMeta: - canvas: - optional: true - checksum: 10/e6bf7250ddd2fbcf68da0ea041a0dc63545dc4bf77fa3ff40a46ae45b1dac1ca55b87574ab904d1f8baeeb547c52cec493a22f545d7d413b320011f41150ec49 - languageName: node - linkType: hard - "jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -11720,7 +11553,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3": +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a @@ -12402,13 +12235,6 @@ __metadata: languageName: node linkType: hard -"nwsapi@npm:^2.2.12": - version: 2.2.23 - resolution: "nwsapi@npm:2.2.23" - checksum: 10/aa4a570039c33d70b51436d1bb533f3e2c33c488ccbe9b09285c46a6cee5ef266fd60103461085c6954ba52460786a8138f042958328c7c1b4763898eb3dadfa - languageName: node - linkType: hard - "nypm@npm:^0.6.0": version: 0.6.2 resolution: "nypm@npm:0.6.2" @@ -12779,12 +12605,10 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.0.0, parse5@npm:^7.1.2": - version: 7.3.0 - resolution: "parse5@npm:7.3.0" - dependencies: - entities: "npm:^6.0.0" - checksum: 10/b0e48be20b820c655b138b86fa6fb3a790de6c891aa2aba536524f8027b4dca4fe538f11a0e5cf2f6f847d120dbb9e4822dcaeb933ff1e10850a2ef0154d1d88 +"parse-srcset@npm:^1.0.2": + version: 1.0.2 + resolution: "parse-srcset@npm:1.0.2" + checksum: 10/d40c131cfc3ab7bb6333b788d30a30d063d76a83b49fa752229823f96475e36cf29fea09e035ce3b2a634b686e93e2a7429cb8dad0041d8a3a3df622093b9ea1 languageName: node linkType: hard @@ -12915,12 +12739,11 @@ __metadata: "@svgr/webpack": "npm:^8.1.0" "@tanstack/react-query": "npm:^5.87.4" "@tanstack/react-query-devtools": "npm:^5.87.4" - "@types/dompurify": "npm:^3.2.0" - "@types/jsdom": "npm:^27.0.0" "@types/navermaps": "npm:^3.9.1" "@types/node": "npm:^20" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" + "@types/sanitize-html": "npm:^2.16.1" "@types/swagger-ui-react": "npm:^5.18.0" "@vitejs/plugin-react": "npm:^5.1.1" "@vitest/ui": "npm:^4.0.9" @@ -12931,12 +12754,10 @@ __metadata: clsx: "npm:^2.1.1" cross-env: "npm:^10.1.0" dayjs: "npm:^1.11.13" - dompurify: "npm:^3.3.1" eslint: "npm:^8" eslint-config-next: "npm:15.0.3" eslint-plugin-storybook: "npm:^0.11.3" hono: "npm:^4.8.3" - jsdom: "npm:^28.1.0" lucide-react: "npm:^0.462.0" next: "npm:15.0.5" next-auth: "npm:^5.0.0-beta.25" @@ -12949,6 +12770,7 @@ __metadata: react-dropzone: "npm:^14.3.8" react-hook-form: "npm:^7.53.2" react-naver-maps: "npm:^0.1.4" + sanitize-html: "npm:^2.17.2" sharp: "npm:^0.33.5" storybook: "npm:^8.5.8" swagger-ui-react: "npm:^5.25.3" @@ -13222,6 +13044,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.3.11": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459 + languageName: node + linkType: hard + "preact-render-to-string@npm:6.5.11": version: 6.5.11 resolution: "preact-render-to-string@npm:6.5.11" @@ -13376,7 +13209,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 @@ -14268,20 +14101,6 @@ __metadata: languageName: node linkType: hard -"rrweb-cssom@npm:^0.7.1": - version: 0.7.1 - resolution: "rrweb-cssom@npm:0.7.1" - checksum: 10/e80cf25c223a823921d7ab57c0ce78f5b7ebceab857b400cce99dd4913420ce679834bc5707e8ada47d062e21ad368108a9534c314dc8d72c20aa4a4fa0ed16a - languageName: node - linkType: hard - -"rrweb-cssom@npm:^0.8.0": - version: 0.8.0 - resolution: "rrweb-cssom@npm:0.8.0" - checksum: 10/07521ee36fb6569c17906afad1ac7ff8f099d49ade9249e190693ac36cdf27f88d9acf0cc66978935d5d0a23fca105643d7e9125b9a9d91ed9db9e02d31d7d80 - languageName: node - linkType: hard - "rtl-css-js@npm:^1.16.1": version: 1.16.1 resolution: "rtl-css-js@npm:1.16.1" @@ -14355,6 +14174,20 @@ __metadata: languageName: node linkType: hard +"sanitize-html@npm:^2.17.2": + version: 2.17.2 + resolution: "sanitize-html@npm:2.17.2" + dependencies: + deepmerge: "npm:^4.2.2" + escape-string-regexp: "npm:^4.0.0" + htmlparser2: "npm:^10.1.0" + is-plain-object: "npm:^5.0.0" + parse-srcset: "npm:^1.0.2" + postcss: "npm:^8.3.11" + checksum: 10/ea3cc6f8ef81839fa88879611a5c355b3075941ee4219d0e7a5b79282d4f998c61deb1e2a3af55f733b949d7283553cad160c26125d8957bee05e1b9f95a8e0b + languageName: node + linkType: hard + "sass-loader@npm:^14.2.1": version: 14.2.1 resolution: "sass-loader@npm:14.2.1" @@ -14381,15 +14214,6 @@ __metadata: languageName: node linkType: hard -"saxes@npm:^6.0.0": - version: 6.0.0 - resolution: "saxes@npm:6.0.0" - dependencies: - xmlchars: "npm:^2.2.0" - checksum: 10/97b50daf6ca3a153e89842efa18a862e446248296622b7473c169c84c823ee8a16e4a43bac2f73f11fc8cb9168c73fbb0d73340f26552bac17970e9052367aa9 - languageName: node - linkType: hard - "scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" @@ -15308,13 +15132,6 @@ __metadata: languageName: node linkType: hard -"symbol-tree@npm:^3.2.4": - version: 3.2.4 - resolution: "symbol-tree@npm:3.2.4" - checksum: 10/c09a00aadf279d47d0c5c46ca3b6b2fbaeb45f0a184976d599637d412d3a70bbdc043ff33effe1206dea0e36e0ad226cb957112e7ce9a4bf2daedf7fa4f85c53 - languageName: node - linkType: hard - "tailwind-merge@npm:^2.5.5": version: 2.6.0 resolution: "tailwind-merge@npm:2.6.0" @@ -15529,24 +15346,6 @@ __metadata: languageName: node linkType: hard -"tldts-core@npm:^6.1.86": - version: 6.1.86 - resolution: "tldts-core@npm:6.1.86" - checksum: 10/cb5dff9cc15661ac773a2099e98c99a5cb3cebc35909c23cc4261ff7992032c7501995ae995de3574dbbf3431e59c47496534d52f5e96abcb231f0e72144c020 - languageName: node - linkType: hard - -"tldts@npm:^6.1.32": - version: 6.1.86 - resolution: "tldts@npm:6.1.86" - dependencies: - tldts-core: "npm:^6.1.86" - bin: - tldts: bin/cli.js - checksum: 10/f7e66824e44479ccdda55ea556af14ce61c4d27708be403e3f90631defde49f82a580e1ca07187cc7e3b349e257a30c2808a22903f3a0548e136ebb609ccc109 - languageName: node - linkType: hard - "to-buffer@npm:^1.2.0, to-buffer@npm:^1.2.1, to-buffer@npm:^1.2.2": version: 1.2.2 resolution: "to-buffer@npm:1.2.2" @@ -15581,24 +15380,6 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^5.0.0": - version: 5.1.2 - resolution: "tough-cookie@npm:5.1.2" - dependencies: - tldts: "npm:^6.1.32" - checksum: 10/de430e6e6d34b794137e05b8ac2aa6b74ebbe6cdceb4126f168cf1e76101162a4b2e0e7587c3b70e728bd8654fc39958b2035be7619ee6f08e7257610ba4cd04 - languageName: node - linkType: hard - -"tr46@npm:^5.1.0": - version: 5.1.1 - resolution: "tr46@npm:5.1.1" - dependencies: - punycode: "npm:^2.3.1" - checksum: 10/833a0e1044574da5790148fd17866d4ddaea89e022de50279967bcd6b28b4ce0d30d59eb3acf9702b60918975b3bad481400337e3a2e6326cffa5c77b874753d - languageName: node - linkType: hard - "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -16325,15 +16106,6 @@ __metadata: languageName: node linkType: hard -"w3c-xmlserializer@npm:^5.0.0": - version: 5.0.0 - resolution: "w3c-xmlserializer@npm:5.0.0" - dependencies: - xml-name-validator: "npm:^5.0.0" - checksum: 10/d78f59e6b4f924aa53b6dfc56949959229cae7fe05ea9374eb38d11edcec01398b7f5d7a12576bd5acc57ff446abb5c9115cd83b9d882555015437cf858d42f0 - languageName: node - linkType: hard - "watchpack@npm:^2.4.1": version: 2.4.4 resolution: "watchpack@npm:2.4.4" @@ -16365,13 +16137,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^7.0.0": - version: 7.0.0 - resolution: "webidl-conversions@npm:7.0.0" - checksum: 10/4c4f65472c010eddbe648c11b977d048dd96956a625f7f8b9d64e1b30c3c1f23ea1acfd654648426ce5c743c2108a5a757c0592f02902cf7367adb7d14e67721 - languageName: node - linkType: hard - "webpack-bundle-analyzer@npm:4.10.1": version: 4.10.1 resolution: "webpack-bundle-analyzer@npm:4.10.1" @@ -16476,32 +16241,6 @@ __metadata: languageName: node linkType: hard -"whatwg-encoding@npm:^3.1.1": - version: 3.1.1 - resolution: "whatwg-encoding@npm:3.1.1" - dependencies: - iconv-lite: "npm:0.6.3" - checksum: 10/bbef815eb67f91487c7f2ef96329743f5fd8357d7d62b1119237d25d41c7e452dff8197235b2d3c031365a17f61d3bb73ca49d0ed1582475aa4a670815e79534 - languageName: node - linkType: hard - -"whatwg-mimetype@npm:^4.0.0": - version: 4.0.0 - resolution: "whatwg-mimetype@npm:4.0.0" - checksum: 10/894a618e2d90bf444b6f309f3ceb6e58cf21b2beaa00c8b333696958c4076f0c7b30b9d33413c9ffff7c5832a0a0c8569e5bb347ef44beded72aeefd0acd62e8 - languageName: node - linkType: hard - -"whatwg-url@npm:^14.0.0": - version: 14.2.0 - resolution: "whatwg-url@npm:14.2.0" - dependencies: - tr46: "npm:^5.1.0" - webidl-conversions: "npm:^7.0.0" - checksum: 10/f0a95b0601c64f417c471536a2d828b4c16fe37c13662483a32f02f183ed0f441616609b0663fb791e524e8cd56d9a86dd7366b1fc5356048ccb09b576495e7c - languageName: node - linkType: hard - "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -16658,21 +16397,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0": - version: 8.19.0 - resolution: "ws@npm:8.19.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b - languageName: node - linkType: hard - "ws@npm:^8.18.2, ws@npm:^8.2.3": version: 8.18.3 resolution: "ws@npm:8.18.3" @@ -16697,13 +16421,6 @@ __metadata: languageName: node linkType: hard -"xml-name-validator@npm:^5.0.0": - version: 5.0.0 - resolution: "xml-name-validator@npm:5.0.0" - checksum: 10/43f30f3f6786e406dd665acf08cd742d5f8a46486bd72517edb04b27d1bcd1599664c2a4a99fc3f1e56a3194bff588b12f178b7972bc45c8047bdc4c3ac8d4a1 - languageName: node - linkType: hard - "xml@npm:=1.0.1": version: 1.0.1 resolution: "xml@npm:1.0.1" @@ -16711,13 +16428,6 @@ __metadata: languageName: node linkType: hard -"xmlchars@npm:^2.2.0": - version: 2.2.0 - resolution: "xmlchars@npm:2.2.0" - checksum: 10/4ad5924974efd004a47cce6acf5c0269aee0e62f9a805a426db3337af7bcbd331099df174b024ace4fb18971b8a56de386d2e73a1c4b020e3abd63a4a9b917f1 - languageName: node - linkType: hard - "xtend@npm:^4.0.0, xtend@npm:^4.0.2": version: 4.0.2 resolution: "xtend@npm:4.0.2" From 6af815d1c87eb4811c1269e517e2b5875979a117 Mon Sep 17 00:00:00 2001 From: kuan Date: Sun, 29 Mar 2026 00:25:08 +0900 Subject: [PATCH 07/39] =?UTF-8?q?refactor:=20core-request=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/utils/api/client.ts | 144 +++++++- src/client/utils/core-request/README.md | 342 ------------------ src/client/utils/core-request/config.ts | 7 - src/client/utils/core-request/core.ts | 117 ------ src/client/utils/core-request/httpClient.ts | 186 ---------- src/client/utils/core-request/index.ts | 9 - src/client/utils/core-request/serverClient.ts | 29 -- src/client/utils/core-request/token.ts | 21 -- 8 files changed, 139 insertions(+), 716 deletions(-) delete mode 100644 src/client/utils/core-request/README.md delete mode 100644 src/client/utils/core-request/config.ts delete mode 100644 src/client/utils/core-request/core.ts delete mode 100644 src/client/utils/core-request/httpClient.ts delete mode 100644 src/client/utils/core-request/index.ts delete mode 100644 src/client/utils/core-request/serverClient.ts delete mode 100644 src/client/utils/core-request/token.ts diff --git a/src/client/utils/api/client.ts b/src/client/utils/api/client.ts index 2c812d04..d234cbed 100644 --- a/src/client/utils/api/client.ts +++ b/src/client/utils/api/client.ts @@ -1,10 +1,144 @@ -import { createHttpClient } from "@/client/utils/core-request"; +const BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000/api/v1"; + +export class APIError extends Error { + constructor( + public status: number, + public message: string, + public errorData?: unknown, + ) { + super(message); + this.name = "APIError"; + } +} + +function objectToQueryString(params: Record): string { + const entries = Object.entries(params).filter( + ([, value]) => value !== undefined && value !== null, + ); + return new URLSearchParams( + Object.fromEntries(entries) as Record, + ).toString(); +} + +async function fetchApi(url: string, init?: RequestInit): Promise { + const response = await fetch(url, init); + + if (!response.ok) { + let errorData: { message?: string } = {}; + try { + errorData = await response.json(); + } catch { + errorData = { message: response.statusText || "Unknown error" }; + } + throw new APIError( + response.status, + errorData.message ?? "Unknown error", + errorData, + ); + } + + if (response.status === 204) return null; + + return response.json() as Promise; +} /** * 중앙 API 클라이언트 * 모든 API 요청에서 공유하는 단일 인스턴스 */ -export const apiClient = createHttpClient({ - baseUrl: - process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000/api/v1", -}); +export const apiClient = { + get( + path: string, + params?: Record, + options?: { cache?: RequestCache; headers?: HeadersInit }, + ): Promise { + let url = `${BASE_URL}${path}`; + if (params && Object.keys(params).length > 0) { + url += `?${objectToQueryString(params)}`; + } + return fetchApi(url, { + method: "GET", + cache: options?.cache, + headers: options?.headers, + }); + }, + + post<_RequestData, ResponseData = _RequestData>( + path: string, + data: FormData | object, + options?: { headers?: HeadersInit; cache?: RequestCache }, + ): Promise { + const url = `${BASE_URL}${path}`; + if (data instanceof FormData) { + return fetchApi(url, { + method: "POST", + body: data, + headers: options?.headers, + cache: options?.cache, + }); + } + const headers = new Headers({ "Content-Type": "application/json" }); + if (options?.headers) { + new Headers(options.headers).forEach((value, key) => + headers.set(key, value), + ); + } + return fetchApi(url, { + method: "POST", + body: JSON.stringify(data), + headers, + cache: options?.cache, + }); + }, + + patch<_RequestData, ResponseData = _RequestData>( + path: string, + data: _RequestData, + options?: { headers?: HeadersInit }, + ): Promise { + const url = `${BASE_URL}${path}`; + const headers = new Headers({ "Content-Type": "application/json" }); + if (options?.headers) { + new Headers(options.headers).forEach((value, key) => + headers.set(key, value), + ); + } + return fetchApi(url, { + method: "PATCH", + body: JSON.stringify(data), + headers, + }); + }, + + put<_RequestData, ResponseData = _RequestData>( + path: string, + data: _RequestData, + options?: { headers?: HeadersInit }, + ): Promise { + const url = `${BASE_URL}${path}`; + const headers = new Headers({ "Content-Type": "application/json" }); + if (options?.headers) { + new Headers(options.headers).forEach((value, key) => + headers.set(key, value), + ); + } + return fetchApi(url, { + method: "PUT", + body: JSON.stringify(data), + headers, + }); + }, + + delete( + path: string, + options?: { cache?: RequestCache; headers?: HeadersInit }, + ): Promise { + const url = `${BASE_URL}${path}`; + return fetchApi(url, { + method: "DELETE", + cache: options?.cache, + headers: options?.headers, + }); + }, +}; diff --git a/src/client/utils/core-request/README.md b/src/client/utils/core-request/README.md deleted file mode 100644 index 2ca1bfbb..00000000 --- a/src/client/utils/core-request/README.md +++ /dev/null @@ -1,342 +0,0 @@ -# HTTP Client Library - -TypeScript 기반의 강력하고 유연한 HTTP 클라이언트 라이브러리입니다. 인터셉터, 자동 토큰 갱신, 타입 안전성을 제공합니다. - -## 주요 기능 - -- ✨ **타입 안전성**: 완전한 TypeScript 지원 -- 🔄 **자동 토큰 갱신**: 401 응답 시 자동으로 토큰 갱신 및 재시도 -- 🎯 **인터셉터**: 요청/응답 인터셉터를 통한 커스터마이징 -- 🔧 **응답 변환**: 응답 데이터 변환 기능 -- 🌐 **Next.js 서버 지원**: Next.js 서버 컴포넌트를 위한 전용 클라이언트 -- 📝 **FormData 지원**: JSON과 FormData 모두 지원 - -## 기본 사용법 - -### 클라이언트 생성 - -```typescript -import { createHttpClient } from "your-http-client-library"; - -const apiClient = createHttpClient({ - baseUrl: "https://api.example.com", - headers: { - "Content-Type": "application/json", - }, - debug: true, // 디버그 로그 활성화 (선택) -}); -``` - -### GET 요청 - -```typescript -// 기본 GET 요청 -const users = await apiClient.get("/users"); - -// 쿼리 파라미터 사용 -const filteredUsers = await apiClient.get("/users", { - page: 1, - limit: 10, - status: "active", -}); - -// 타입 안전성과 함께 사용 -interface User { - id: number; - name: string; - email: string; -} - -const users = await apiClient.get("/users"); -``` - -### POST 요청 - -```typescript -// JSON 데이터 전송 -interface CreateUserRequest { - name: string; - email: string; -} - -interface UserResponse { - id: number; - name: string; - email: string; - createdAt: string; -} - -const newUser = await apiClient.post( - "/users", - { - name: "John Doe", - email: "john@example.com", - } -); - -// FormData 전송 -const formData = new FormData(); -formData.append("file", file); -formData.append("title", "My File"); - -const uploadResult = await apiClient.post("/upload", formData); -``` - -### PUT / PATCH 요청 - -```typescript -// PUT 요청 (전체 업데이트) -const updatedUser = await apiClient.put("/users/123", { - name: "Jane Doe", - email: "jane@example.com", -}); - -// PATCH 요청 (부분 업데이트) -const partialUpdate = await apiClient.patch("/users/123", { - name: "Jane Smith", -}); -``` - -### DELETE 요청 - -```typescript -// 삭제 요청 -await apiClient.delete("/users/123"); -``` - -## 고급 기능 - -### 응답 데이터 변환 - -```typescript -interface ApiResponse { - data: User[]; - meta: { total: number }; -} - -const users = await apiClient.get( - "/users", - {}, - { - transformResponse: (response) => response.data, - } -); -// users는 User[] 타입 -``` - -### 요청 인터셉터 - -```typescript -// 모든 요청에 Authorization 헤더 추가 -apiClient.useRequestInterceptor((config) => { - const token = localStorage.getItem("token"); - const headers = new Headers(config.headers); - - if (token) { - headers.set("Authorization", `Bearer ${token}`); - } - - return { - ...config, - headers, - }; -}); -``` - -### 응답 인터셉터 - -```typescript -// 모든 응답 로깅 -apiClient.useResponseInterceptor((response) => { - console.log("Response status:", response.status); - return response; -}); - -// 에러 응답 처리 -apiClient.useResponseInterceptor(async (response) => { - if (response.status === 403) { - console.error("Access forbidden"); - // 추가 처리 로직 - } - return response; -}); -``` - -### 자동 토큰 갱신 - -```typescript -const apiClient = createHttpClient({ - baseUrl: "https://api.example.com", - refreshToken: async () => { - // 토큰 갱신 로직 - const response = await fetch("/auth/refresh", { - method: "POST", - credentials: "include", - }); - - if (!response.ok) { - throw new Error("Token refresh failed"); - } - - const data = await response.json(); - localStorage.setItem("token", data.accessToken); - }, -}); - -// 401 응답 시 자동으로 토큰을 갱신하고 요청을 재시도합니다 -const data = await apiClient.get("/protected-resource"); -``` - -### Next.js 서버 컴포넌트 사용 - -```typescript -import { createApiServerClient } from "your-http-client-library/serverClient"; - -export default async function ServerComponent() { - const apiClient = await createApiServerClient(); - - const data = await apiClient.get("/server-data"); - - return
{/* 렌더링 로직 */}
; -} -``` - -## 에러 처리 - -```typescript -import { APIError } from "your-http-client-library"; - -try { - const data = await apiClient.get("/users"); -} catch (error) { - if (error instanceof APIError) { - console.error("API Error:", { - status: error.status, - message: error.message, - data: error.errorData, - }); - - // 상태 코드별 처리 - if (error.status === 404) { - console.error("Resource not found"); - } else if (error.status === 500) { - console.error("Server error"); - } - } else { - console.error("Unknown error:", error); - } -} -``` - -## 설정 옵션 - -### LibraryConfig - -```typescript -interface LibraryConfig { - baseUrl: string; // API 기본 URL (필수) - headers?: HeadersInit; // 기본 헤더 - tokenProvider?: () => string | null; // 토큰 제공 함수 - refreshToken?: () => Promise; // 토큰 갱신 함수 - debug?: boolean; // 디버그 모드 -} -``` - -### 요청 옵션 - -```typescript -interface GetApiOptions { - cache?: RequestCache; // 캐시 설정 - headers?: HeadersInit; // 추가 헤더 - skipAuth?: boolean; // 인증 스킵 - transformResponse?: (response: RawData) => TransformedData; -} - -interface MutateApiOptions { - cache?: RequestCache; - headers?: HeadersInit; - transformResponse?: (response: RawData) => TransformedData; -} -``` - -## 예제 - -### 완전한 인증 플로우 예제 - -```typescript -const apiClient = createHttpClient({ - baseUrl: "https://api.example.com", - debug: true, - refreshToken: async () => { - const response = await fetch("https://api.example.com/auth/refresh", { - method: "POST", - credentials: "include", - }); - - if (!response.ok) { - window.location.href = "/login"; - throw new Error("Refresh failed"); - } - - const data = await response.json(); - localStorage.setItem("accessToken", data.accessToken); - }, -}); - -// Authorization 헤더 자동 추가 -apiClient.useRequestInterceptor((config) => { - const token = localStorage.getItem("accessToken"); - const headers = new Headers(config.headers); - - if (token && !config.skipAuth) { - headers.set("Authorization", `Bearer ${token}`); - } - - return { ...config, headers }; -}); - -// 사용 -const userData = await apiClient.get("/user/me"); -``` - -### React에서 사용하기 - -```typescript -// hooks/useApi.ts -import { createHttpClient } from "your-http-client-library"; -import { useMemo } from "react"; - -export function useApiClient() { - return useMemo(() => { - const client = createHttpClient({ - baseUrl: process.env.NEXT_PUBLIC_API_URL!, - refreshToken: async () => { - // 토큰 갱신 로직 - }, - }); - - client.useRequestInterceptor((config) => { - // 인터셉터 로직 - return config; - }); - - return client; - }, []); -} - -// 컴포넌트에서 사용 -function UserList() { - const apiClient = useApiClient(); - const [users, setUsers] = useState([]); - - useEffect(() => { - apiClient.get("/users").then(setUsers); - }, [apiClient]); - - return
{/* 렌더링 */}
; -} -``` - -## 라이센스 - -MIT diff --git a/src/client/utils/core-request/config.ts b/src/client/utils/core-request/config.ts deleted file mode 100644 index 3dfbade4..00000000 --- a/src/client/utils/core-request/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface LibraryConfig { - baseUrl: string; - headers?: HeadersInit; - tokenProvider?: () => string | null; - refreshToken?: () => Promise; - debug?: boolean; -} diff --git a/src/client/utils/core-request/core.ts b/src/client/utils/core-request/core.ts deleted file mode 100644 index 1de47f34..00000000 --- a/src/client/utils/core-request/core.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { LibraryConfig } from "./config"; -import { manageTokenRefresh } from "./token"; - -// --- 타입 정의 --- -export interface CustomRequestInit extends RequestInit { - skipAuth?: boolean; -} - -type RequestInterceptor = ( - config: CustomRequestInit, -) => Promise | CustomRequestInit; -type ResponseInterceptor = (response: Response) => Promise | Response; - -export interface RequestOptions< - ResponseData, - TransformedData, -> extends CustomRequestInit { - url: string; - isRetry?: boolean; - transformResponse?: (response: ResponseData) => TransformedData; -} - -// --- 커스텀 에러 클래스 --- -export class APIError extends Error { - constructor( - public status: number, - public message: string, - public errorData?: unknown, - ) { - super(message); - this.name = "APIError"; - } -} - -// --- HttpClient 팩토리 함수 --- -export function createCoreClient(config: LibraryConfig) { - const requestInterceptors: RequestInterceptor[] = []; - const responseInterceptors: ResponseInterceptor[] = []; - - // 요청 인터셉터 함수 - const useRequestInterceptor = (interceptor: RequestInterceptor) => { - requestInterceptors.push(interceptor); - }; - - // 응답 인터셉터 함수 - const useResponseInterceptor = (interceptor: ResponseInterceptor) => { - responseInterceptors.push(interceptor); - }; - - // HTTP 요청을 수행하는 함수 - const request = async ( - originalOptions: RequestOptions, - ): Promise => { - const { url, transformResponse, ...options } = originalOptions; - - let reqConfig: CustomRequestInit = { ...options }; - for (const interceptor of requestInterceptors) { - reqConfig = await interceptor(reqConfig); - } - - const headers = new Headers(reqConfig.headers); - - if (config.debug) { - console.log(`[Request] Calling fetch for URL: ${url}`); - try { - console.log( - `[Request] Fetch options:`, - JSON.stringify(reqConfig, null, 2), - ); - } catch { - console.log(`[Request] Fetch options:`, reqConfig); - } - } - - let response = await fetch(url, { - ...reqConfig, - headers, - cache: reqConfig.cache, - }); - - if (response.status === 401 && !options.isRetry && config.refreshToken) { - await manageTokenRefresh(config.refreshToken); - return await request({ ...originalOptions, isRetry: true }); - } - - for (const interceptor of responseInterceptors) { - response = await interceptor(response); - } - - // 응답 상태 체크 - if (!response.ok) { - let errorData; - try { - errorData = await response.json(); - } catch { - errorData = { message: response.statusText || "Unknown error" }; - } - throw new APIError(response.status, errorData.message, errorData); - } - - if (response.status === 204) return null; - - const data = (await response.json()) as RawData; - - if (transformResponse) { - return transformResponse(data); - } - - return data as unknown as TransformedData; - }; - - return { - useRequestInterceptor, - useResponseInterceptor, - request, - }; -} diff --git a/src/client/utils/core-request/httpClient.ts b/src/client/utils/core-request/httpClient.ts deleted file mode 100644 index dab0015e..00000000 --- a/src/client/utils/core-request/httpClient.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { createCoreClient } from "./core"; -import type { LibraryConfig } from "./config"; - -export interface BaseApiOptions { - cache?: RequestCache; - headers?: HeadersInit; -} - -export interface GetApiOptions - extends BaseApiOptions { - skipAuth?: boolean; - transformResponse?: (response: RawData) => TransformedData; -} - -export interface MutateApiOptions - extends BaseApiOptions { - transformResponse?: (response: RawData) => TransformedData; -} - -function objectToQueryString(params: Record): string { - const entries = Object.entries(params); - const filteredEntries = entries.filter( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_key, value]) => value !== undefined && value !== null - ); - const cleanedParams = Object.fromEntries(filteredEntries); - - return new URLSearchParams( - cleanedParams as Record - ).toString(); -} - -export function createHttpClient(config: LibraryConfig) { - const core = createCoreClient(config); - - const mergeHeaders = ( - baseHeaders: HeadersInit, - userHeaders?: HeadersInit - ): Headers => { - const result = new Headers(baseHeaders); - if (userHeaders) { - new Headers(userHeaders).forEach((value, key) => { - result.set(key, value); - }); - } - return result; - }; - - const mutate = < - RequestData extends object, - RawData, - TransformedData = RawData - >( - method: "PUT" | "PATCH", - path: string, - data: RequestData, - options?: MutateApiOptions - ) => { - const finalUrl = `${config.baseUrl}${path}`; - - const headers = mergeHeaders( - { "Content-Type": "application/json" }, - options?.headers - ); - - return core.request({ - ...options, - url: finalUrl, - method, - body: JSON.stringify(data), - headers, - }); - }; - - const get = ( - path: string, - params?: Record, - options?: GetApiOptions - ) => { - let finalUrl = `${config.baseUrl}${path}`; - if (params && Object.keys(params).length > 0) { - finalUrl += `?${objectToQueryString(params)}`; - } - return core.request({ - ...options, - url: finalUrl, - method: "GET", - cache: options?.cache || "force-cache", - }); - }; - - // Overloading 정의 - function post( - path: string, - data: FormData, - options?: MutateApiOptions - ): Promise; - function post( - path: string, - data: RequestData, - options?: MutateApiOptions - ): Promise; - - // Overloading 구현 코드 - function post( - path: string, - data: RequestData | FormData, - options?: MutateApiOptions - ): Promise { - const finalUrl = `${config.baseUrl}${path}`; - - if (data instanceof FormData) { - return core.request({ - ...options, - url: finalUrl, - method: "POST", - body: data, - }); - } else { - const headers = mergeHeaders( - { "Content-Type": "application/json" }, - options?.headers - ); - return core.request({ - ...options, - url: finalUrl, - method: "POST", - body: JSON.stringify(data), - headers, - }); - } - } - - const put = ( - path: string, - data: RequestData, - options?: MutateApiOptions - ) => { - return mutate( - "PUT", - path, - data, - options - ); - }; - - const patch = < - RequestData extends object, - RawData, - TransformedData = RawData - >( - path: string, - data: RequestData, - options?: MutateApiOptions - ) => { - return mutate( - "PATCH", - path, - data, - options - ); - }; - - const remove = ( - path: string, - options?: MutateApiOptions - ) => { - const finalUrl = `${config.baseUrl}${path}`; - return core.request({ - ...options, - url: finalUrl, - method: "DELETE", - }); - }; - - return Object.freeze({ - request: core.request, - useRequestInterceptor: core.useRequestInterceptor, - useResponseInterceptor: core.useResponseInterceptor, - get, - post, - put, - patch, - delete: remove, - }); -} diff --git a/src/client/utils/core-request/index.ts b/src/client/utils/core-request/index.ts deleted file mode 100644 index 94ad682a..00000000 --- a/src/client/utils/core-request/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { createHttpClient } from "./httpClient"; -export type { LibraryConfig } from "./config"; -export { APIError } from "./core"; - -export type { - GetApiOptions, - MutateApiOptions, - BaseApiOptions, -} from "./httpClient"; diff --git a/src/client/utils/core-request/serverClient.ts b/src/client/utils/core-request/serverClient.ts deleted file mode 100644 index 8f31af26..00000000 --- a/src/client/utils/core-request/serverClient.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { cookies } from "next/headers"; -import { createHttpClient } from "./httpClient"; - -export const createApiServerClient = async () => { - const cookieStore = await cookies(); - const sessionToken = - cookieStore.get("authjs.session-token")?.value || - cookieStore.get("__Secure-authjs.session-token")?.value; - - const apiClient = createHttpClient({ - baseUrl: - process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000/api/v1", - headers: sessionToken ? { cookie: sessionToken } : undefined, - }); - apiClient.useRequestInterceptor((config) => { - const headers = new Headers(config.headers); - - const cookieString = cookieStore - .getAll() - .map((cookie) => `${cookie.name}=${cookie.value}`) - .join("; "); - headers.set("cookie", cookieString); - return { - ...config, - headers, - }; - }); - return apiClient; -}; diff --git a/src/client/utils/core-request/token.ts b/src/client/utils/core-request/token.ts deleted file mode 100644 index 0b556c10..00000000 --- a/src/client/utils/core-request/token.ts +++ /dev/null @@ -1,21 +0,0 @@ -let isRefreshing = false; -let refreshPromise: Promise | null = null; - -type RefreshFn = () => Promise; - -/** - * 토큰 갱신을 관리하는 함수 - * @param refreshToken 토큰 갱신 함수 - * @returns 토큰 갱신이 완료될 때까지의 Promise - */ -export function manageTokenRefresh(refreshToken: RefreshFn): Promise { - if (!isRefreshing) { - isRefreshing = true; - refreshPromise = refreshToken().finally(() => { - isRefreshing = false; - refreshPromise = null; - }); - } - - return refreshPromise ?? Promise.resolve(); -} From 65339ae22d0b93073cb3464bc345a598befa234a Mon Sep 17 00:00:00 2001 From: kuan Date: Sun, 29 Mar 2026 02:17:47 +0900 Subject: [PATCH 08/39] =?UTF-8?q?refactor:=20=ED=8C=8C=ED=8E=B8=ED=99=94?= =?UTF-8?q?=EB=90=9C=20Zustand=20=EC=8A=A4=ED=86=A0=EC=96=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/community/post/(write)/page.tsx | 6 +- src/app/community/post/[id]/edit/page.tsx | 5 +- src/client/hooks/useInfinitePerfumes.ts | 4 +- src/client/hooks/useRecentItemsSync.ts | 2 +- src/client/stores/createRecentItemsStore.ts | 82 ------ src/client/stores/perfumeStore.ts | 255 ++++++++++++++++++ src/client/stores/uiStore.ts | 113 ++++++++ src/client/stores/useCountStore.ts | 11 - src/client/stores/useFilterStore.ts | 131 --------- src/client/stores/useLogRecentItem.ts | 24 -- src/client/stores/useModalStore.ts | 27 -- src/client/stores/useRecentPerfumesStore.ts | 21 -- src/client/stores/useRecentPostsStore.ts | 14 - src/client/stores/useVisibilityStore.ts | 60 ----- .../commons/Provider/RecentSyncManager.tsx | 4 +- .../commons/dropdown/DropdownBase.tsx | 2 +- .../commons/navBar/UserProfileSection.tsx | 2 +- .../commons/perfumeList/filter/FilterItem.tsx | 4 +- .../commons/perfumeList/filter/FilterList.tsx | 4 +- .../commons/perfumeList/modal/ItemModal.tsx | 4 +- .../commons/perfumeList/modal/Layout.tsx | 2 +- .../commons/wrapper/PageWrapper.tsx | 2 +- src/components/domains/main/MainSearchBar.tsx | 2 +- .../domains/perfumeDetail/DetailClient.tsx | 4 +- .../domains/perfumes/PageClient.tsx | 2 +- .../postRelatedPerfume/PostRelatedPerfume.tsx | 2 +- .../domains/postDetail/PageClient.tsx | 4 +- .../postDetail/header/PostInteractions.tsx | 2 +- .../hooks/useRecentPerfumes.ts | 2 +- .../RecentViewSection/hooks/useRecentPosts.ts | 2 +- src/components/modal/LoginModalProvider.tsx | 4 +- 31 files changed, 403 insertions(+), 400 deletions(-) delete mode 100644 src/client/stores/createRecentItemsStore.ts create mode 100644 src/client/stores/perfumeStore.ts create mode 100644 src/client/stores/uiStore.ts delete mode 100644 src/client/stores/useCountStore.ts delete mode 100644 src/client/stores/useFilterStore.ts delete mode 100644 src/client/stores/useLogRecentItem.ts delete mode 100644 src/client/stores/useModalStore.ts delete mode 100644 src/client/stores/useRecentPerfumesStore.ts delete mode 100644 src/client/stores/useRecentPostsStore.ts delete mode 100644 src/client/stores/useVisibilityStore.ts diff --git a/src/app/community/post/(write)/page.tsx b/src/app/community/post/(write)/page.tsx index 70863065..c0719d3c 100644 --- a/src/app/community/post/(write)/page.tsx +++ b/src/app/community/post/(write)/page.tsx @@ -1,8 +1,10 @@ +import { Suspense } from "react"; import PageClient from "@/components/domains/post/PageClient"; + export default async function Page() { return ( - <> + - + ); } diff --git a/src/app/community/post/[id]/edit/page.tsx b/src/app/community/post/[id]/edit/page.tsx index 4839c58c..41a4d4c1 100644 --- a/src/app/community/post/[id]/edit/page.tsx +++ b/src/app/community/post/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import PageClient from "@/components/domains/post/PageClient"; import { communityApi } from "@/client/utils/api/community.api"; import getQueryClient from "@/client/utils/getQueryClient"; @@ -32,7 +33,9 @@ export default async function Page({ return ( - + + + ); } diff --git a/src/client/hooks/useInfinitePerfumes.ts b/src/client/hooks/useInfinitePerfumes.ts index d9e95b72..76604ca0 100644 --- a/src/client/hooks/useInfinitePerfumes.ts +++ b/src/client/hooks/useInfinitePerfumes.ts @@ -1,8 +1,8 @@ "use client"; import { useEffect, useMemo } from "react"; -import { useFilterStore } from "@/client/stores/useFilterStore"; -import { useTotalStore } from "@/client/stores/useCountStore"; +import { useFilterStore } from "@/client/stores/perfumeStore"; +import { useTotalStore } from "@/client/stores/uiStore"; import { useInfiniteSearchPerfumesQuery } from "@/client/hooks/query/useSearchQuery"; /** diff --git a/src/client/hooks/useRecentItemsSync.ts b/src/client/hooks/useRecentItemsSync.ts index 83ebb8f3..8043dc68 100644 --- a/src/client/hooks/useRecentItemsSync.ts +++ b/src/client/hooks/useRecentItemsSync.ts @@ -3,7 +3,7 @@ import type { UseBoundStore, StoreApi } from "zustand"; import type { GenericRecentItem, RecentItemsState, -} from "../stores/createRecentItemsStore"; +} from "../stores/perfumeStore"; import { ApiRecentPerfumeItem, ApiRecentPostItem, diff --git a/src/client/stores/createRecentItemsStore.ts b/src/client/stores/createRecentItemsStore.ts deleted file mode 100644 index 8a00f86d..00000000 --- a/src/client/stores/createRecentItemsStore.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { create } from "zustand"; -import { persist, createJSONStorage } from "zustand/middleware"; - -// 스토어에 저장될 아이템의 기본 형태 정의 -export type GenericRecentItem = { - id: string; - type: string; // 'perfume', 'post' 등 아이템 타입을 명시 - item: T; - viewedAt: number; -}; - -// 스토어의 상태(State) 타입 정의 -export interface RecentItemsState { - items: GenericRecentItem[]; - lastSyncedAt: number | null; - _hasHydrated: boolean; - setLastSyncedAt: (timestamp: number) => void; - addItem: (newItem: { id: string; item: T }) => void; - setItems: (items: GenericRecentItem[]) => void; - clearItems: () => void; -} - -// 스토어 생성을 위한 설정값 타입 정의 -interface CreateRecentItemsStoreOptions { - name: string; // localStorage에 저장될 키 이름 - maxItems: number; // 최대 저장 개수 - type: string; // 아이템 타입 -} - -/** - * 최근 본 아이템 목록을 관리하는 Zustand 스토어를 생성하는 팩토리 함수입니다. - * @param options 스토어 설정 (name, maxItems, type) - * @returns Zustand 스토어 Hook - */ -export function createRecentItemsStore({ - name, - maxItems, - type, -}: CreateRecentItemsStoreOptions) { - return create>()( - persist( - (set, get) => ({ - items: [], - lastSyncedAt: null, - _hasHydrated: false, - setLastSyncedAt: (timestamp: number) => - set({ lastSyncedAt: timestamp }), - addItem: (newItem) => { - const currentItems = get().items; - - const filteredItems = currentItems.filter( - (item) => item.id !== newItem.id - ); - - const newItemWithTimestamp: GenericRecentItem = { - ...newItem, - type, - viewedAt: Date.now(), - }; - - const updatedItems = [newItemWithTimestamp, ...filteredItems]; - const slicedItems = updatedItems.slice(0, maxItems); - - set({ items: slicedItems }); - }, - setItems: (items) => { - set({ items }); - }, - clearItems: () => set({ items: [], lastSyncedAt: null }), - }), - { - name, - storage: createJSONStorage(() => localStorage), - onRehydrateStorage: () => (state) => { - if (state) { - state._hasHydrated = true; - } - }, - } - ) - ); -} diff --git a/src/client/stores/perfumeStore.ts b/src/client/stores/perfumeStore.ts new file mode 100644 index 00000000..227dbcf4 --- /dev/null +++ b/src/client/stores/perfumeStore.ts @@ -0,0 +1,255 @@ +import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; +import { useEffect } from "react"; +import type { UseBoundStore, StoreApi } from "zustand"; +import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema"; + +// ─── createRecentItemsStore (factory) ───────────────────────────────────────── + +export type GenericRecentItem = { + id: string; + type: string; + item: T; + viewedAt: number; +}; + +export interface RecentItemsState { + items: GenericRecentItem[]; + lastSyncedAt: number | null; + _hasHydrated: boolean; + setLastSyncedAt: (timestamp: number) => void; + addItem: (newItem: { id: string; item: T }) => void; + setItems: (items: GenericRecentItem[]) => void; + clearItems: () => void; +} + +interface CreateRecentItemsStoreOptions { + name: string; + maxItems: number; + type: string; +} + +export function createRecentItemsStore({ + name, + maxItems, + type, +}: CreateRecentItemsStoreOptions) { + return create>()( + persist( + (set, get) => ({ + items: [], + lastSyncedAt: null, + _hasHydrated: false, + setLastSyncedAt: (timestamp: number) => + set({ lastSyncedAt: timestamp }), + addItem: (newItem) => { + const currentItems = get().items; + + const filteredItems = currentItems.filter( + (item) => item.id !== newItem.id + ); + + const newItemWithTimestamp: GenericRecentItem = { + ...newItem, + type, + viewedAt: Date.now(), + }; + + const updatedItems = [newItemWithTimestamp, ...filteredItems]; + const slicedItems = updatedItems.slice(0, maxItems); + + set({ items: slicedItems }); + }, + setItems: (items) => { + set({ items }); + }, + clearItems: () => set({ items: [], lastSyncedAt: null }), + }), + { + name, + storage: createJSONStorage(() => localStorage), + onRehydrateStorage: () => (state) => { + if (state) { + state._hasHydrated = true; + } + }, + } + ) + ); +} + +// ─── Filter ─────────────────────────────────────────────────────────────────── + +interface FilterStore { + committedFilters: Record; + pendingFilters: Record; + + handlePendingChange: (category: string, value: string) => void; + resetPending: () => void; + commitFilters: (category: string) => void; + cancelPending: (category: string) => void; + initializePending: () => void; + + closeFilter: (id: string) => void; + resetFilters: () => void; + + // 레거시 호환성 + filters: Record; +} + +export const useFilterStore = create((set, get) => ({ + committedFilters: {}, + pendingFilters: {}, + + handlePendingChange: (category, value) => + set((state) => { + const newPending = { ...state.pendingFilters }; + const prevArray = newPending[category] ?? []; + + const nextArray = prevArray.includes(value) + ? prevArray.filter((v) => v !== value) + : [...prevArray, value].sort(); + + if (nextArray.length === 0) { + delete newPending[category]; + } else { + newPending[category] = nextArray; + } + + return { pendingFilters: newPending }; + }), + + resetPending: () => + set(() => ({ + pendingFilters: {}, + })), + + commitFilters: (category) => + set((state) => { + const newCommitted = { ...state.committedFilters }; + const pendingValue = state.pendingFilters[category]; + + if (pendingValue && pendingValue.length > 0) { + newCommitted[category] = pendingValue; + } else { + delete newCommitted[category]; + } + + return { + committedFilters: newCommitted, + }; + }), + + cancelPending: (category) => + set((state) => { + const newPending = { ...state.pendingFilters }; + const committedValue = state.committedFilters[category]; + + if (committedValue && committedValue.length > 0) { + newPending[category] = [...committedValue]; + } else { + delete newPending[category]; + } + + return { pendingFilters: newPending }; + }), + + initializePending: () => + set((state) => ({ + pendingFilters: JSON.parse(JSON.stringify(state.committedFilters)), + })), + + closeFilter: (id) => + set((state) => { + const newCommitted = { ...state.committedFilters }; + for (const category in newCommitted) { + const values = newCommitted[category]; + if (values.includes(id)) { + const nextArray = values.filter((v) => v !== id); + if (nextArray.length === 0) { + delete newCommitted[category]; + } else { + newCommitted[category] = nextArray; + } + } + } + const newPending = { ...state.pendingFilters }; + for (const category in newPending) { + const values = newPending[category]; + if (values.includes(id)) { + const nextArray = values.filter((v) => v !== id); + if (nextArray.length === 0) { + delete newPending[category]; + } else { + newPending[category] = nextArray; + } + } + } + return { committedFilters: newCommitted, pendingFilters: newPending }; + }), + + resetFilters: () => + set({ + committedFilters: {}, + pendingFilters: {}, + }), + + // 레거시 호환성 + get filters() { + return get().committedFilters; + }, +})); + +// ─── Recent Perfumes ────────────────────────────────────────────────────────── + +type RecentPerfumeData = { + id: string; + perfumeName: string; + brandName: string; + imageUrl: string; +}; + +export type RecentPerfumeItem = GenericRecentItem; + +export const useRecentPerfumesStore = createRecentItemsStore( + { + name: "recent-perfumes", + maxItems: 10, + type: "perfume", + } +); + +// ─── Recent Posts ───────────────────────────────────────────────────────────── + +export type RecentPostItem = GenericRecentItem; + +export const useRecentPostsStore = + createRecentItemsStore({ + name: "recent-posts", + maxItems: 10, + type: "post", + }); + +// ─── useLogRecentItem (hook) ────────────────────────────────────────────────── + +type StoreWithAddItem = UseBoundStore< + StoreApi<{ + addItem: (newItem: { id: string; item: T }) => void; + }> +>; + +export function useLogRecentItem( + item: T | null | undefined, + useStore: StoreWithAddItem +) { + const addItem = useStore((state) => state.addItem); + + useEffect(() => { + if (item) { + addItem({ + id: item.id, + item: item, + }); + } + }, [item, addItem]); +} diff --git a/src/client/stores/uiStore.ts b/src/client/stores/uiStore.ts new file mode 100644 index 00000000..5a9b3fd3 --- /dev/null +++ b/src/client/stores/uiStore.ts @@ -0,0 +1,113 @@ +import { create } from "zustand"; + +// ─── Modal ─────────────────────────────────────────────────────────────────── + +export type ModalKey = "loginModal"; + +export type ModalState = Record; + +interface ModalStoreActions { + openModal: (name: ModalKey) => void; + closeModal: (name: ModalKey) => void; + toggleModal: (name: ModalKey) => void; +} + +export const MODAL_KEYS: Record = { + LOGIN: "loginModal", +}; + +// ─── Visibility ────────────────────────────────────────────────────────────── + +interface VisibilityState { + isOpen: boolean; + type: "modal" | "dropdown" | (string & {}); +} + +interface VisibilityStoreState { + visibilities: Record; + open: (id: string, type?: string) => void; + close: (id: string) => void; + toggle: (id: string) => void; + isOpen: (id: string) => boolean; +} + +// ─── Count ─────────────────────────────────────────────────────────────────── + +interface TotalStoreState { + totalCount: number | null; + setTotalCount: (n: number) => void; +} + +// ─── Combined UI Store ──────────────────────────────────────────────────────── + +type UIStore = ModalState & + ModalStoreActions & + VisibilityStoreState & + TotalStoreState; + +const initialModalState: ModalState = { + loginModal: false, +}; + +export const useUIStore = create((set, get) => ({ + // Modal + ...initialModalState, + openModal: (name) => set({ [name]: true }), + closeModal: (name) => set({ [name]: false }), + toggleModal: (name) => set((state) => ({ [name]: !state[name] })), + + // Visibility + visibilities: {}, + + open: (id, type = "dropdown") => + set((state) => { + const newVisibilities = { ...state.visibilities }; + + if (type === "modal") { + for (const key in newVisibilities) { + if (newVisibilities[key].type === "modal") { + newVisibilities[key] = { ...newVisibilities[key], isOpen: false }; + } + } + } + + newVisibilities[id] = { isOpen: true, type }; + + return { visibilities: newVisibilities }; + }), + + close: (id) => + set((state) => ({ + visibilities: { + ...state.visibilities, + [id]: { + ...state.visibilities[id], + isOpen: false, + }, + }, + })), + + toggle: (id) => { + const current = get().visibilities[id]; + const isCurrentlyOpen = current?.isOpen ?? false; + const type = current?.type ?? "dropdown"; + + if (isCurrentlyOpen) { + get().close(id); + } else { + get().open(id, type); + } + }, + + isOpen: (id) => get().visibilities[id]?.isOpen ?? false, + + // Count + totalCount: null, + setTotalCount: (n: number) => set({ totalCount: n }), +})); + +// ─── 하위 호환 re-export ────────────────────────────────────────────────────── + +export const useModalStore = useUIStore; +export const useVisibilityStore = useUIStore; +export const useTotalStore = useUIStore; diff --git a/src/client/stores/useCountStore.ts b/src/client/stores/useCountStore.ts deleted file mode 100644 index 1255f061..00000000 --- a/src/client/stores/useCountStore.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from "zustand"; - -interface TotalStore { - totalCount: number | null; - setTotalCount: (n: number) => void; -} - -export const useTotalStore = create((set) => ({ - totalCount: null, - setTotalCount: (n: number) => set({ totalCount: n }), -})); diff --git a/src/client/stores/useFilterStore.ts b/src/client/stores/useFilterStore.ts deleted file mode 100644 index 84756b40..00000000 --- a/src/client/stores/useFilterStore.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { create } from "zustand"; - -interface FilterStore { - committedFilters: Record; - pendingFilters: Record; - - handlePendingChange: (category: string, value: string) => void; - resetPending: () => void; - commitFilters: (category: string) => void; - cancelPending: (category: string) => void; - initializePending: () => void; - - // 개별 필터 항목 제거 (FilterList에서 사용) - closeFilter: (id: string) => void; - // 전체 필터 초기화 - resetFilters: () => void; - - // 레거시 호환성 (기존 코드와의 호환) - filters: Record; -} - -export const useFilterStore = create((set, get) => ({ - committedFilters: {}, - pendingFilters: {}, - - // Pending 필터 변경 (토글) - handlePendingChange: (category, value) => - set((state) => { - const newPending = { ...state.pendingFilters }; - const prevArray = newPending[category] ?? []; - - const nextArray = prevArray.includes(value) - ? prevArray.filter((v) => v !== value) - : [...prevArray, value].sort(); - - if (nextArray.length === 0) { - delete newPending[category]; - } else { - newPending[category] = nextArray; - } - - return { pendingFilters: newPending }; - }), - - // Pending 초기화 (모달 내 "초기화" 버튼) - resetPending: () => - set(() => ({ - pendingFilters: {}, - })), - - // Pending → Committed 적용 (모달 제출) - commitFilters: (category) => - set((state) => { - const newCommitted = { ...state.committedFilters }; - const pendingValue = state.pendingFilters[category]; - - if (pendingValue && pendingValue.length > 0) { - newCommitted[category] = pendingValue; - } else { - delete newCommitted[category]; - } - - return { - committedFilters: newCommitted, - }; - }), - - // 모달 취소 시 Pending 복원 (모달 열 때 committed 상태로 동기화) - cancelPending: (category) => - set((state) => { - const newPending = { ...state.pendingFilters }; - const committedValue = state.committedFilters[category]; - - if (committedValue && committedValue.length > 0) { - newPending[category] = [...committedValue]; // 복사본 생성 - } else { - delete newPending[category]; - } - - return { pendingFilters: newPending }; - }), - - // 모달 열 때 전체 pending을 committed로 동기화 - initializePending: () => - set((state) => ({ - pendingFilters: JSON.parse(JSON.stringify(state.committedFilters)), - })), - - // 개별 필터 제거 (FilterList의 X 버튼) - closeFilter: (id) => - set((state) => { - const newCommitted = { ...state.committedFilters }; - for (const category in newCommitted) { - const values = newCommitted[category]; - if (values.includes(id)) { - const nextArray = values.filter((v) => v !== id); - if (nextArray.length === 0) { - delete newCommitted[category]; - } else { - newCommitted[category] = nextArray; - } - } - } - // pending도 동기화 - const newPending = { ...state.pendingFilters }; - for (const category in newPending) { - const values = newPending[category]; - if (values.includes(id)) { - const nextArray = values.filter((v) => v !== id); - if (nextArray.length === 0) { - delete newPending[category]; - } else { - newPending[category] = nextArray; - } - } - } - return { committedFilters: newCommitted, pendingFilters: newPending }; - }), - - // 전체 필터 초기화 - resetFilters: () => - set({ - committedFilters: {}, - pendingFilters: {}, - }), - - // 레거시 호환성 - get filters() { - return get().committedFilters; - }, -})); diff --git a/src/client/stores/useLogRecentItem.ts b/src/client/stores/useLogRecentItem.ts deleted file mode 100644 index 20cb4394..00000000 --- a/src/client/stores/useLogRecentItem.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect } from "react"; -import type { UseBoundStore, StoreApi } from "zustand"; - -type StoreWithAddItem = UseBoundStore< - StoreApi<{ - addItem: (newItem: { id: string; item: T }) => void; - }> ->; - -export function useLogRecentItem( - item: T | null | undefined, - useStore: StoreWithAddItem -) { - const addItem = useStore((state) => state.addItem); - - useEffect(() => { - if (item) { - addItem({ - id: item.id, - item: item, - }); - } - }, [item, addItem]); -} diff --git a/src/client/stores/useModalStore.ts b/src/client/stores/useModalStore.ts deleted file mode 100644 index f3c9afba..00000000 --- a/src/client/stores/useModalStore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { create } from "zustand"; - -//modal 구독 -export type ModalKey = "loginModal"; - -export type ModalState = Record; - -interface ModalStoreActions { - openModal: (name: ModalKey) => void; - closeModal: (name: ModalKey) => void; - toggleModal: (name: ModalKey) => void; -} - -const initialState: ModalState = { - loginModal: false, -}; - -export const useModalStore = create((set) => ({ - ...initialState, - openModal: (name) => set({ [name]: true }), - closeModal: (name) => set({ [name]: false }), - toggleModal: (name) => set((state) => ({ [name]: !state[name] })), -})); - -export const MODAL_KEYS: Record = { - LOGIN: "loginModal", -}; diff --git a/src/client/stores/useRecentPerfumesStore.ts b/src/client/stores/useRecentPerfumesStore.ts deleted file mode 100644 index b53d671d..00000000 --- a/src/client/stores/useRecentPerfumesStore.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createRecentItemsStore } from "./createRecentItemsStore"; -import type { GenericRecentItem } from "./createRecentItemsStore"; - -type RecentPerfumeData = { - id: string; - perfumeName: string; - brandName: string; - imageUrl: string; -}; - -export type RecentPerfumeItem = GenericRecentItem; - -const MAX_RECENT_PERFUMES = 10; - -export const useRecentPerfumesStore = createRecentItemsStore( - { - name: "recent-perfumes", - maxItems: MAX_RECENT_PERFUMES, - type: "perfume", - } -); diff --git a/src/client/stores/useRecentPostsStore.ts b/src/client/stores/useRecentPostsStore.ts deleted file mode 100644 index 0f44a142..00000000 --- a/src/client/stores/useRecentPostsStore.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createRecentItemsStore } from "./createRecentItemsStore"; -import type { GenericRecentItem } from "./createRecentItemsStore"; -import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema"; - -export type RecentPostItem = GenericRecentItem; - -const MAX_RECENT_POSTS = 10; - -export const useRecentPostsStore = - createRecentItemsStore({ - name: "recent-posts", - maxItems: MAX_RECENT_POSTS, - type: "post", - }); diff --git a/src/client/stores/useVisibilityStore.ts b/src/client/stores/useVisibilityStore.ts deleted file mode 100644 index 6c2d0d72..00000000 --- a/src/client/stores/useVisibilityStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { create } from "zustand"; - -interface VisibilityState { - isOpen: boolean; - type: "modal" | "dropdown" | (string & {}); -} - -interface VisibilityStore { - visibilities: Record; - open: (id: string, type?: string) => void; - close: (id: string) => void; - toggle: (id: string) => void; - isOpen: (id: string) => boolean; -} - -export const useVisibilityStore = create((set, get) => ({ - visibilities: {}, - - open: (id, type = "dropdown") => - set((state) => { - const newVisibilities = { ...state.visibilities }; - - if (type === "modal") { - for (const key in newVisibilities) { - if (newVisibilities[key].type === "modal") { - newVisibilities[key] = { ...newVisibilities[key], isOpen: false }; - } - } - } - - newVisibilities[id] = { isOpen: true, type }; - - return { visibilities: newVisibilities }; - }), - - close: (id) => - set((state) => ({ - visibilities: { - ...state.visibilities, - [id]: { - ...state.visibilities[id], - isOpen: false, - }, - }, - })), - - toggle: (id) => { - const current = get().visibilities[id]; - const isOpen = current?.isOpen ?? false; - const type = current?.type ?? "dropdown"; - - if (isOpen) { - get().close(id); - } else { - get().open(id, type); - } - }, - - isOpen: (id) => get().visibilities[id]?.isOpen ?? false, -})); diff --git a/src/components/commons/Provider/RecentSyncManager.tsx b/src/components/commons/Provider/RecentSyncManager.tsx index 11007a37..2e287bd0 100644 --- a/src/components/commons/Provider/RecentSyncManager.tsx +++ b/src/components/commons/Provider/RecentSyncManager.tsx @@ -2,8 +2,8 @@ import { useEffect } from "react"; import { useRecentItemsSync } from "@/client/hooks/useRecentItemsSync"; -import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; -import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; +import { useRecentPerfumesStore } from "@/client/stores/perfumeStore"; +import { useRecentPostsStore } from "@/client/stores/perfumeStore"; import { useUserStore } from "@/client/stores/useUserStore"; import { pointApi } from "@/client/utils/api/points.api"; diff --git a/src/components/commons/dropdown/DropdownBase.tsx b/src/components/commons/dropdown/DropdownBase.tsx index 76bca167..7cc0db88 100644 --- a/src/components/commons/dropdown/DropdownBase.tsx +++ b/src/components/commons/dropdown/DropdownBase.tsx @@ -3,7 +3,7 @@ import DropdownMenu from "./DropdownMenu"; import Image from "next/image"; import ICONS from "@/shared/constants/icons"; import { Option } from "@/shared/constants/options"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; interface IFilterDropdownProps { id: string; diff --git a/src/components/commons/navBar/UserProfileSection.tsx b/src/components/commons/navBar/UserProfileSection.tsx index 18e2bec1..dc8bdf2a 100644 --- a/src/components/commons/navBar/UserProfileSection.tsx +++ b/src/components/commons/navBar/UserProfileSection.tsx @@ -5,7 +5,7 @@ import { useUserStore } from "@/client/stores/useUserStore"; import { NAV_LABELS } from "./navBar.constants"; import ICONS from "@/shared/constants/icons"; import { NavDropdown } from "./Dropdown"; -import { useModalStore, MODAL_KEYS } from "@/client/stores/useModalStore"; +import { useModalStore, MODAL_KEYS } from "@/client/stores/uiStore"; const UserProfileSection = () => { const navBarRef = useRef(null); diff --git a/src/components/commons/perfumeList/filter/FilterItem.tsx b/src/components/commons/perfumeList/filter/FilterItem.tsx index 82773167..d495a7a6 100644 --- a/src/components/commons/perfumeList/filter/FilterItem.tsx +++ b/src/components/commons/perfumeList/filter/FilterItem.tsx @@ -1,8 +1,8 @@ import { memo, useState, useEffect } from "react"; import Image from "next/image"; import { Option } from "@/shared/constants/options"; -import { useFilterStore } from "@/client/stores/useFilterStore"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useFilterStore } from "@/client/stores/perfumeStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; import ICONS from "@/shared/constants/icons"; import FilterItemModal from "../modal/ItemModal"; import FilterTriggerButton from "@/components/commons/dropdown/DropdownButton"; diff --git a/src/components/commons/perfumeList/filter/FilterList.tsx b/src/components/commons/perfumeList/filter/FilterList.tsx index 9b89d66d..15a4f1bb 100644 --- a/src/components/commons/perfumeList/filter/FilterList.tsx +++ b/src/components/commons/perfumeList/filter/FilterList.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import { useFilterStore } from "@/client/stores/useFilterStore"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useFilterStore } from "@/client/stores/perfumeStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; import ICONS from "@/shared/constants/icons"; import { Option } from "@/shared/constants/options"; import ListModal from "../modal/ListModal"; diff --git a/src/components/commons/perfumeList/modal/ItemModal.tsx b/src/components/commons/perfumeList/modal/ItemModal.tsx index 94bae208..99a3f9d3 100644 --- a/src/components/commons/perfumeList/modal/ItemModal.tsx +++ b/src/components/commons/perfumeList/modal/ItemModal.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo } from "react"; import { Option } from "@/shared/constants/options"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; -import { useFilterStore } from "@/client/stores/useFilterStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; +import { useFilterStore } from "@/client/stores/perfumeStore"; import { ModalContainer } from "@/components/modal/ModalContainer"; import { FilterModalLayout } from "./Layout"; import { getLabel } from "../perfumes.helpers"; diff --git a/src/components/commons/perfumeList/modal/Layout.tsx b/src/components/commons/perfumeList/modal/Layout.tsx index f5e23a5d..6d540a1d 100644 --- a/src/components/commons/perfumeList/modal/Layout.tsx +++ b/src/components/commons/perfumeList/modal/Layout.tsx @@ -2,7 +2,7 @@ import { ReactNode } from "react"; // import { useTotalStore } from "@/client/stores/useCountStore"; import { ButtonFilledPrimaryLFull } from "@/components/commons/button/ButtonFilled"; import { ButtonOutlinedPrimaryLFull } from "@/components/commons/button/ButtonOutlined"; -import { useFilterStore } from "@/client/stores/useFilterStore"; +import { useFilterStore } from "@/client/stores/perfumeStore"; import { Indicator } from "../../loading/Indicator"; interface FilterModalLayoutProps { diff --git a/src/components/commons/wrapper/PageWrapper.tsx b/src/components/commons/wrapper/PageWrapper.tsx index 30120bb2..3bfd2116 100644 --- a/src/components/commons/wrapper/PageWrapper.tsx +++ b/src/components/commons/wrapper/PageWrapper.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; -import { useModalStore, MODAL_KEYS } from "@/client/stores/useModalStore"; +import { useModalStore, MODAL_KEYS } from "@/client/stores/uiStore"; interface PageWrapperProps { className?: string; diff --git a/src/components/domains/main/MainSearchBar.tsx b/src/components/domains/main/MainSearchBar.tsx index cb608eb1..91aadb97 100644 --- a/src/components/domains/main/MainSearchBar.tsx +++ b/src/components/domains/main/MainSearchBar.tsx @@ -8,7 +8,7 @@ import useDebounce from "@/client/hooks/useDebounce"; import { searchApi } from "@/client/utils/api/search.api"; import { SearchResultsDropdown } from "@/components/commons/dropdown/SearchResultsDropdown"; import useOnClickOutside from "@/client/hooks/useOnClickOutside"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; import Image from "next/image"; import { highlightText } from "@/shared/utils/highlightText"; import Logo from "../../../..//public/images/Logo.webp"; diff --git a/src/components/domains/perfumeDetail/DetailClient.tsx b/src/components/domains/perfumeDetail/DetailClient.tsx index ad502b51..2481e95b 100644 --- a/src/components/domains/perfumeDetail/DetailClient.tsx +++ b/src/components/domains/perfumeDetail/DetailClient.tsx @@ -10,8 +10,8 @@ import { PerfumeReviews } from "./review"; import { PerfumeDetailSidebar } from "./sidebar"; import { MobileSeparator } from "@/components/commons/mobileSeparator"; import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema"; -import { useLogRecentItem } from "@/client/stores/useLogRecentItem"; -import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; +import { useLogRecentItem } from "@/client/stores/perfumeStore"; +import { useRecentPerfumesStore } from "@/client/stores/perfumeStore"; // temp // sematic tag 적용하기 (하위 컴포넌트 전부) diff --git a/src/components/domains/perfumes/PageClient.tsx b/src/components/domains/perfumes/PageClient.tsx index f7304517..5b0c2e5e 100644 --- a/src/components/domains/perfumes/PageClient.tsx +++ b/src/components/domains/perfumes/PageClient.tsx @@ -13,7 +13,7 @@ import SortDropdown from "@/components/commons/dropdown/SortDropdown"; import { useInfinitePerfumes } from "@/client/hooks/useInfinitePerfumes"; import { usePerfumeSearchState } from "./hook/usePerfumeSearchState"; import { useInfiniteScrollTrigger } from "@/client/hooks/useInfiniteScrollTrigger"; -import { useFilterStore } from "@/client/stores/useFilterStore"; +import { useFilterStore } from "@/client/stores/perfumeStore"; import { useMemo } from "react"; export default function PageClient() { diff --git a/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx b/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx index 4dce5104..0471f544 100644 --- a/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx +++ b/src/components/domains/post/form/postRelatedPerfume/PostRelatedPerfume.tsx @@ -8,7 +8,7 @@ import InputBase from "@/components/commons/input"; import type { PerfumeForPost } from "@/server/hono/schemas/community.schema"; import useDebounce from "@/client/hooks/useDebounce"; import useOnClickOutside from "@/client/hooks/useOnClickOutside"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; import { searchApi } from "@/client/utils/api/search.api"; import SubTitleLabel from "../element/SubTitleLabel"; diff --git a/src/components/domains/postDetail/PageClient.tsx b/src/components/domains/postDetail/PageClient.tsx index b1f83480..e2e38021 100644 --- a/src/components/domains/postDetail/PageClient.tsx +++ b/src/components/domains/postDetail/PageClient.tsx @@ -3,8 +3,8 @@ import dynamic from "next/dynamic"; import PostContent from "./content"; import PostDetailHeader from "./header"; -import { useLogRecentItem } from "@/client/stores/useLogRecentItem"; -import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; +import { useLogRecentItem } from "@/client/stores/perfumeStore"; +import { useRecentPostsStore } from "@/client/stores/perfumeStore"; import { PageSkeleton, CategoryPostListSectionSkeleton, diff --git a/src/components/domains/postDetail/header/PostInteractions.tsx b/src/components/domains/postDetail/header/PostInteractions.tsx index 29a555e5..a52975f9 100644 --- a/src/components/domains/postDetail/header/PostInteractions.tsx +++ b/src/components/domains/postDetail/header/PostInteractions.tsx @@ -6,7 +6,7 @@ import { ShareIcon } from "@/components/commons/interactions/icons/ShareIcon"; import { Interactions } from "@/components/commons/interactions"; import usePostInteractionMutation from "./usePostInteractionMutation"; import { useRef } from "react"; -import { useVisibilityStore } from "@/client/stores/useVisibilityStore"; +import { useVisibilityStore } from "@/client/stores/uiStore"; import ShareDropdownMenu from "@/components/commons/dropdown/ShareDropdownMenu"; import useOnClickOutside from "@/client/hooks/useOnClickOutside"; diff --git a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts index 6b347cd1..a2b78ce5 100644 --- a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts +++ b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useRecentPerfumesStore } from "@/client/stores/useRecentPerfumesStore"; +import { useRecentPerfumesStore } from "@/client/stores/perfumeStore"; import { useFooterPagination } from "./useFooterPagination"; const PERFUMES_PER_VIEW = 5; diff --git a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts index 5fa68c71..381e0949 100644 --- a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts +++ b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useRecentPostsStore } from "@/client/stores/useRecentPostsStore"; +import { useRecentPostsStore } from "@/client/stores/perfumeStore"; import { useFooterPagination } from "./useFooterPagination"; import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema"; diff --git a/src/components/modal/LoginModalProvider.tsx b/src/components/modal/LoginModalProvider.tsx index f7309d79..2b19acc6 100644 --- a/src/components/modal/LoginModalProvider.tsx +++ b/src/components/modal/LoginModalProvider.tsx @@ -2,9 +2,9 @@ import { useState, useEffect, Suspense } from "react"; import { createPortal } from "react-dom"; -import { useModalStore } from "@/client/stores/useModalStore"; +import { useModalStore } from "@/client/stores/uiStore"; import { LoginModal } from "@/components/modal/LoginModal"; -import { MODAL_KEYS } from "@/client/stores/useModalStore"; +import { MODAL_KEYS } from "@/client/stores/uiStore"; export default function LoginModalProvider() { const { loginModal, closeModal } = useModalStore(); From 891254b78d04958928ec18c0a4ae765cdd317c80 Mon Sep 17 00:00:00 2001 From: kuan Date: Sun, 29 Mar 2026 02:32:43 +0900 Subject: [PATCH 09/39] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=B2=88=EB=93=A4=20=EC=B5=9C=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- .../commons/ckeditor5/ck-content.css | 21 ++++++++++++++++ .../commons/ckeditor5/ckeditor5.css | 24 ++----------------- .../domains/postDetail/content/index.tsx | 2 +- 4 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 src/components/commons/ckeditor5/ck-content.css diff --git a/package.json b/package.json index 0416391a..0a2d1056 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "react-naver-maps": "^0.1.4", "sanitize-html": "^2.17.2", "sharp": "^0.33.5", - "swagger-ui-react": "^5.25.3", "swiper": "^11.2.7", "tailwind-merge": "^2.5.5", "tailwind-scrollbar-hide": "^2.0.0", @@ -72,6 +71,7 @@ "@types/react-dom": "^18", "@types/sanitize-html": "^2.16.1", "@types/swagger-ui-react": "^5.18.0", + "swagger-ui-react": "^5.25.3", "@vitejs/plugin-react": "^5.1.1", "@vitest/ui": "^4.0.9", "cross-env": "^10.1.0", diff --git a/src/components/commons/ckeditor5/ck-content.css b/src/components/commons/ckeditor5/ck-content.css new file mode 100644 index 00000000..8e15a586 --- /dev/null +++ b/src/components/commons/ckeditor5/ck-content.css @@ -0,0 +1,21 @@ +.ck-content { + line-height: 1.6; + word-break: break-word; +} + +.ck-content h1 { + font-size: 20px; + font-weight: bold; + margin: 1.5em 0 1em; +} + +.ck-content h2 { + font-size: 18px; + font-weight: bold; + margin: 1.2em 0 0.8em; +} + +.ck-content h3 { + font-size: 16px; + font-weight: bold; +} diff --git a/src/components/commons/ckeditor5/ckeditor5.css b/src/components/commons/ckeditor5/ckeditor5.css index 0203c11e..86cf14cb 100644 --- a/src/components/commons/ckeditor5/ckeditor5.css +++ b/src/components/commons/ckeditor5/ckeditor5.css @@ -1,3 +1,5 @@ +@import "./ck-content.css"; + :root { --ck-color-focus-border: #a47764; --ck-font-size-base: 14px; @@ -10,28 +12,6 @@ margin-left: auto; margin-right: auto; } - -.ck-content { - line-height: 1.6; - word-break: break-word; -} - -.ck-content h1 { - font-size: 20px; - font-weight: bold; - margin: 1.5em 0 1em; -} - -.ck-content h2 { - font-size: 18px; - font-weight: bold; - margin: 1.2em 0 0.8em; -} - -.ck-content h3 { - font-size: 16px; - font-weight: bold; -} .editor-container_classic-editor .editor-container__editor { width: 100%; min-height: 640px; diff --git a/src/components/domains/postDetail/content/index.tsx b/src/components/domains/postDetail/content/index.tsx index 250eb250..44f306b9 100644 --- a/src/components/domains/postDetail/content/index.tsx +++ b/src/components/domains/postDetail/content/index.tsx @@ -1,6 +1,6 @@ "use client"; -import "@/components/commons/ckeditor5/ckeditor5.css"; +import "@/components/commons/ckeditor5/ck-content.css"; import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import PostActions from "../header/PostActions"; import RelatedPerfume from "./RelatedPerfume"; From a850fe2456c26476191dd8a0df10770c62c36c63 Mon Sep 17 00:00:00 2001 From: kuan Date: Mon, 30 Mar 2026 01:27:03 +0900 Subject: [PATCH 10/39] =?UTF-8?q?test:=20drift=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/__tests__/brand.service.test.ts | 226 ++++++++-- .../__tests__/comment.service.test.ts | 57 +++ .../services/__tests__/draft.service.test.ts | 424 ++++++++++++++++++ .../__tests__/earnPoints.service.test.ts | 139 +++--- .../__tests__/helpers/draft.test.helpers.ts | 115 +++++ .../services/__tests__/point.service.test.ts | 281 +++++++++++- 6 files changed, 1121 insertions(+), 121 deletions(-) create mode 100644 src/server/hono/services/__tests__/draft.service.test.ts create mode 100644 src/server/hono/services/__tests__/helpers/draft.test.helpers.ts diff --git a/src/server/hono/services/__tests__/brand.service.test.ts b/src/server/hono/services/__tests__/brand.service.test.ts index 2e143399..a66a2e1f 100644 --- a/src/server/hono/services/__tests__/brand.service.test.ts +++ b/src/server/hono/services/__tests__/brand.service.test.ts @@ -1,9 +1,10 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { prisma } from "@/server/prisma"; import { getAllBrandsService, getBrandByIdService, getBrandByNameService, + getStoresByNameService, } from "../brand.service"; /** @@ -12,13 +13,8 @@ import { * 테스트 전략: * - 브랜드 CRUD 핵심 로직 검증 * - nameKo null 시 nameEn 대체 로직 확인 + * - 네이버 API 호출 및 거리 계산 검증 * - 에러 처리 검증 - * - * 주요 시나리오: - * 1. 전체 브랜드 목록 조회 - * 2. ID로 브랜드 조회 (존재/미존재) - * 3. 이름으로 브랜드 조회 (존재/미존재) - * 4. nameKo null 시 nameEn 대체 */ // Mock Prisma @@ -31,7 +27,7 @@ vi.mock("@/server/prisma", () => ({ }, })); -vi.mock("../utils/prisma.utils", () => ({ +vi.mock("../../repositories/brand.repository", () => ({ brandDetailSelect: { id: true, nameEn: true, @@ -41,7 +37,16 @@ vi.mock("../utils/prisma.utils", () => ({ brandUrl: true, mapLocation: true, }, - parseMapLocation: vi.fn((location) => location), + parseMapLocation: vi.fn((location) => { + if (location === null) return null; + if (typeof location === "object" && location !== null && !Array.isArray(location)) { + const obj = location as Record; + if (typeof obj.latitude === "number" && typeof obj.longitude === "number") { + return { latitude: obj.latitude, longitude: obj.longitude }; + } + } + return null; + }), })); describe("Brand Service", () => { @@ -52,24 +57,8 @@ describe("Brand Service", () => { describe("getAllBrandsService", () => { it("모든 브랜드를 조회해야 한다", async () => { const mockBrands = [ - { - id: "brand-1", - nameEn: "Brand One", - nameKo: "브랜드 원", - description: "Description 1", - imageUrl: "brand1.jpg", - brandUrl: "https://brand1.com", - mapLocation: null, - }, - { - id: "brand-2", - nameEn: "Brand Two", - nameKo: null, - description: "Description 2", - imageUrl: "brand2.jpg", - brandUrl: null, - mapLocation: null, - }, + { id: "brand-1", nameEn: "Brand One", nameKo: "브랜드 원" }, + { id: "brand-2", nameEn: "Brand Two", nameKo: null }, ]; vi.mocked(prisma.brand.findMany).mockResolvedValue(mockBrands as never); @@ -80,14 +69,12 @@ describe("Brand Service", () => { if (result.success) { expect(result.data).toHaveLength(2); expect(result.data[0].nameKo).toBe("브랜드 원"); - expect(result.data[1].nameKo).toBe("Brand Two"); // nameKo가 null이면 nameEn 사용 + expect(result.data[1].nameKo).toBe("Brand Two"); // nameKo null → nameEn 사용 } }); it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { - vi.mocked(prisma.brand.findMany).mockRejectedValue( - new Error("Database error") - ); + vi.mocked(prisma.brand.findMany).mockRejectedValue(new Error("Database error")); const result = await getAllBrandsService(); @@ -142,6 +129,25 @@ describe("Brand Service", () => { } }); + it("mapLocation이 있으면 파싱하여 반환해야 한다", async () => { + const { parseMapLocation } = await import("../../repositories/brand.repository"); + const mockBrand = { + id: "brand-1", + nameEn: "Test Brand", + nameKo: "테스트 브랜드", + description: null, + imageUrl: null, + brandUrl: null, + mapLocation: { latitude: 37.5665, longitude: 126.978 }, + }; + + vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never); + + await getBrandByIdService("brand-1"); + + expect(parseMapLocation).toHaveBeenCalledWith(mockBrand.mapLocation); + }); + it("존재하지 않는 브랜드는 NOT_FOUND 에러를 반환해야 한다", async () => { vi.mocked(prisma.brand.findUnique).mockResolvedValue(null); @@ -152,6 +158,17 @@ describe("Brand Service", () => { expect(result.error).toBe("NOT_FOUND"); } }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.mocked(prisma.brand.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await getBrandByIdService("brand-1"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); }); describe("getBrandByNameService", () => { @@ -168,7 +185,7 @@ describe("Brand Service", () => { vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never); - const result = await getBrandByNameService("Test Brand"); + const result = await getBrandByNameService("테스트 브랜드"); expect(result.success).toBe(true); if (result.success) { @@ -186,5 +203,150 @@ describe("Brand Service", () => { expect(result.error).toBe("NOT_FOUND"); } }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.mocked(prisma.brand.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await getBrandByNameService("테스트 브랜드"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + }); + + describe("getStoresByNameService", () => { + const mockNaverResponse = { + total: 3, + items: [ + { + title: "샤넬 명동", + address: "서울 중구 명동길 1", + roadAddress: "서울 중구 명동길 1", + telephone: "02-1234-5678", + mapx: "1269780000", + mapy: "375665000", + category: "쇼핑,화장품", + link: "https://chanel.com", + }, + { + title: "샤넬 강남", + address: "서울 강남구 압구정로 1", + roadAddress: "서울 강남구 압구정로 1", + telephone: "02-9876-5432", + mapx: "1270540000", + mapy: "373936000", + category: "쇼핑,화장품", + link: "", + }, + ], + }; + + beforeEach(() => { + // 환경변수 설정 + process.env.NAVER_CLIENT_ID = "test-client-id"; + process.env.NAVER_CLIENT_SECRET = "test-client-secret"; + + // global fetch mock + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue(mockNaverResponse), + }) + ); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + delete process.env.NAVER_CLIENT_ID; + delete process.env.NAVER_CLIENT_SECRET; + }); + + it("네이버 API를 호출하고 매장 목록을 반환해야 한다", async () => { + const result = await getStoresByNameService("샤넬"); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.stores).toHaveLength(2); + expect(result.data.total).toBe(3); + // HTML 태그 제거 확인 + expect(result.data.stores[0].name).toBe("샤넬 명동"); + } + }); + + it("coords가 제공되면 거리순 정렬이 적용되어야 한다", async () => { + // 강남점 근처 좌표 (decimal 형식, service에서 parseFloat으로 파싱) + const coords = { x: "127.054", y: "37.3936" }; + + const result = await getStoresByNameService("샤넬", coords); + + expect(result.success).toBe(true); + if (result.success) { + // distance 필드가 추가되어야 함 + expect(result.data.stores[0]).toHaveProperty("distance"); + // 강남에서 가까운 강남점이 먼저 와야 함 + expect(result.data.stores[0].name).toBe("샤넬 강남"); + } + }); + + it("검색 결과가 없으면 빈 배열을 반환해야 한다", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ total: 0, items: [] }), + }) + ); + + const result = await getStoresByNameService("존재하지않는브랜드"); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.stores).toHaveLength(0); + expect(result.data.total).toBe(0); + } + }); + + it("NAVER API 자격증명이 없으면 INTERNAL_ERROR를 반환해야 한다", async () => { + delete process.env.NAVER_CLIENT_ID; + delete process.env.NAVER_CLIENT_SECRET; + + const result = await getStoresByNameService("샤넬"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + + it("네이버 API 응답 오류 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + status: 500, + }) + ); + + const result = await getStoresByNameService("샤넬"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + + it("네트워크 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error"))); + + const result = await getStoresByNameService("샤넬"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); }); }); diff --git a/src/server/hono/services/__tests__/comment.service.test.ts b/src/server/hono/services/__tests__/comment.service.test.ts index 719836c3..52a13d42 100644 --- a/src/server/hono/services/__tests__/comment.service.test.ts +++ b/src/server/hono/services/__tests__/comment.service.test.ts @@ -6,6 +6,7 @@ import { updateCommentService, deleteCommentService, getPaginatedCommentService, + getCommentService, } from "../comment.service"; import { DELETED_COMMENT_MESSAGE_BY_USER } from "../../schemas/comment.schema"; import { getTestData } from "./helpers/comment.test.helpers"; @@ -502,4 +503,60 @@ describe("Comment Service", () => { } }); }); + + describe("getCommentService", () => { + it("게시글의 전체 댓글 목록을 반환해야 한다", async () => { + const { ids, mockPost, mockComments } = getTestData(); + + vi.mocked(prisma.post.findUnique).mockResolvedValue(mockPost as never); + vi.mocked(prisma.comment.findMany).mockResolvedValue(mockComments(3) as never); + + const result = await getCommentService(ids.postId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveLength(3); + } + }); + + it("parentId=null 조건으로 최상위 댓글만 조회해야 한다", async () => { + const { ids, mockPost, mockComments } = getTestData(); + + vi.mocked(prisma.post.findUnique).mockResolvedValue(mockPost as never); + vi.mocked(prisma.comment.findMany).mockResolvedValue(mockComments(2) as never); + + await getCommentService(ids.postId); + + expect(prisma.comment.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { postId: ids.postId, parentId: null }, + }) + ); + }); + + it("존재하지 않는 postId이면 NOT_FOUND를 반환해야 한다", async () => { + vi.mocked(prisma.post.findUnique).mockResolvedValue(null); + + const result = await getCommentService("non-existent-post"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + const { ids, mockPost } = getTestData(); + + vi.mocked(prisma.post.findUnique).mockResolvedValue(mockPost as never); + vi.mocked(prisma.comment.findMany).mockRejectedValue(new Error("Database error")); + + const result = await getCommentService(ids.postId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + }); }); diff --git a/src/server/hono/services/__tests__/draft.service.test.ts b/src/server/hono/services/__tests__/draft.service.test.ts new file mode 100644 index 00000000..ee634061 --- /dev/null +++ b/src/server/hono/services/__tests__/draft.service.test.ts @@ -0,0 +1,424 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { prisma } from "@/server/prisma"; +import { DraftType } from "@prisma/client"; +import { + createOrUpdateDraftService, + getDraftService, + listDraftsService, + deleteDraftService, +} from "../draft.service"; +import { getTestData } from "./helpers/draft.test.helpers"; +import { checkResourceExists } from "../../repositories/base.repository"; + +/** + * Draft 서비스 테스트 + * + * 테스트 전략: + * - CREATE/UPDATE 타입 분기 검증 + * - 소유권 확인 (FORBIDDEN) + * - perfumeIds 기반 향수 조회 (batch 최적화 포함) + * - 에러 처리 검증 + */ + +vi.mock("@/server/prisma", () => ({ + prisma: { + postDraft: { + upsert: vi.fn(), + findUnique: vi.fn(), + findMany: vi.fn(), + delete: vi.fn(), + }, + perfume: { + findMany: vi.fn(), + }, + }, +})); + +vi.mock("../../repositories/base.repository", () => ({ + checkResourceExists: vi.fn(), +})); + +describe("Draft Service", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("createOrUpdateDraftService", () => { + describe("CREATE 타입", () => { + it("CREATE 타입 임시 저장을 생성하고 향수 정보를 반환해야 한다", async () => { + const { ids, createDraftPayload, mockDraft, mockPerfumeRecord } = getTestData(); + vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true }); + vi.mocked(prisma.postDraft.upsert).mockResolvedValue(mockDraft() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + ] as never); + + const result = await createOrUpdateDraftService(createDraftPayload()); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe(DraftType.CREATE); + expect(result.data.perfumes).toHaveLength(1); + expect(result.data.perfumes![0].nameKo).toBe("테스트 향수"); + expect(result.data.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO string + } + }); + + it("perfumeIds가 빈 배열이어도 정상 생성되어야 한다", async () => { + const { createDraftPayload, mockDraft } = getTestData(); + const emptyPerfumeDraft = { ...mockDraft(), perfumeIds: [] }; + vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true }); + vi.mocked(prisma.postDraft.upsert).mockResolvedValue(emptyPerfumeDraft as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([] as never); + + const result = await createOrUpdateDraftService({ + ...createDraftPayload(), + perfumeIds: [], + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.perfumes).toHaveLength(0); + } + }); + + it("CREATE 타입은 postId가 있어도 null로 저장되어야 한다", async () => { + const { ids, createDraftPayload, mockDraft, mockPerfumeRecord } = getTestData(); + vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true }); + vi.mocked(prisma.postDraft.upsert).mockResolvedValue(mockDraft() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + ] as never); + + await createOrUpdateDraftService({ + ...createDraftPayload(), + }); + + // upsert 호출 시 CREATE 타입의 postId는 null이어야 함 + expect(prisma.postDraft.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + create: expect.objectContaining({ postId: null }), + update: expect.objectContaining({ postId: null }), + }) + ); + }); + }); + + describe("UPDATE 타입", () => { + it("UPDATE 타입에 postId가 없으면 BAD_REQUEST를 반환해야 한다", async () => { + const { updateDraftPayload } = getTestData(); + vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true }); + + const result = await createOrUpdateDraftService({ + ...updateDraftPayload(), + postId: undefined, + }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("BAD_REQUEST"); + } + }); + + it("UPDATE 타입에 postId와 함께 임시 저장을 생성해야 한다", async () => { + const { ids, updateDraftPayload, mockUpdateDraft, mockPerfumeRecord } = getTestData(); + vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true }); + vi.mocked(prisma.postDraft.upsert).mockResolvedValue(mockUpdateDraft() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + mockPerfumeRecord(ids.perfumeId2), + ] as never); + + const result = await createOrUpdateDraftService(updateDraftPayload()); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe(DraftType.UPDATE); + expect(result.data.postId).toBe(ids.postId); + } + }); + + it("UPDATE 타입에서 존재하지 않는 postId이면 NOT_FOUND를 반환해야 한다", async () => { + const { updateDraftPayload } = getTestData(); + vi.mocked(checkResourceExists) + .mockResolvedValueOnce({ success: true, data: true }) // user 존재 + .mockResolvedValueOnce({ + success: false, + error: "NOT_FOUND", + message: "게시글을(를) 찾을 수 없습니다.", + }); // post 미존재 + + const result = await createOrUpdateDraftService(updateDraftPayload()); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + }); + + describe("에러 처리", () => { + it("존재하지 않는 userId이면 NOT_FOUND를 반환해야 한다", async () => { + const { createDraftPayload } = getTestData(); + vi.mocked(checkResourceExists).mockResolvedValue({ + success: false, + error: "NOT_FOUND", + message: "사용자을(를) 찾을 수 없습니다.", + }); + + const result = await createOrUpdateDraftService(createDraftPayload()); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + const { createDraftPayload } = getTestData(); + vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true }); + vi.mocked(prisma.postDraft.upsert).mockRejectedValue(new Error("Database error")); + + const result = await createOrUpdateDraftService(createDraftPayload()); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + }); + }); + + describe("getDraftService", () => { + it("임시 저장을 조회하고 향수 정보를 포함하여 반환해야 한다", async () => { + const { ids, mockDraft, mockPerfumeRecord } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(mockDraft() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + ] as never); + + const result = await getDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.id).toBe(ids.draftId); + expect(result.data.perfumes).toHaveLength(1); + expect(result.data.updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); + } + }); + + it("perfumeIds 기반으로 향수 정보를 조회해야 한다", async () => { + const { ids, mockDraft, mockPerfumeRecord } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(mockDraft() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + ] as never); + + await getDraftService(ids.draftId, ids.userId); + + expect(prisma.perfume.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: { in: [ids.perfumeId1] } }, + }) + ); + }); + + it("존재하지 않는 draft이면 NOT_FOUND를 반환해야 한다", async () => { + const { ids } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(null); + + const result = await getDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("다른 사용자의 draft 조회 시 FORBIDDEN을 반환해야 한다", async () => { + const { ids, mockDraft } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue({ + ...mockDraft(), + userId: "other-user-id", + } as never); + + const result = await getDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("FORBIDDEN"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + const { ids } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await getDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + }); + + describe("listDraftsService", () => { + it("사용자의 모든 임시 저장을 최신순으로 반환해야 한다", async () => { + const { ids, mockDraftList, mockPerfumeRecord } = getTestData(); + + vi.mocked(prisma.postDraft.findMany).mockResolvedValue(mockDraftList() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + mockPerfumeRecord(ids.perfumeId2), + ] as never); + + const result = await listDraftsService(ids.userId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveLength(2); + expect(prisma.postDraft.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { userId: ids.userId }, + orderBy: { updatedAt: "desc" }, + }) + ); + } + }); + + it("여러 draft의 perfumeIds를 한 번에 batch 조회해야 한다", async () => { + const { ids, mockDraftList, mockPerfumeRecord } = getTestData(); + + vi.mocked(prisma.postDraft.findMany).mockResolvedValue(mockDraftList() as never); + vi.mocked(prisma.perfume.findMany).mockResolvedValue([ + mockPerfumeRecord(ids.perfumeId1), + mockPerfumeRecord(ids.perfumeId2), + ] as never); + + await listDraftsService(ids.userId); + + // 두 draft의 perfumeIds를 한 번의 쿼리로 조회 + expect(prisma.perfume.findMany).toHaveBeenCalledOnce(); + expect(prisma.perfume.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { + id: { + in: expect.arrayContaining([ids.perfumeId1, ids.perfumeId2]), + }, + }, + }) + ); + }); + + it("임시 저장이 없으면 빈 배열을 반환해야 한다", async () => { + const { ids } = getTestData(); + + vi.mocked(prisma.postDraft.findMany).mockResolvedValue([] as never); + + const result = await listDraftsService(ids.userId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveLength(0); + // perfume 조회가 호출되지 않아야 함 + expect(prisma.perfume.findMany).not.toHaveBeenCalled(); + } + }); + + it("perfumeIds가 없는 draft는 빈 perfumes 배열을 반환해야 한다", async () => { + const { ids, mockDraft } = getTestData(); + const draftWithNoPerfumes = { ...mockDraft(), perfumeIds: [] }; + + vi.mocked(prisma.postDraft.findMany).mockResolvedValue([draftWithNoPerfumes] as never); + + const result = await listDraftsService(ids.userId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data[0].perfumes).toHaveLength(0); + // perfumeIds가 없으므로 perfume 조회가 호출되지 않아야 함 + expect(prisma.perfume.findMany).not.toHaveBeenCalled(); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + const { ids } = getTestData(); + + vi.mocked(prisma.postDraft.findMany).mockRejectedValue(new Error("Database error")); + + const result = await listDraftsService(ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + }); + + describe("deleteDraftService", () => { + it("소유한 임시 저장을 삭제해야 한다", async () => { + const { ids, mockDraft } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(mockDraft() as never); + vi.mocked(prisma.postDraft.delete).mockResolvedValue({} as never); + + const result = await deleteDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.message).toContain("삭제"); + } + expect(prisma.postDraft.delete).toHaveBeenCalledWith({ + where: { id: ids.draftId }, + }); + }); + + it("존재하지 않는 draft 삭제 시 NOT_FOUND를 반환해야 한다", async () => { + const { ids } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(null); + + const result = await deleteDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("다른 사용자의 draft 삭제 시 FORBIDDEN을 반환해야 한다", async () => { + const { ids, mockDraft } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockResolvedValue({ + ...mockDraft(), + userId: "other-user-id", + } as never); + + const result = await deleteDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("FORBIDDEN"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + const { ids } = getTestData(); + + vi.mocked(prisma.postDraft.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await deleteDraftService(ids.draftId, ids.userId); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); + }); +}); diff --git a/src/server/hono/services/__tests__/earnPoints.service.test.ts b/src/server/hono/services/__tests__/earnPoints.service.test.ts index ecec22cb..46fdb090 100644 --- a/src/server/hono/services/__tests__/earnPoints.service.test.ts +++ b/src/server/hono/services/__tests__/earnPoints.service.test.ts @@ -49,96 +49,59 @@ describe("earnPointsService", () => { vi.clearAllMocks(); }); - describe("정상 포인트 적립", () => { - it("게시물 작성 시 5포인트가 적립되어야 한다", async () => { - (prisma.user.findUnique as ReturnType).mockResolvedValue({ - id: TEST_USER_ID, - }); - - (prisma.$transaction as ReturnType).mockImplementation( - async (callback) => { - await callback({ - pointHistory: { create: vi.fn() }, - user: { - update: vi.fn().mockResolvedValue({ totalPoints: 105 }), - }, - }); - return { totalPoints: 105 }; + describe("활동 타입별 포인트 금액 검증", () => { + const testCases = [ + { + activityType: PointActivityType.CREATE_POST, + expectedPoints: 5, + referenceId: "post-123", + label: "게시물 작성", + }, + { + activityType: PointActivityType.CREATE_COMMENT, + expectedPoints: 1, + referenceId: "comment-456", + label: "댓글 작성", + }, + { + activityType: PointActivityType.LIKE_POST, + expectedPoints: 1, + referenceId: "post-789", + label: "좋아요", + }, + ]; + + it.each(testCases)( + "$label 시 $expectedPoints포인트가 적립되어야 한다", + async ({ activityType, expectedPoints, referenceId }) => { + const initialPoints = 100; + const expectedTotal = initialPoints + expectedPoints; + + (prisma.user.findUnique as ReturnType).mockResolvedValue({ + id: TEST_USER_ID, + }); + + (prisma.$transaction as ReturnType).mockImplementation( + async (callback) => { + await callback({ + pointHistory: { create: vi.fn() }, + user: { + update: vi.fn().mockResolvedValue({ totalPoints: expectedTotal }), + }, + }); + return { totalPoints: expectedTotal }; + } + ); + + const result = await earnPointsService(TEST_USER_ID, activityType, referenceId); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.pointAmount).toBe(expectedPoints); + expect(result.data.totalPoints).toBe(expectedTotal); } - ); - - const result = await earnPointsService( - TEST_USER_ID, - PointActivityType.CREATE_POST, - "post-123" - ); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.pointAmount).toBe(5); - expect(result.data.totalPoints).toBe(105); } - }); - - it("댓글 작성 시 1포인트가 적립되어야 한다", async () => { - (prisma.user.findUnique as ReturnType).mockResolvedValue({ - id: TEST_USER_ID, - }); - - (prisma.$transaction as ReturnType).mockImplementation( - async (callback) => { - await callback({ - pointHistory: { create: vi.fn() }, - user: { - update: vi.fn().mockResolvedValue({ totalPoints: 101 }), - }, - }); - return { totalPoints: 101 }; - } - ); - - const result = await earnPointsService( - TEST_USER_ID, - PointActivityType.CREATE_COMMENT, - "comment-456" - ); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.pointAmount).toBe(1); - expect(result.data.totalPoints).toBe(101); - } - }); - - it("좋아요 시 1포인트가 적립되어야 한다", async () => { - (prisma.user.findUnique as ReturnType).mockResolvedValue({ - id: TEST_USER_ID, - }); - - (prisma.$transaction as ReturnType).mockImplementation( - async (callback) => { - await callback({ - pointHistory: { create: vi.fn() }, - user: { - update: vi.fn().mockResolvedValue({ totalPoints: 101 }), - }, - }); - return { totalPoints: 101 }; - } - ); - - const result = await earnPointsService( - TEST_USER_ID, - PointActivityType.LIKE_POST, - "post-789" - ); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.pointAmount).toBe(1); - expect(result.data.totalPoints).toBe(101); - } - }); + ); }); describe("커스텀 description", () => { diff --git a/src/server/hono/services/__tests__/helpers/draft.test.helpers.ts b/src/server/hono/services/__tests__/helpers/draft.test.helpers.ts new file mode 100644 index 00000000..db92b23f --- /dev/null +++ b/src/server/hono/services/__tests__/helpers/draft.test.helpers.ts @@ -0,0 +1,115 @@ +import { DraftType, PostCategory } from "@prisma/client"; + +export const getTestData = () => { + const TEST_USER_ID = "123e4567-e89b-12d3-a456-426614174000"; + const TEST_POST_ID = "223e4567-e89b-12d3-a456-426614174001"; + const TEST_DRAFT_ID = "323e4567-e89b-12d3-a456-426614174002"; + const TEST_PERFUME_ID_1 = "423e4567-e89b-12d3-a456-426614174003"; + const TEST_PERFUME_ID_2 = "523e4567-e89b-12d3-a456-426614174004"; + + return { + ids: { + userId: TEST_USER_ID, + postId: TEST_POST_ID, + draftId: TEST_DRAFT_ID, + perfumeId1: TEST_PERFUME_ID_1, + perfumeId2: TEST_PERFUME_ID_2, + }, + + createDraftPayload: () => ({ + userId: TEST_USER_ID, + type: DraftType.CREATE, + title: "테스트 제목", + content: "테스트 내용", + contentText: "테스트 내용 텍스트", + category: PostCategory.QUESTION, + thumbnailUrl: null, + perfumeIds: [TEST_PERFUME_ID_1], + }), + + updateDraftPayload: () => ({ + userId: TEST_USER_ID, + type: DraftType.UPDATE, + postId: TEST_POST_ID, + title: "수정 제목", + content: "수정 내용", + contentText: "수정 내용 텍스트", + category: PostCategory.QUESTION, + thumbnailUrl: null, + perfumeIds: [TEST_PERFUME_ID_1, TEST_PERFUME_ID_2], + }), + + mockDraft: () => ({ + id: TEST_DRAFT_ID, + userId: TEST_USER_ID, + type: DraftType.CREATE, + title: "테스트 제목", + content: "테스트 내용", + contentText: "테스트 내용 텍스트", + category: PostCategory.QUESTION, + thumbnailUrl: null, + perfumeIds: [TEST_PERFUME_ID_1], + postId: null, + createdAt: new Date("2025-01-15T10:00:00.000Z"), + updatedAt: new Date("2025-01-15T10:00:00.000Z"), + }), + + mockUpdateDraft: () => ({ + id: TEST_DRAFT_ID, + userId: TEST_USER_ID, + type: DraftType.UPDATE, + title: "수정 제목", + content: "수정 내용", + contentText: "수정 내용 텍스트", + category: PostCategory.QUESTION, + thumbnailUrl: null, + perfumeIds: [TEST_PERFUME_ID_1, TEST_PERFUME_ID_2], + postId: TEST_POST_ID, + createdAt: new Date("2025-01-15T10:00:00.000Z"), + updatedAt: new Date("2025-01-15T11:00:00.000Z"), + }), + + mockPerfumeRecord: (id: string = TEST_PERFUME_ID_1) => ({ + id, + nameEn: "Test Perfume EN", + nameKo: "테스트 향수", + brand: { + nameEn: "Test Brand EN", + nameKo: "테스트 브랜드", + brandUrl: "https://brand.test", + }, + perfumeImage: [{ imageUrl: "https://perfume.test/image.jpg" }], + }), + + mockDraftList: () => [ + { + id: TEST_DRAFT_ID, + userId: TEST_USER_ID, + type: DraftType.CREATE, + title: "임시 저장 1", + content: "내용 1", + contentText: "내용 텍스트 1", + category: PostCategory.QUESTION, + thumbnailUrl: null, + perfumeIds: [TEST_PERFUME_ID_1], + postId: null, + createdAt: new Date("2025-01-15T11:00:00.000Z"), + updatedAt: new Date("2025-01-15T11:00:00.000Z"), + }, + { + id: "draft-other-id", + userId: TEST_USER_ID, + type: DraftType.UPDATE, + title: "임시 저장 2", + content: "내용 2", + contentText: "내용 텍스트 2", + category: PostCategory.FREEBOARD, + thumbnailUrl: null, + perfumeIds: [TEST_PERFUME_ID_2], + postId: TEST_POST_ID, + createdAt: new Date("2025-01-14T10:00:00.000Z"), + updatedAt: new Date("2025-01-14T10:00:00.000Z"), + }, + ], + }; +}; diff --git a/src/server/hono/services/__tests__/point.service.test.ts b/src/server/hono/services/__tests__/point.service.test.ts index aad9de7c..cf4f4ff7 100644 --- a/src/server/hono/services/__tests__/point.service.test.ts +++ b/src/server/hono/services/__tests__/point.service.test.ts @@ -1,5 +1,26 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { validateTimestamp } from "../point.service"; +import { PointActivityType } from "@prisma/client"; +import { prisma } from "@/server/prisma"; +import { + validateTimestamp, + getPointHistoryService, + getUserPointsService, + getPointStatisticsService, +} from "../point.service"; + +// Mock prisma (validateTimestamp는 순수 함수라 영향 없음) +vi.mock("@/server/prisma", () => ({ + prisma: { + user: { + findUnique: vi.fn(), + }, + pointHistory: { + findMany: vi.fn(), + count: vi.fn(), + groupBy: vi.fn(), + }, + }, +})); /** * validateTimestamp 함수 테스트 @@ -110,3 +131,261 @@ describe("Point Service - validateTimestamp", () => { }); }); }); + +describe("getPointHistoryService", () => { + const TEST_USER_ID = "test-user-123"; + + const makeHistoryItem = (id: string) => ({ + id, + userId: TEST_USER_ID, + pointAmount: 5, + activityType: PointActivityType.CREATE_POST, + referenceId: null, + description: "게시물 작성", + createdAt: new Date(), + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("포인트 이력을 페이지네이션하여 반환해야 한다", async () => { + const items = Array.from({ length: 5 }, (_, i) => makeHistoryItem(`history-${i}`)); + + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.findMany).mockResolvedValue(items as never); + vi.mocked(prisma.pointHistory.count).mockResolvedValue(5); + + const result = await getPointHistoryService(TEST_USER_ID, { limit: 10 }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.histories).toHaveLength(5); + expect(result.data.totalCount).toBe(5); + } + }); + + it("limit+1 조회로 다음 페이지 여부를 확인해야 한다 (nextCursor 있음)", async () => { + // limit=5 요청 → 6개 반환 → hasNextPage=true + const items = Array.from({ length: 6 }, (_, i) => makeHistoryItem(`history-${i}`)); + + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.findMany).mockResolvedValue(items as never); + vi.mocked(prisma.pointHistory.count).mockResolvedValue(10); + + const result = await getPointHistoryService(TEST_USER_ID, { limit: 5 }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.histories).toHaveLength(5); // 마지막 1개 제거 + expect(result.data.nextCursor).toBe("history-4"); // 5번째 항목 id + } + expect(prisma.pointHistory.findMany).toHaveBeenCalledWith( + expect.objectContaining({ take: 6 }) // limit + 1 + ); + }); + + it("마지막 페이지에서는 nextCursor가 null이어야 한다", async () => { + const items = Array.from({ length: 3 }, (_, i) => makeHistoryItem(`history-${i}`)); + + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.findMany).mockResolvedValue(items as never); + vi.mocked(prisma.pointHistory.count).mockResolvedValue(3); + + const result = await getPointHistoryService(TEST_USER_ID, { limit: 5 }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.nextCursor).toBeNull(); + } + }); + + it("cursor가 있으면 해당 id 미만의 이력을 조회해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.findMany).mockResolvedValue([] as never); + vi.mocked(prisma.pointHistory.count).mockResolvedValue(0); + + await getPointHistoryService(TEST_USER_ID, { cursor: "cursor-id" }); + + expect(prisma.pointHistory.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + id: { lt: "cursor-id" }, + }), + }) + ); + }); + + it("activityType 필터가 적용되어야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.findMany).mockResolvedValue([] as never); + vi.mocked(prisma.pointHistory.count).mockResolvedValue(0); + + await getPointHistoryService(TEST_USER_ID, { + activityType: PointActivityType.CREATE_POST, + }); + + expect(prisma.pointHistory.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + activityType: PointActivityType.CREATE_POST, + }), + }) + ); + }); + + it("존재하지 않는 사용자는 NOT_FOUND를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue(null); + + const result = await getPointHistoryService(TEST_USER_ID); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await getPointHistoryService(TEST_USER_ID); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); +}); + +describe("getUserPointsService", () => { + const TEST_USER_ID = "test-user-123"; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("totalPoints와 consecutiveLoginDays를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ + totalPoints: 350, + consecutiveLoginDays: 5, + } as never); + + const result = await getUserPointsService(TEST_USER_ID); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.totalPoints).toBe(350); + expect(result.data.consecutiveLoginDays).toBe(5); + } + }); + + it("존재하지 않는 사용자는 NOT_FOUND를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue(null); + + const result = await getUserPointsService(TEST_USER_ID); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await getUserPointsService(TEST_USER_ID); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); +}); + +describe("getPointStatisticsService", () => { + const TEST_USER_ID = "test-user-123"; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("활동 타입별 포인트 통계를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.groupBy).mockResolvedValue([ + { + activityType: PointActivityType.CREATE_POST, + _sum: { pointAmount: 15 }, + _count: { id: 3 }, + }, + { + activityType: PointActivityType.CREATE_COMMENT, + _sum: { pointAmount: 5 }, + _count: { id: 5 }, + }, + ] as never); + + const result = await getPointStatisticsService(TEST_USER_ID); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.statistics).toHaveLength(2); + expect(result.data.statistics[0]).toEqual({ + activityType: PointActivityType.CREATE_POST, + totalPoints: 15, + count: 3, + }); + } + }); + + it("_sum.pointAmount가 null이면 0으로 처리해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.groupBy).mockResolvedValue([ + { + activityType: PointActivityType.CREATE_COMMENT, + _sum: { pointAmount: null }, + _count: { id: 1 }, + }, + ] as never); + + const result = await getPointStatisticsService(TEST_USER_ID); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.statistics[0].totalPoints).toBe(0); + } + }); + + it("포인트 이력이 없으면 빈 statistics 배열을 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: TEST_USER_ID } as never); + vi.mocked(prisma.pointHistory.groupBy).mockResolvedValue([] as never); + + const result = await getPointStatisticsService(TEST_USER_ID); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.statistics).toHaveLength(0); + } + }); + + it("존재하지 않는 사용자는 NOT_FOUND를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockResolvedValue(null); + + const result = await getPointStatisticsService(TEST_USER_ID); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("NOT_FOUND"); + } + }); + + it("DB 에러 시 INTERNAL_ERROR를 반환해야 한다", async () => { + vi.mocked(prisma.user.findUnique).mockRejectedValue(new Error("Database error")); + + const result = await getPointStatisticsService(TEST_USER_ID); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toBe("INTERNAL_ERROR"); + } + }); +}); From e6a4f821a86eeb33e53ed8f163c551fba6170812 Mon Sep 17 00:00:00 2001 From: kuan Date: Tue, 31 Mar 2026 00:25:25 +0900 Subject: [PATCH 11/39] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20ckeditor?= =?UTF-8?q?=20->=20tiptap=EC=9C=BC=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 13 +- src/app/community/post/(write)/layout.tsx | 3 - src/app/community/post/[id]/edit/layout.tsx | 3 - .../ckeditor/localPreviewUploadPlugin.ts | 37 - src/client/ckeditor/supabaseUploadPlugin.ts | 35 - .../blobRegistry.ts} | 31 +- .../commons/ckeditor5/CkEditor5.tsx | 207 -- .../commons/ckeditor5/ck-content.css | 21 - .../commons/ckeditor5/ckeditor5.css | 40 - .../{ckeditor5 => tiptap}/EditorLoading.tsx | 2 +- .../commons/tiptap/TiptapEditor.tsx | 161 ++ .../commons/tiptap/TiptapToolbar.tsx | 235 ++ .../commons/tiptap/tiptap-content.css | 106 + .../domains/post/form/PostEditor.tsx | 10 +- src/components/domains/post/form/index.tsx | 8 +- .../domains/postDetail/content/index.tsx | 4 +- yarn.lock | 2004 ++++++----------- 17 files changed, 1203 insertions(+), 1717 deletions(-) delete mode 100644 src/client/ckeditor/localPreviewUploadPlugin.ts delete mode 100644 src/client/ckeditor/supabaseUploadPlugin.ts rename src/client/{ckeditor/finalizeWithBlobRegistry.ts => tiptap/blobRegistry.ts} (55%) delete mode 100644 src/components/commons/ckeditor5/CkEditor5.tsx delete mode 100644 src/components/commons/ckeditor5/ck-content.css delete mode 100644 src/components/commons/ckeditor5/ckeditor5.css rename src/components/commons/{ckeditor5 => tiptap}/EditorLoading.tsx (73%) create mode 100644 src/components/commons/tiptap/TiptapEditor.tsx create mode 100644 src/components/commons/tiptap/TiptapToolbar.tsx create mode 100644 src/components/commons/tiptap/tiptap-content.css diff --git a/package.json b/package.json index 0a2d1056..0b20c572 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,22 @@ }, "dependencies": { "@auth/supabase-adapter": "^1.8.0", - "@ckeditor/ckeditor5-react": "^9.5.0", "@hono/zod-openapi": "^0.19.9", "@hono/zod-validator": "^0.4.3", "@hookform/resolvers": "^5.1.1", "@prisma/client": "6.19.0", "@supabase/supabase-js": "^2.46.2", "@tanstack/react-query": "^5.87.4", + "@tiptap/core": "^2.11.7", + "@tiptap/extension-image": "^2.11.7", + "@tiptap/extension-link": "^2.11.7", + "@tiptap/extension-text-align": "^2.11.7", + "@tiptap/extension-underline": "^2.11.7", + "@tiptap/pm": "^2.11.7", + "@tiptap/react": "^2.11.7", + "@tiptap/starter-kit": "^2.11.7", "chart.js": "^4.4.9", "chartjs-plugin-datalabels": "^2.2.0", - "ckeditor5": "^45.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -42,7 +48,6 @@ "react-dom": "18", "react-dropzone": "^14.3.8", "react-hook-form": "^7.53.2", - "react-naver-maps": "^0.1.4", "sanitize-html": "^2.17.2", "sharp": "^0.33.5", "swiper": "^11.2.7", @@ -71,7 +76,6 @@ "@types/react-dom": "^18", "@types/sanitize-html": "^2.16.1", "@types/swagger-ui-react": "^5.18.0", - "swagger-ui-react": "^5.25.3", "@vitejs/plugin-react": "^5.1.1", "@vitest/ui": "^4.0.9", "cross-env": "^10.1.0", @@ -82,6 +86,7 @@ "postcss": "^8", "prisma": "6.19.0", "storybook": "^8.5.8", + "swagger-ui-react": "^5.25.3", "tailwindcss": "^3.4.1", "ts-node": "^10.9.2", "typescript": "^5", diff --git a/src/app/community/post/(write)/layout.tsx b/src/app/community/post/(write)/layout.tsx index ab4a17d5..790272de 100644 --- a/src/app/community/post/(write)/layout.tsx +++ b/src/app/community/post/(write)/layout.tsx @@ -1,6 +1,3 @@ -import "ckeditor5/ckeditor5.css"; -import "@/components/commons/ckeditor5/ckeditor5.css"; - export default function Layout({ children }: { children: React.ReactNode }) { return <>{children}; } diff --git a/src/app/community/post/[id]/edit/layout.tsx b/src/app/community/post/[id]/edit/layout.tsx index ab4a17d5..790272de 100644 --- a/src/app/community/post/[id]/edit/layout.tsx +++ b/src/app/community/post/[id]/edit/layout.tsx @@ -1,6 +1,3 @@ -import "ckeditor5/ckeditor5.css"; -import "@/components/commons/ckeditor5/ckeditor5.css"; - export default function Layout({ children }: { children: React.ReactNode }) { return <>{children}; } diff --git a/src/client/ckeditor/localPreviewUploadPlugin.ts b/src/client/ckeditor/localPreviewUploadPlugin.ts deleted file mode 100644 index 43955874..00000000 --- a/src/client/ckeditor/localPreviewUploadPlugin.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Editor, FileLoader } from "ckeditor5"; - -export type BlobRegistry = Map; - -const MAX_FILE_SIZE = 5 * 1024 * 1024; - -class LocalPreviewAdapter { - private loader: FileLoader; - private registry: BlobRegistry; - - constructor(loader: FileLoader, registry: BlobRegistry) { - this.loader = loader; - this.registry = registry; - } - - async upload() { - const file = await this.loader.file; - if (!file) throw new Error("파일이 존재하지 않습니다."); - if (file.size > MAX_FILE_SIZE) { - return Promise.reject("5MB 이하 이미지 파일만 업로드할 수 있습니다."); - } - - const objectURL = URL.createObjectURL(file); - this.registry.set(objectURL, file); - return { default: objectURL }; - } - - abort() {} -} - -export default function makeLocalPreviewUploadPlugin(registry: BlobRegistry) { - return function localPreviewUploadPlugin(editor: Editor) { - editor.plugins.get("FileRepository").createUploadAdapter = ( - loader: FileLoader - ) => new LocalPreviewAdapter(loader, registry); - }; -} diff --git a/src/client/ckeditor/supabaseUploadPlugin.ts b/src/client/ckeditor/supabaseUploadPlugin.ts deleted file mode 100644 index be598deb..00000000 --- a/src/client/ckeditor/supabaseUploadPlugin.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Editor, FileLoader } from "ckeditor5"; -import { fileApi } from "@/client/utils/api/files.api"; -import { POST_IMAGES_BUCKET_NAME } from "@/shared/constants/buckets"; - -class SupabaseUploadAdapter { - loader: FileLoader; - constructor(loader: FileLoader) { - this.loader = loader; - } - async upload() { - const file = await this.loader.file; - if (!file) throw new Error("파일이 존재하지 않습니다."); - - try { - const result = await fileApi.upload(file, POST_IMAGES_BUCKET_NAME); - if (!result.success || !result.data.imageUrl) { - return Promise.reject("이미지 업로드에 실패했습니다."); - } - return { default: result.data.imageUrl }; - } catch (error) { - const message = - error instanceof Error - ? error.message - : "이미지 업로드에 실패했습니다."; - return Promise.reject(message); - } - } - abort() {} -} - -export default function supabaseUploadPlugin(editor: Editor) { - editor.plugins.get("FileRepository").createUploadAdapter = (loader) => { - return new SupabaseUploadAdapter(loader); - }; -} diff --git a/src/client/ckeditor/finalizeWithBlobRegistry.ts b/src/client/tiptap/blobRegistry.ts similarity index 55% rename from src/client/ckeditor/finalizeWithBlobRegistry.ts rename to src/client/tiptap/blobRegistry.ts index 3d99e1e6..5fda319b 100644 --- a/src/client/ckeditor/finalizeWithBlobRegistry.ts +++ b/src/client/tiptap/blobRegistry.ts @@ -1,23 +1,40 @@ -import type { BlobRegistry } from "@/client/ckeditor/localPreviewUploadPlugin"; import { fileApi } from "@/client/utils/api/files.api"; import { POST_IMAGES_BUCKET_NAME } from "@/shared/constants/buckets"; -export async function finalizeWithBlobRegistry( +export type BlobRegistry = Map; + +export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; + +export function createBlobRegistry(): BlobRegistry { + return new Map(); +} + +export function registerBlob(registry: BlobRegistry, file: File): string { + const objectURL = URL.createObjectURL(file); + registry.set(objectURL, file); + return objectURL; +} + +export function clearBlobRegistry(registry: BlobRegistry): void { + for (const url of registry.keys()) { + URL.revokeObjectURL(url); + } + registry.clear(); +} + +export async function finalizeContentWithBlobUpload( html: string, registry: BlobRegistry -) { +): Promise { const doc = new DOMParser().parseFromString(html, "text/html"); const imgs = Array.from(doc.querySelectorAll("img[src^='blob:']")); for (const img of imgs) { const src = img.getAttribute("src")!; const file = registry.get(src); - if (!file) { - continue; - } + if (!file) continue; const result = await fileApi.upload(file, POST_IMAGES_BUCKET_NAME); - if (!result.success || !result.data.imageUrl) { throw new Error("이미지 업로드에 실패했습니다."); } diff --git a/src/components/commons/ckeditor5/CkEditor5.tsx b/src/components/commons/ckeditor5/CkEditor5.tsx deleted file mode 100644 index 02f0b0e3..00000000 --- a/src/components/commons/ckeditor5/CkEditor5.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { useState, useEffect, useRef, useMemo, MutableRefObject } from "react"; -import { CKEditor } from "@ckeditor/ckeditor5-react"; -import { - ClassicEditor, - Alignment, - AutoImage, - Bold, - Essentials, - Heading, - ImageBlock, - ImageCaption, - ImageEditing, - ImageInline, - ImageInsertViaUrl, - ImageResize, - ImageStyle, - ImageTextAlternative, - ImageToolbar, - ImageUpload, - Indent, - IndentBlock, - Italic, - Link, - Paragraph, - Strikethrough, - Underline, -} from "ckeditor5"; - -import translations from "ckeditor5/translations/ko.js"; -import makeLocalPreviewUploadPlugin, { - BlobRegistry, -} from "@/client/ckeditor/localPreviewUploadPlugin"; - -const LICENSE_KEY = "GPL"; - -export default function CkEditor5({ - content, - onChange, - blobRegistryRef, -}: { - content?: string; - onChange: (newContent: string) => void; - blobRegistryRef: MutableRefObject; -}) { - const editorContainerRef = useRef(null); - const editorRef = useRef(null); - const [isLayoutReady, setIsLayoutReady] = useState(false); - - const localPreviewPlugin = useMemo( - () => makeLocalPreviewUploadPlugin(blobRegistryRef.current), - [blobRegistryRef], - ); - - useEffect(() => { - setIsLayoutReady(true); - const blobRegistry = blobRegistryRef.current; - - return () => { - for (const url of blobRegistry.keys()) { - URL.revokeObjectURL(url); - } - blobRegistry.clear(); - setIsLayoutReady(false); - }; - }, [blobRegistryRef]); - - const { editorConfig } = useMemo(() => { - if (!isLayoutReady) { - return {}; - } - - return { - editorConfig: { - toolbar: { - items: [ - "undo", - "redo", - "|", - "heading", - "|", - "bold", - "italic", - "underline", - "strikethrough", - "|", - "imageUpload", - "link", - "|", - "alignment", - "|", - "outdent", - "indent", - ], - shouldNotGroupWhenFull: false, - }, - plugins: [ - Alignment, - AutoImage, - Bold, - Essentials, - Heading, - ImageBlock, - ImageCaption, - ImageEditing, - ImageInline, - ImageInsertViaUrl, - ImageResize, - ImageStyle, - ImageTextAlternative, - ImageToolbar, - ImageUpload, - Indent, - IndentBlock, - Italic, - Link, - Paragraph, - Strikethrough, - Underline, - localPreviewPlugin, - ], - heading: { - options: [ - { - model: "paragraph" as const, - title: "Paragraph", - class: "ck-heading_paragraph", - }, - { - model: "heading1" as const, - view: "h1", - title: "Heading 1", - class: "ck-heading_heading1", - }, - { - model: "heading2" as const, - view: "h2", - title: "Heading 2", - class: "ck-heading_heading2", - }, - { - model: "heading3" as const, - view: "h3", - title: "Heading 3", - class: "ck-heading_heading3", - }, - ], - }, - image: { - toolbar: [ - "toggleImageCaption", - "imageTextAlternative", - "|", - "imageStyle:inline", - "imageStyle:wrapText", - "imageStyle:breakText", - "|", - "resizeImage", - ], - }, - initialData: content, - language: "ko", - licenseKey: LICENSE_KEY, - link: { - addTargetToExternalLinks: true, - defaultProtocol: "https://", - decorators: { - toggleDownloadable: { - mode: "manual" as const, - label: "Downloadable", - attributes: { - download: "file", - }, - }, - }, - }, - - placeholder: "글 내용을 작성해 주세요.", - translations: [translations], - }, - }; - }, [isLayoutReady, content, localPreviewPlugin]); - - return ( -
-
-
-
- {editorConfig && ( - { - if (!editor) return; - const data = editor.getData?.() ?? ""; - onChange(data); - }} - /> - )} -
-
-
-
- ); -} diff --git a/src/components/commons/ckeditor5/ck-content.css b/src/components/commons/ckeditor5/ck-content.css deleted file mode 100644 index 8e15a586..00000000 --- a/src/components/commons/ckeditor5/ck-content.css +++ /dev/null @@ -1,21 +0,0 @@ -.ck-content { - line-height: 1.6; - word-break: break-word; -} - -.ck-content h1 { - font-size: 20px; - font-weight: bold; - margin: 1.5em 0 1em; -} - -.ck-content h2 { - font-size: 18px; - font-weight: bold; - margin: 1.2em 0 0.8em; -} - -.ck-content h3 { - font-size: 16px; - font-weight: bold; -} diff --git a/src/components/commons/ckeditor5/ckeditor5.css b/src/components/commons/ckeditor5/ckeditor5.css deleted file mode 100644 index 86cf14cb..00000000 --- a/src/components/commons/ckeditor5/ckeditor5.css +++ /dev/null @@ -1,40 +0,0 @@ -@import "./ck-content.css"; - -:root { - --ck-color-focus-border: #a47764; - --ck-font-size-base: 14px; - --ck-border-radius: 0.5rem; - --ck-custom-border: #a8a8a8; -} - -.main-container { - width: 100%; - margin-left: auto; - margin-right: auto; -} -.editor-container_classic-editor .editor-container__editor { - width: 100%; - min-height: 640px; - border-radius: 8px; -} -.main-container .ck-editor__editable { - padding-left: 20px; - padding-right: 20px; -} - -.ck.ck-editor__editable > .ck-placeholder::before { - color: #a8a8a8; - font-size: 15px; -} -.editor-container_classic-editor .ck-content { - min-height: 600px; -} - -@media (max-width: 767px) { - .editor-container_classic-editor .ck-content { - min-height: 400px; - } - .editor-container_classic-editor .editor-container__editor { - min-height: 440px; - } -} diff --git a/src/components/commons/ckeditor5/EditorLoading.tsx b/src/components/commons/tiptap/EditorLoading.tsx similarity index 73% rename from src/components/commons/ckeditor5/EditorLoading.tsx rename to src/components/commons/tiptap/EditorLoading.tsx index fc5c9414..bfa5b519 100644 --- a/src/components/commons/ckeditor5/EditorLoading.tsx +++ b/src/components/commons/tiptap/EditorLoading.tsx @@ -1,4 +1,4 @@ -export default function EditorPlaceholder() { +export default function EditorLoading() { return (
); diff --git a/src/components/commons/tiptap/TiptapEditor.tsx b/src/components/commons/tiptap/TiptapEditor.tsx new file mode 100644 index 00000000..5710943c --- /dev/null +++ b/src/components/commons/tiptap/TiptapEditor.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useEffect, useRef, MutableRefObject, useCallback } from "react"; +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Underline from "@tiptap/extension-underline"; +import Link from "@tiptap/extension-link"; +import TextAlign from "@tiptap/extension-text-align"; +import Image from "@tiptap/extension-image"; + +import { + BlobRegistry, + MAX_IMAGE_SIZE, + registerBlob, + clearBlobRegistry, +} from "@/client/tiptap/blobRegistry"; +import TiptapToolbar from "./TiptapToolbar"; + +interface TiptapEditorProps { + content?: string; + onChange: (html: string) => void; + blobRegistryRef: MutableRefObject; +} + +export default function TiptapEditor({ + content, + onChange, + blobRegistryRef, +}: TiptapEditorProps) { + const fileInputRef = useRef(null); + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + heading: { levels: [1, 2, 3] }, + }), + Underline, + Link.configure({ + openOnClick: false, + defaultProtocol: "https", + HTMLAttributes: { target: "_blank", rel: "noopener noreferrer" }, + }), + TextAlign.configure({ types: ["heading", "paragraph"] }), + Image.configure({ allowBase64: false }), + ], + content: content ?? "", + editorProps: { + attributes: { + class: + "prose prose-sm max-w-none min-h-[400px] tablet:min-h-[600px] outline-none px-4 py-3", + "data-placeholder": "글 내용을 작성해 주세요.", + }, + handlePaste(view, event) { + const items = event.clipboardData?.items; + if (!items) return false; + + for (const item of Array.from(items)) { + if (item.type.startsWith("image/")) { + const file = item.getAsFile(); + if (!file) continue; + + if (file.size > MAX_IMAGE_SIZE) { + alert("5MB 이하 이미지 파일만 업로드할 수 있습니다."); + return true; + } + + const src = registerBlob(blobRegistryRef.current, file); + view.dispatch( + view.state.tr.replaceSelectionWith( + view.state.schema.nodes.image.create({ src }) + ) + ); + return true; + } + } + return false; + }, + handleDrop(view, event) { + const files = event.dataTransfer?.files; + if (!files?.length) return false; + + const file = files[0]; + if (!file.type.startsWith("image/")) return false; + + if (file.size > MAX_IMAGE_SIZE) { + alert("5MB 이하 이미지 파일만 업로드할 수 있습니다."); + return true; + } + + const src = registerBlob(blobRegistryRef.current, file); + const { schema } = view.state; + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + if (!coordinates) return false; + + const node = schema.nodes.image.create({ src }); + const transaction = view.state.tr.insert(coordinates.pos, node); + view.dispatch(transaction); + return true; + }, + }, + onUpdate({ editor }) { + onChange(editor.getHTML()); + }, + immediatelyRender: false, + }); + + // 외부에서 content가 변경될 때 (예: draft 로드) 에디터 내용 동기화 + useEffect(() => { + if (!editor || content === undefined) return; + const current = editor.getHTML(); + if (current !== content) { + editor.commands.setContent(content, false); + } + }, [editor, content]); + + // 언마운트 시 blob URL 정리 + useEffect(() => { + const registry = blobRegistryRef.current; + return () => { + clearBlobRegistry(registry); + }; + }, [blobRegistryRef]); + + const handleImageUploadClick = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !editor) return; + + if (file.size > MAX_IMAGE_SIZE) { + alert("5MB 이하 이미지 파일만 업로드할 수 있습니다."); + return; + } + + const src = registerBlob(blobRegistryRef.current, file); + editor.chain().focus().setImage({ src }).run(); + e.target.value = ""; + }, + [editor, blobRegistryRef] + ); + + return ( +
+ + + +
+ ); +} diff --git a/src/components/commons/tiptap/TiptapToolbar.tsx b/src/components/commons/tiptap/TiptapToolbar.tsx new file mode 100644 index 00000000..4608e57e --- /dev/null +++ b/src/components/commons/tiptap/TiptapToolbar.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { Editor } from "@tiptap/react"; +import { + Bold, + Italic, + Underline, + Strikethrough, + Heading1, + Heading2, + Heading3, + Image, + Link, + AlignLeft, + AlignCenter, + AlignRight, + AlignJustify, + Indent, + Outdent, + Undo, + Redo, +} from "lucide-react"; +import { useCallback } from "react"; + +interface TiptapToolbarProps { + editor: Editor | null; + onImageUpload: () => void; +} + +interface ToolbarButtonProps { + onClick: () => void; + isActive?: boolean; + disabled?: boolean; + title: string; + children: React.ReactNode; +} + +function ToolbarButton({ + onClick, + isActive, + disabled, + title, + children, +}: ToolbarButtonProps) { + return ( + + ); +} + +function Divider() { + return
; +} + +export default function TiptapToolbar({ + editor, + onImageUpload, +}: TiptapToolbarProps) { + const handleSetLink = useCallback(() => { + if (!editor) return; + + const previousUrl = editor.getAttributes("link").href as string | undefined; + const url = window.prompt("링크 URL을 입력해 주세요.", previousUrl ?? "https://"); + + if (url === null) return; + + if (url === "") { + editor.chain().focus().extendMarkRange("link").unsetLink().run(); + return; + } + + editor + .chain() + .focus() + .extendMarkRange("link") + .setLink({ href: url }) + .run(); + }, [editor]); + + if (!editor) return null; + + const iconSize = 16; + + return ( +
+ {/* Undo / Redo */} + editor.chain().focus().undo().run()} + disabled={!editor.can().undo()} + title="실행 취소" + > + + + editor.chain().focus().redo().run()} + disabled={!editor.can().redo()} + title="다시 실행" + > + + + + + + {/* Headings */} + editor.chain().focus().toggleHeading({ level: 1 }).run()} + isActive={editor.isActive("heading", { level: 1 })} + title="제목 1" + > + + + editor.chain().focus().toggleHeading({ level: 2 }).run()} + isActive={editor.isActive("heading", { level: 2 })} + title="제목 2" + > + + + editor.chain().focus().toggleHeading({ level: 3 }).run()} + isActive={editor.isActive("heading", { level: 3 })} + title="제목 3" + > + + + + + + {/* Text formatting */} + editor.chain().focus().toggleBold().run()} + isActive={editor.isActive("bold")} + title="굵게" + > + + + editor.chain().focus().toggleItalic().run()} + isActive={editor.isActive("italic")} + title="기울임" + > + + + editor.chain().focus().toggleUnderline().run()} + isActive={editor.isActive("underline")} + title="밑줄" + > + + + editor.chain().focus().toggleStrike().run()} + isActive={editor.isActive("strike")} + title="취소선" + > + + + + + + {/* Image & Link */} + + + + + + + + + + {/* Alignment */} + editor.chain().focus().setTextAlign("left").run()} + isActive={editor.isActive({ textAlign: "left" })} + title="왼쪽 정렬" + > + + + editor.chain().focus().setTextAlign("center").run()} + isActive={editor.isActive({ textAlign: "center" })} + title="가운데 정렬" + > + + + editor.chain().focus().setTextAlign("right").run()} + isActive={editor.isActive({ textAlign: "right" })} + title="오른쪽 정렬" + > + + + editor.chain().focus().setTextAlign("justify").run()} + isActive={editor.isActive({ textAlign: "justify" })} + title="양쪽 정렬" + > + + + + + + {/* Indent */} + editor.chain().focus().liftListItem("listItem").run()} + disabled={!editor.can().liftListItem("listItem")} + title="내어쓰기" + > + + + editor.chain().focus().sinkListItem("listItem").run()} + disabled={!editor.can().sinkListItem("listItem")} + title="들여쓰기" + > + + +
+ ); +} diff --git a/src/components/commons/tiptap/tiptap-content.css b/src/components/commons/tiptap/tiptap-content.css new file mode 100644 index 00000000..c049f3a7 --- /dev/null +++ b/src/components/commons/tiptap/tiptap-content.css @@ -0,0 +1,106 @@ +.tiptap-content h1 { + font-size: 1.75rem; + font-weight: 700; + line-height: 1.3; + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +.tiptap-content h2 { + font-size: 1.375rem; + font-weight: 700; + line-height: 1.3; + margin-top: 1.25rem; + margin-bottom: 0.625rem; +} + +.tiptap-content h3 { + font-size: 1.125rem; + font-weight: 600; + line-height: 1.3; + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +.tiptap-content p { + margin-bottom: 0.75rem; +} + +.tiptap-content strong { + font-weight: 700; +} + +.tiptap-content em { + font-style: italic; +} + +.tiptap-content u { + text-decoration: underline; +} + +.tiptap-content s { + text-decoration: line-through; +} + +.tiptap-content a { + color: #2563eb; + text-decoration: underline; + word-break: break-all; +} + +.tiptap-content a:hover { + color: #1d4ed8; +} + +.tiptap-content img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + margin: 0.75rem 0; +} + +.tiptap-content ul { + list-style-type: disc; + padding-left: 1.5rem; + margin-bottom: 0.75rem; +} + +.tiptap-content ol { + list-style-type: decimal; + padding-left: 1.5rem; + margin-bottom: 0.75rem; +} + +.tiptap-content li { + margin-bottom: 0.25rem; +} + +.tiptap-content blockquote { + border-left: 3px solid #d1d5db; + padding-left: 1rem; + color: #6b7280; + margin: 1rem 0; +} + +.tiptap-content code { + background-color: #f3f4f6; + border-radius: 0.25rem; + padding: 0.125rem 0.375rem; + font-size: 0.875em; + font-family: ui-monospace, monospace; +} + +.tiptap-content pre { + background-color: #1f2937; + color: #f9fafb; + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; + margin: 1rem 0; +} + +.tiptap-content pre code { + background-color: transparent; + padding: 0; + color: inherit; +} diff --git a/src/components/domains/post/form/PostEditor.tsx b/src/components/domains/post/form/PostEditor.tsx index 7cbde205..25e405cf 100644 --- a/src/components/domains/post/form/PostEditor.tsx +++ b/src/components/domains/post/form/PostEditor.tsx @@ -1,12 +1,12 @@ import dynamic from "next/dynamic"; import SubTitleLabel from "./element/SubTitleLabel"; -import EditorLoading from "@/components/commons/ckeditor5/EditorLoading"; +import EditorLoading from "@/components/commons/tiptap/EditorLoading"; import { Controller, useFormContext } from "react-hook-form"; -import { BlobRegistry } from "@/client/ckeditor/localPreviewUploadPlugin"; +import { BlobRegistry } from "@/client/tiptap/blobRegistry"; import { MutableRefObject } from "react"; -const CkEditor5 = dynamic( - () => import("@/components/commons/ckeditor5/CkEditor5"), +const TiptapEditor = dynamic( + () => import("@/components/commons/tiptap/TiptapEditor"), { ssr: false, loading: () => } ); @@ -24,7 +24,7 @@ export default function PostEditor({ name="content" control={control} render={({ field }) => ( - (new Map()); + const blobRegistryRef = useRef(new Map()); const method = useForm({ resolver: zodResolver(CreatePostClientSchema), @@ -114,7 +114,7 @@ export default function PostForm({ }, [initialData, reset]); const onSubmit = async (data: CreatePostClientInput) => { - const finalizedContent = await finalizeWithBlobRegistry( + const finalizedContent = await finalizeContentWithBlobUpload( data.content, blobRegistryRef.current, ); diff --git a/src/components/domains/postDetail/content/index.tsx b/src/components/domains/postDetail/content/index.tsx index 44f306b9..5da011bf 100644 --- a/src/components/domains/postDetail/content/index.tsx +++ b/src/components/domains/postDetail/content/index.tsx @@ -1,6 +1,6 @@ "use client"; -import "@/components/commons/ckeditor5/ck-content.css"; +import "@/components/commons/tiptap/tiptap-content.css"; import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema"; import PostActions from "../header/PostActions"; import RelatedPerfume from "./RelatedPerfume"; @@ -24,7 +24,7 @@ export default function PostContent({
diff --git a/yarn.lock b/yarn.lock index bd959fcc..689e83f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2099,7 +2099,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.3.1": +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.3.1": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 @@ -2218,884 +2218,6 @@ __metadata: languageName: node linkType: hard -"@ckeditor/ckeditor5-adapter-ckfinder@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-adapter-ckfinder@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-upload": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/88acced34b722b222010ad927e541a642ddafa4b36e3b7a1f723818e5d301e277ee477feab1b6f7314cf7ac2971188ce57da823193579db8d0f65f0f5fe58f07 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-alignment@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-alignment@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/1faed6f68d590edd728dda98f964e726a3bfb9e0cca17219fa1d2215ab00d4d8a25d42e29f7be7bc538aca5fc4182f99b2e2c7924726b1d344d77f53ad488d5a - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-autoformat@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-autoformat@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-heading": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/ceec452bf2fb8de60742abc0c50d53b5231a8011e623930e222e34d25e5012653fc86f750b48d6c8130611eab19553e10419a8335ba65d9450d70550b43d3d71 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-autosave@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-autosave@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/12d6163e856fc6df978b1143feee92a61a7236d11ef177e8d884c6aef37a7ad38b13892dd077fa8f9ede55a610e9ca209e562abb76137c3f7075fd02fcef3589 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-basic-styles@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-basic-styles@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/64893c51dd79745cf8b31aeafb62db07754a89fb1b4daeef697d164092bd645fa291dc0b1865fb759a6908ceba479b6117749aacb8635b8f9a3e73153f294154 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-block-quote@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-block-quote@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/89e20e3bd4db9f4cef448df4572a7fe2416ce65b9a21d461356b9f940c797f8da42632dd9f937a9d6e48ee8bca4b9de1e64e079bd0ab13d50ef175beeef6c4de - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-bookmark@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-bookmark@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-link": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/7be3ec8e4ab577182a0c8c287fbef14d6e3f53f3e9a21d6e3d4d359e6bd6173f0d58444fe59ce26ccda4a1a0c07003a9f1eb5f2c0ab585e06cf736252096b79e - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-ckbox@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-ckbox@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-cloud-services": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-image": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-upload": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - blurhash: "npm:2.0.5" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/8f0a9261a35ad9461e0d95b9f6b2c10f185ea36daf533affd359a733374f5859c1d008efa65d41d749e29e3750f3aae323fac3b542b9dec653b27cb1a64c9f2e - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-ckfinder@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-ckfinder@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-image": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/15244ba46d4513c3b0c521289fad972efad3cdd63a6240ad92eba0c2cc166e9584fd7b2f1f6219e9a993901334e759533c68e8d7e131717728dc977d0355bbc0 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-clipboard@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-clipboard@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/f601b8a4cf856a01204520cbbb9f44c8eeb2d80592b1b1b9d9d7ac5379f3a78e23d6597e9e057562bdd4952fffec7690a35d61063359ad69fab7607e229d1e24 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-cloud-services@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-cloud-services@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/52676d9a813eabcfbcf81b1bc2130dcf2df76025bb11d9fd3d529dbb0f3dc2d371602056e9b71ff84e6f4abbce8a9bbf9953f1d861ff8d44876d4df02b10f550 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-code-block@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-code-block@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/56293da83080fe2ca471da0ef4e3d5b53a5f512a96a0a5af177f7d41bdbbc6160445169cb6294004aaf262c8676d33ba76c806a01b830d49db3b0d87f1dd8594 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-core@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-core@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-watchdog": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/4c7bcba489f87278a1d93502e058cb131d5b9baf2e0f4acec5edc65cc8e07ee07e4ef16603f5dbc95a29507579b5b8670b42727f4598a7195091ce8394400685 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-easy-image@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-easy-image@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-cloud-services": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-upload": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/434c41778b5dd107b3af2187483da0f34ae4d437c346e2f1315b1bd8518194359b73dcd6e8cd295c64afe111a26ae47c0753d31f8c044f0b79a2932bdced8aa5 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-editor-balloon@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-editor-balloon@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/5310b0ad37764804795c866dc151fac44c09c5ed0df28abdcfad7282112229ccdfe09da68eb632a4ae1dfd5c1664b8f562c95fc34a9b21f7df823d8f519d6128 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-editor-classic@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-editor-classic@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/808fd51f2cf9cf4edf980eca910f20952a9f25e475ac3c4eda10651e0f27bd0f71f338dbb5a90fa9b4ea178a4b48af71d680770652bd0df41a04ee57e1e02540 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-editor-decoupled@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-editor-decoupled@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/bc1243f69076c4835722e249847c7f27efbaef61ceaff54bd6595400e58ffce31aedca38f20eb65099bb05e460d3a3b69a1483fd91c1aea6a44e17515c7e00bb - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-editor-inline@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-editor-inline@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/bcb4f4fafa74617e67ead92f2e03e8df70756b5bbae1daadd62144a60474b20f60c31f34b1e96a8f46d05ba7fd72a7b8b82be346ad2c8da9d879b3fa47d48620 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-editor-multi-root@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-editor-multi-root@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/a463163ecba5de24534f678d651804f5419bc6ca4246789bd01a4be9170f31b2fa8b54cad62df4a8d3ea676ac9cc83f399a3129ca1ab3925b25b426cbdb0f044 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-emoji@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-emoji@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-mention": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - fuzzysort: "npm:3.1.0" - checksum: 10/5c4d6a85382ee97e75a74f5a0bf70256f87d17191f858e5ff61902cf5f60111a0e0c00b01298d0d6ce1a38b6e9cf3225fd432bc660ac4ef1c73a06472905e811 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-engine@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-engine@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/647c4eb215e86274b4c0f36c2c971323b8479d5a89d8b46dfb37a6ed5e52da8c7e748be080f50c1b04542bfc7dbdb07461a1facc7335d440e4df549c0684adab - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-enter@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-enter@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - checksum: 10/dee282886737ee0e0251529cdc9816ddf96f4733a0c1796aaf0995c55defbe089308b50f742668d25ccc740d57039e7ae8b07dcabc1b348119150e8758126803 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-essentials@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-essentials@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-select-all": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-undo": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/8cdaf2597ccc522f7b5bad284b5e6833ebecefbf51740b1c3ffa64ca47f296d3fe74b39c6c30219f3ebd1f4d3471677dd294c8b2831b9e8061f5862c91b5726c - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-find-and-replace@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-find-and-replace@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/d342729534c27434d13f81c1cb59c71682f7448afc4ec1420296407f1faf7b695917308fdd137ab2dacaade3e592874216967000ad36a2bea6a2e888882c9fff - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-font@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-font@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/4d08e73343ef024f148f77b8a51a0b408db8bc712e93239fe1c8342dd049d973b852d6560ef4cf93f42f69d08995c3e6009c268f452ce0e975199875ed8d5771 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-fullscreen@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-fullscreen@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-classic": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-decoupled": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/693a99280937e04ff7b2d64a00510efbcec41462b0cb60016007f65056f22f6def3f6fb739e6b50b1934666cc5d5af4d11fea3d0e860f26808480ea58b87724c - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-heading@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-heading@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-paragraph": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/3139872476d5ca545ffa0069e9b43d075b6e47b24b40dec5f3482f8237fad0d70b9f4785fd086fb9d6d2b1f41dd504280b30f03146ddd5cf14b1795df789d085 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-highlight@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-highlight@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/1d3a5ba24e890afba6cd06b9d0f40259919baa4ca5653514cf6847e24f47ecbeaf1c2f6bbcb15114867f01f5e9cc69af2788e46819f35458233cc1a89ff184e2 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-horizontal-line@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-horizontal-line@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/e4cca39ef3cd8f773af1e120432fc5bb8cc44dc07fc180f833709b6141c07590a1a8988a256993d8193d02f0f0670aaf0efd88d2646747c25b5d5f6c2fead2fb - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-html-embed@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-html-embed@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/c864798acdf15bb8ad236647df77423b57aca53434060ce29ba719f46bacffc208e1908d24af051d5683039e807d0991290fba932a2f79915dbe1a172e4ece91 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-html-support@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-html-support@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-heading": "npm:45.2.2" - "@ckeditor/ckeditor5-image": "npm:45.2.2" - "@ckeditor/ckeditor5-list": "npm:45.2.2" - "@ckeditor/ckeditor5-table": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/2877be6b3b755350d459754668aacb3b67fe3e274b14cae516522c6de268ec315af34f82441cd61e0807f538788ae22ea35fa961b8c2fd600d3655ab5e064f07 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-icons@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-icons@npm:45.2.2" - checksum: 10/64c032de1b886caebe48240a3ae9754311bf5cd6a4e3bf859e82bd75227a535fcfbc81a87be2f58696a648e0546545ed54245c332eb41d8f7fadab7689a5d0a3 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-image@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-image@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-undo": "npm:45.2.2" - "@ckeditor/ckeditor5-upload": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/f5510ca1107ec6c78d2282016477f54d7c30233eebb419899afe4ed4803b1f103baf888435d93887045450cba80adfdfac5cb9d7db29ae1faa403fddb402ce1e - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-indent@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-indent@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-heading": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-list": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/1d012e65d8279def213841d5962ee4907c17e00387e4bffa78653501a899e86b36b842aa8b0c03b0388b6f8dc6c866c104ca4a204a45f817b45bcb004386f465 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-integrations-common@npm:^2.2.2": - version: 2.2.3 - resolution: "@ckeditor/ckeditor5-integrations-common@npm:2.2.3" - peerDependencies: - ckeditor5: ">=42.0.0 || ^0.0.0-nightly" - checksum: 10/7e0caaf8ad43889b33372dbc503150818c636780c768e4b8c97adff0783a7565ffbdbc072fca983ed4889be840136a728e58b27979945855d67b11f128ea88a4 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-language@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-language@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/94a3afc63b7406a29e6599f7cced7b175780788d4ffc3a525c9485f99541db2ebaf09b5cbced81b134b6f8a1a0eff22e249bfcad5c7e2a2cb54c41018671242e - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-link@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-link@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-image": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/40c4b766cdfe82b14c4b169f7c38e758e2a0134468171572c1a7689c5617a52191db8ee215da988a90620914f53e612ad6094b49ee87ee39b5bc9a53c7049a7c - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-list@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-list@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/afad322e2b90690ca21bebb2ffbddbefad5d18c9b98f12e33ebc194956e05aeb8bc7a473de42e74684560cc186a4f624555a2e54f9299458c00fea92724145b0 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-markdown-gfm@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-markdown-gfm@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@types/marked": "npm:4.3.2" - "@types/turndown": "npm:5.0.5" - ckeditor5: "npm:45.2.2" - marked: "npm:4.0.12" - turndown: "npm:7.2.0" - turndown-plugin-gfm: "npm:1.0.2" - checksum: 10/46aa639632cd7febeffcf7fa869b1dcaa6f50526082c667886c18e494261645cd798765be5f6aa6195728d4c5e38ae0be6ada414b627810aba4b7ae13f39085d - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-media-embed@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-media-embed@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-undo": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/8f4f2ba7c56f9ac42a160f645e136498634a40dfb8e6fa5eb9baae7a81132f73b5ef030393f7203623b3e4be4a45d43171eaf5962c7e99e4a687960b495da8d3 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-mention@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-mention@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/ce3b725d01014824a172e52711b876d56d896f133bd8a8e9d6c708c664214e425b2ed9057f9fb26652b65c485fb8d62e9446a386e5ae4c08eb287b2ec139408f - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-minimap@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-minimap@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/94ef326fcb364dc5efc31029a9c46cd9efcfc51806b3b13eb7262637213e0492ccf0d055460c26ef7984738ba7732dabf9399a9955d56319c8b6b22e3203bd77 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-page-break@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-page-break@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/f0329d4fe6654cd7c053b736faf6836cf3020967482e2f5f445ac64c9fddf0049aad0533d86a14464770eac0bebcdabb04065facf23a593749d17e50b58fb42e - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-paragraph@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-paragraph@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - checksum: 10/1ea6bfc46538bb28f4ccc22f55a2cc0ff3b3fef46154dd01d4e99a25922514ffe725daa2f9478915a970baa9862b972d842b531a6fbf1c6d783a81c334b50d5f - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-paste-from-office@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-paste-from-office@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/3d954bb3aeb1d9ccf01642fa3abd2c1d4b6f6f621a1f93851acfb5104146f7773bcdc73328903f121d386374b314712746867612362b9b74c0ca626b0bfd0e90 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-react@npm:^9.5.0": - version: 9.5.0 - resolution: "@ckeditor/ckeditor5-react@npm:9.5.0" - dependencies: - "@ckeditor/ckeditor5-integrations-common": "npm:^2.2.2" - peerDependencies: - ckeditor5: ">=42.0.0 || ^0.0.0-nightly" - react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/f36fdb8c18026ea7e0920d7f58dd49305431656ad2ae22b329583724aedbd97a527ff470ceb1850c6967aecf6975763618ec3a69af461124b8ffad3f0bfcc671 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-remove-format@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-remove-format@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/453f551e887fdc5178dc7547e3085c8f0fc3b62fc384ae513b79576e52c2bc216576f7b5ae8f6eb2a742ee35df52cc2c1ac72ad05ee907e91910147e3df6f068 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-restricted-editing@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-restricted-editing@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/5e597a8a229ed73cd7a32fe96c19e66614d8e72bcac9ef95ab205f56c96fd9950b9a8b90fd4b88f6c23e24efa26b74cf873723e294932d9280a841642e1516be - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-select-all@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-select-all@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - checksum: 10/63bf3c36849df73eefbf43f97322e18f613064991c4ab9c17933a62fd172ffe3979b5c22f4e7e6f4f43a05a1d90f3e85f03dc33485f40718a860e4b6a2402bb8 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-show-blocks@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-show-blocks@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/9c08d00c87d8cb9e3bd225570bfa20179e0181eea5178028e92abef0fc2f8919c6fa4aad87193467f72ca3a31d68797beb3241fc04b52840e286b46be3d1a7d0 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-source-editing@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-source-editing@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-theme-lark": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/ef749569995a313e45c1bf5b2729ad14ff604bb386a172279bab7fb8daec90df14ff4c5be1ac20487aad0b7fdb46553b4b2c5b558c552373904ced91cc25dd12 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-special-characters@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-special-characters@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - checksum: 10/362388c4bef08748f3c87104bdcf1077f44608d15f377688120a4b5a0ca09b242efcbbf160070e6d337f89b01d77be52b46be203ad54f3c0a3e6638253f1711d - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-style@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-style@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-html-support": "npm:45.2.2" - "@ckeditor/ckeditor5-list": "npm:45.2.2" - "@ckeditor/ckeditor5-table": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/75d0a59b8dc76143a626cdad20824583bbfad039abd6a2ea5e5285767098379e41d4b7bc5df67ccf77194b80b86cb501d91131b35aec917a9196b06c2e48efc8 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-table@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-table@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/1520158625abf1568ba4b14e276f048ef0db086779d0aab893c11881b7b752a3bcdc8f03f2a99dd8e09dae829677f0d50c692f6e1622aab865d60f83ddade434 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-theme-lark@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-theme-lark@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - checksum: 10/bef4ab4242773072802473d50612562cd8a338c81c87d2549e01ea280461f22a64452fd592b0ad1d9e117cc234f26dca72d9d00caa8596f651bb7b00e4d9ab4a - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-typing@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-typing@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/1802269536f3a3db1d34e8742689161d25405ee5012cd604f775b3da89e70263f208395cef28f2fe40d886a15be7384b9545cf1985badd7785ed3c292ba3f0bd - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-ui@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-ui@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-multi-root": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@types/color-convert": "npm:2.0.4" - color-convert: "npm:2.0.1" - color-parse: "npm:1.4.2" - es-toolkit: "npm:1.32.0" - vanilla-colorful: "npm:0.7.2" - checksum: 10/aac99072d2b5801417cd466b0c8bab0ae70df17cfe14a429ee46b71bfbf2a95b8271ab70aeabf41eeeb0b95295ed6a31d987338c65974ef4107da7c0c1053f5b - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-undo@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-undo@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - checksum: 10/5375c228644227d1203a6beca8c0fa1c1f897df117997a42829b8df9a7b36e77c15d849a67529636ba973f80d14786ec19fcf9fd5a986e9f6aab4da8cfe29b32 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-upload@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-upload@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - checksum: 10/4f2f88aba61780f1acd2622c11969befd973087bead08f7963d8777dfaf4810dcbde7d0ecbfeb84e771664b1c6ed5948205240de5c2b4a48c05a1b9765af2920 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-utils@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-utils@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/8592b8c9b7bf9f3fb1fbee5b76d598540fde8008c2c08ff0d26c8e90011dfa3f1fa4839a3889bf90a9f6f8ff2527c7eeb0d9c02a6fe34d8292d79f037e9f1bcf - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-watchdog@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-watchdog@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-multi-root": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/ddb769741cb9f8616c6c6db4ec0ba2f654ea6061bd5b4abfce6935e6b261357180e273e6e491f03dd9578ef7edb4eff65acea1865f9f6d17ae09235719ae3bef - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-widget@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-widget@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/4db17430cd9dff19d0d1ba6fba8b8a59b5f160542b451c39fd2df3a25bc85975412ec386fb9d6dc3742121170603a1ffd5950163f4ddfd325a3e417b376f8687 - languageName: node - linkType: hard - -"@ckeditor/ckeditor5-word-count@npm:45.2.2": - version: 45.2.2 - resolution: "@ckeditor/ckeditor5-word-count@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - ckeditor5: "npm:45.2.2" - es-toolkit: "npm:1.32.0" - checksum: 10/bd7ef6bb6ca9670aa821c3b9a94c414b7a0b3eb673554093f700a067b3de5a4f3a54fbc70c1ed07d0e379d92ee35ea98dbd9b2fa8c300c229e0244f6f87e672b - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -3858,7 +2980,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -3904,13 +3026,6 @@ __metadata: languageName: node linkType: hard -"@mixmark-io/domino@npm:^2.2.0": - version: 2.2.0 - resolution: "@mixmark-io/domino@npm:2.2.0" - checksum: 10/839624ba6baab655c4f7393e8b8561516849926651e02f40484729b9869436b1e077906810bcac0bba4762448512d3ebd2f6d9b463d8ab0d5f54d75ca5306519 - languageName: node - linkType: hard - "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -4117,6 +3232,13 @@ __metadata: languageName: node linkType: hard +"@popperjs/core@npm:^2.9.0": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: 10/ddd16090cde777aaf102940f05d0274602079a95ad9805bd20bc55dcc7c3a2ba1b99dd5c73e5cc2753c3d31250ca52a67d58059459d7d27debb983a9f552936c + languageName: node + linkType: hard + "@prisma/client@npm:6.19.0": version: 6.19.0 resolution: "@prisma/client@npm:6.19.0" @@ -4222,6 +3344,13 @@ __metadata: languageName: node linkType: hard +"@remirror/core-constants@npm:3.0.0": + version: 3.0.0 + resolution: "@remirror/core-constants@npm:3.0.0" + checksum: 10/de15b1df099a7646739e5fb6bb55195618a8ac4fa938db7c719e867eefd72ebc5a05865591788ade449613141619cc1002fb6c0f824de4468dfefa951fbf19a2 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.47": version: 1.0.0-beta.47 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47" @@ -5778,6 +4907,327 @@ __metadata: languageName: node linkType: hard +"@tiptap/core@npm:^2.11.7, @tiptap/core@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/core@npm:2.27.2" + peerDependencies: + "@tiptap/pm": ^2.7.0 + checksum: 10/f8196a068416d0739c76a0b3cb838fe08bbde55e57978ab4f130193b94cbf1cd71653f19a124618a9145e51a0bda9db83e7723505e9fb83e3e50e133569eef0d + languageName: node + linkType: hard + +"@tiptap/extension-blockquote@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-blockquote@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/193fdd83cc1bc8f1f40758998d1f57976aefeb0302f0a2c369f9021d1291e53aef3324c9100c0f0d491a4dca5fd1f92f8e8b69c5a2897886e1b2c3883b306c76 + languageName: node + linkType: hard + +"@tiptap/extension-bold@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-bold@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/6047620d7def5d793a7e0077226657e7f947ab568448fb425561777a9b15cae9a34f1354b24929f2751d11afa337365189d89ce9a3dbb81ce2a0d7ab8b9f9553 + languageName: node + linkType: hard + +"@tiptap/extension-bubble-menu@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-bubble-menu@npm:2.27.2" + dependencies: + tippy.js: "npm:^6.3.7" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/ceb74df19c6a196638013b0759191f8131c90c62e8fbfbeae4444c79a447512995587d0a0ced85e0fd2f8688c4fbcf77a5ad8da9c9fee91610620add1dbcdf44 + languageName: node + linkType: hard + +"@tiptap/extension-bullet-list@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-bullet-list@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/fb3f53d5aa59d095c174b5afdcaac3851b32df93012a4db19ca3e68439655dd5dd7d94b414da302c439b0cee42a49f2dcc75d8cb8ea3ab2db7df17dc12a76594 + languageName: node + linkType: hard + +"@tiptap/extension-code-block@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-code-block@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/adc8fbd556a1a392304750ebec0e3ff36e341332867c6243f6f1f608ed17222adebe1f0a49da20ecf5d27a35d8f719fa6e89d2a44943cfc81895acddf4c41609 + languageName: node + linkType: hard + +"@tiptap/extension-code@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-code@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/aa6e4e0e7c7451e0f962f1a60a0d2323e48102acfcf3abd22c550455a866d113db10bb188e2bf1940e8a5467a06a63f9207bea0acb58bdf6b6327485b805e491 + languageName: node + linkType: hard + +"@tiptap/extension-document@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-document@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/cfd1fb8170d7b0b42461356e869a600e13570c3d1bac61ee4b7c52ffc942a93700c3a41b9a6a603a5a07e33aefc7b0dc2c74b8fb728509e60f22f0d77871d87e + languageName: node + linkType: hard + +"@tiptap/extension-dropcursor@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-dropcursor@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/1d76d08f0a2eb4710025d1c69b304f6d29d9e4e0b6c11ce765d316d3c26446a90427f2255e8b79319f011766a56c558e3592a97b7492e629a55d583cff8fa7c4 + languageName: node + linkType: hard + +"@tiptap/extension-floating-menu@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-floating-menu@npm:2.27.2" + dependencies: + tippy.js: "npm:^6.3.7" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/875004c01fed1a1a7b7b1f4306ad2bcbb989941111f135875fd9f02e3848249a669a59ddd0c294dc5c637c67bb2ca899f52b9d3f5a8ddd38d26bde6faa49c27b + languageName: node + linkType: hard + +"@tiptap/extension-gapcursor@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-gapcursor@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/de295e7a14a5e6253b49bf9513ab7d4724c9271fcd839ec4fb4818b9e4493408d12002bc2b129f31986255a92dca29dbe84c0ccfa68e6f140cd61b45b61b0170 + languageName: node + linkType: hard + +"@tiptap/extension-hard-break@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-hard-break@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/66ceee0136cb1d73c7282ec0616d6321002a531ccac434fed5119d5e2b869e86d31c6498e8d62b80a8b5dfafce48ddf1006de12c0dff6a1161d823008a89d94c + languageName: node + linkType: hard + +"@tiptap/extension-heading@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-heading@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/f91d52e8bd0278d1028d421483dc8b0d78ed80697c1f65eada9be18836e1f08bc5760ab9067343ccad038bacf38da55cd0d2ee5241a78cffa3b8ebb4e626b467 + languageName: node + linkType: hard + +"@tiptap/extension-history@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-history@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/f659368f3943f0c216eb0c263cc38a0a5bd1a156073ea2c7f42a395ad2147cfed3276a85bf305a33faf1ff526f5b87a95d039a0fc8d110e2fe99e656696b85fb + languageName: node + linkType: hard + +"@tiptap/extension-horizontal-rule@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-horizontal-rule@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/4bdf2d628f0741edd434bdf4212fadd6b96d18073e3ec9799b206fa39c0989e7ebe6a4d070b4cbc3398679db93ecab2ee2e75919e117f470d1e43959c0f2484e + languageName: node + linkType: hard + +"@tiptap/extension-image@npm:^2.11.7": + version: 2.27.2 + resolution: "@tiptap/extension-image@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/f40e1243cda89dee3959d41d287fe304ed21eb63fb57c34cd2aa81b069e57d156ac054fb6a175deafae784b6e1d04c2c35a87edb726907cd1a9cc5bca9312c46 + languageName: node + linkType: hard + +"@tiptap/extension-italic@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-italic@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/fcd178f07ce9c98e61a91f032a8eed7832824a7e4a28ce09e4b92829d852e887d5b5a7e962bee3cb98230afb83234feb33924efa084f932c2a33bc194d8c71c8 + languageName: node + linkType: hard + +"@tiptap/extension-link@npm:^2.11.7": + version: 2.27.2 + resolution: "@tiptap/extension-link@npm:2.27.2" + dependencies: + linkifyjs: "npm:^4.3.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10/9f794842bae46c02667794cd6bc8eecb4126c1a19515047312072080ae88e8ff4c4631d9ac7c967c55fcd78780bfa1e4d90a4f419f1c1cd40544c0d9e2b1fad0 + languageName: node + linkType: hard + +"@tiptap/extension-list-item@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-list-item@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/b981632083dfacc991ac4446cfaaa2696e3ad79732a9290920ec56149402a0a1544d7fe6fa23a81fedee21afcaeb5f22e5f8f293e11d89994ff008cbe6e24d13 + languageName: node + linkType: hard + +"@tiptap/extension-ordered-list@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-ordered-list@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/e28ac87cd0aa2a43a408ba037b6639ee1fabfdb804accdbcd8f3de39029c0691ccf56f45a4530ea09fa3b986808e6377bffb68702956664303a7958ee134ab7e + languageName: node + linkType: hard + +"@tiptap/extension-paragraph@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-paragraph@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/a45228b23c11cbd666829a40af1397679b4505972e084873a1183e8da5a3ccc8f75cf8f3ccf5365979a51fe317b3b67e46a24287ac4d45172ad4b3c8cf8dc59b + languageName: node + linkType: hard + +"@tiptap/extension-strike@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-strike@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/1d9b65486041870be3bba77c110678bdf6ad8a0e1b94db0434c8873a1d86a1896e9224b6d9a8d3dc33061717d8a1aed48d819ba3843af604a77be26ae25251a9 + languageName: node + linkType: hard + +"@tiptap/extension-text-align@npm:^2.11.7": + version: 2.27.2 + resolution: "@tiptap/extension-text-align@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/423aa762f6886aedf883bd7d562f5fa5372714cecd9104ff0f81c4c3cac16d16b9d29867a473c4b5e62deee40bbfb59ea6c37930661b8fcbd29e54598d1df404 + languageName: node + linkType: hard + +"@tiptap/extension-text-style@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-text-style@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/3beaf6e74d9b8031b8c1b5368b156274c3408a6875d7c2e20b7f9e9b1da34df8c3346d9a6eae0bcddbaca31811504b7b893f673ea18e6c4319dbbd0442d5f90e + languageName: node + linkType: hard + +"@tiptap/extension-text@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/extension-text@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/69cb4dd9a588be6ea799afdc7c26519e5f888b95617c2fac0f5b3824c89f7fc3283408a0c8b9075f4607844eb7263fad46f46052bcd7096c09c16418aa1f61fd + languageName: node + linkType: hard + +"@tiptap/extension-underline@npm:^2.11.7": + version: 2.27.2 + resolution: "@tiptap/extension-underline@npm:2.27.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10/6e92f49ae0a7884d437aa8d8b9875822485c2d3a759e9ff6cf6e47a6360b4a72532ece65ea5e06ace1840ca013785c4ffa473595b7fe793d2196305d6b4d47b5 + languageName: node + linkType: hard + +"@tiptap/pm@npm:^2.11.7, @tiptap/pm@npm:^2.27.2": + version: 2.27.2 + resolution: "@tiptap/pm@npm:2.27.2" + dependencies: + prosemirror-changeset: "npm:^2.3.0" + prosemirror-collab: "npm:^1.3.1" + prosemirror-commands: "npm:^1.6.2" + prosemirror-dropcursor: "npm:^1.8.1" + prosemirror-gapcursor: "npm:^1.3.2" + prosemirror-history: "npm:^1.4.1" + prosemirror-inputrules: "npm:^1.4.0" + prosemirror-keymap: "npm:^1.2.2" + prosemirror-markdown: "npm:^1.13.1" + prosemirror-menu: "npm:^1.2.4" + prosemirror-model: "npm:^1.23.0" + prosemirror-schema-basic: "npm:^1.2.3" + prosemirror-schema-list: "npm:^1.4.1" + prosemirror-state: "npm:^1.4.3" + prosemirror-tables: "npm:^1.6.4" + prosemirror-trailing-node: "npm:^3.0.0" + prosemirror-transform: "npm:^1.10.2" + prosemirror-view: "npm:^1.37.0" + checksum: 10/2178e5c5795e5f61962b4931ba3fb0797b4761c01a49d458bd6faaed0031dd020246bbfbad41aa3bb6d7f7268685729486b1dcad79603967443578f1535833a2 + languageName: node + linkType: hard + +"@tiptap/react@npm:^2.11.7": + version: 2.27.2 + resolution: "@tiptap/react@npm:2.27.2" + dependencies: + "@tiptap/extension-bubble-menu": "npm:^2.27.2" + "@tiptap/extension-floating-menu": "npm:^2.27.2" + "@types/use-sync-external-store": "npm:^0.0.6" + fast-deep-equal: "npm:^3" + use-sync-external-store: "npm:^1" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/2b37a09b1bd7522a6e43e15bbd1981047b354b7ffbd290fcc1768774acd1cec521594f469365ebcd35eed76415fecafb3f48ad0bbc501ac095a3ecc5802d3173 + languageName: node + linkType: hard + +"@tiptap/starter-kit@npm:^2.11.7": + version: 2.27.2 + resolution: "@tiptap/starter-kit@npm:2.27.2" + dependencies: + "@tiptap/core": "npm:^2.27.2" + "@tiptap/extension-blockquote": "npm:^2.27.2" + "@tiptap/extension-bold": "npm:^2.27.2" + "@tiptap/extension-bullet-list": "npm:^2.27.2" + "@tiptap/extension-code": "npm:^2.27.2" + "@tiptap/extension-code-block": "npm:^2.27.2" + "@tiptap/extension-document": "npm:^2.27.2" + "@tiptap/extension-dropcursor": "npm:^2.27.2" + "@tiptap/extension-gapcursor": "npm:^2.27.2" + "@tiptap/extension-hard-break": "npm:^2.27.2" + "@tiptap/extension-heading": "npm:^2.27.2" + "@tiptap/extension-history": "npm:^2.27.2" + "@tiptap/extension-horizontal-rule": "npm:^2.27.2" + "@tiptap/extension-italic": "npm:^2.27.2" + "@tiptap/extension-list-item": "npm:^2.27.2" + "@tiptap/extension-ordered-list": "npm:^2.27.2" + "@tiptap/extension-paragraph": "npm:^2.27.2" + "@tiptap/extension-strike": "npm:^2.27.2" + "@tiptap/extension-text": "npm:^2.27.2" + "@tiptap/extension-text-style": "npm:^2.27.2" + "@tiptap/pm": "npm:^2.27.2" + checksum: 10/30bc98c15fe3b8d819fd55bb396688f11cfe4e8c25af68a020e3c60d7d6c24bfdd8f3731bbe035065758f09a77075b24f9c562d6b34608398af2e82629bd4d98 + languageName: node + linkType: hard + "@tree-sitter-grammars/tree-sitter-yaml@npm:=0.7.1": version: 0.7.1 resolution: "@tree-sitter-grammars/tree-sitter-yaml@npm:0.7.1" @@ -5896,22 +5346,6 @@ __metadata: languageName: node linkType: hard -"@types/color-convert@npm:2.0.4": - version: 2.0.4 - resolution: "@types/color-convert@npm:2.0.4" - dependencies: - "@types/color-name": "npm:^1.1.0" - checksum: 10/08385948a3baf0c1dcb021791c3980d948423e92d1f1f400d7d50d089f4d26cf3a6ef1fdc402bd5bbccc325c44d2884834e66cde242c794483950e99967b9c8d - languageName: node - linkType: hard - -"@types/color-name@npm:^1.1.0": - version: 1.1.5 - resolution: "@types/color-name@npm:1.1.5" - checksum: 10/47600afcd1dbd7c668103ccaed912acd97760336dbb9fb08bdb34be004daa1f06a336183b51d44852e4460fe8b336347c7f5af330160c755f6a8fb4fa19e6bc0 - languageName: node - linkType: hard - "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -5976,13 +5410,6 @@ __metadata: languageName: node linkType: hard -"@types/js-cookie@npm:^2.2.6": - version: 2.2.7 - resolution: "@types/js-cookie@npm:2.2.7" - checksum: 10/851f47e94ca1fc43661d8f51614d67a613e7810c91b876d0a3b311ce72f7df800107fd02a08cb6948184e12c120b4f058edca2f50424d8798bdcffd6627281e3 - languageName: node - linkType: hard - "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -5997,10 +5424,27 @@ __metadata: languageName: node linkType: hard -"@types/marked@npm:4.3.2": - version: 4.3.2 - resolution: "@types/marked@npm:4.3.2" - checksum: 10/c1b5aa2cee0b8929164f4a8d206d7f89256ebde0d5b9998e9cdf6da2a5fa71162f66ef30e80b98213ee2c3372514b39b756e8494980174a4907a4aa4690b4d1d +"@types/linkify-it@npm:^5": + version: 5.0.0 + resolution: "@types/linkify-it@npm:5.0.0" + checksum: 10/c3919044d4876f9d71d037e861745cd2485c95ac8c36a4fa67b132d4e60eb1d067e123cc7965c9cf5110eea351517d767f0d306af5e9147d6d0af87bc374ddcf + languageName: node + linkType: hard + +"@types/markdown-it@npm:^14.0.0": + version: 14.1.2 + resolution: "@types/markdown-it@npm:14.1.2" + dependencies: + "@types/linkify-it": "npm:^5" + "@types/mdurl": "npm:^2" + checksum: 10/ca2f239c8d59610b9f936fd40261a6ccf2fa1ae27a21816c031e5712542dcf9ee01e2fe29b31118df90716e11ade54e47d92a498e9b6488800e77ca8827255a2 + languageName: node + linkType: hard + +"@types/mdurl@npm:^2": + version: 2.0.0 + resolution: "@types/mdurl@npm:2.0.0" + checksum: 10/78746e96c655ceed63db06382da466fd52c7e9dc54d60b12973dfdd110cae06b9439c4b90e17bb8d4461109184b3ea9f3e9f96b3e4bf4aa9fe18b6ac35f283c8 languageName: node linkType: hard @@ -6135,13 +5579,6 @@ __metadata: languageName: node linkType: hard -"@types/turndown@npm:5.0.5": - version: 5.0.5 - resolution: "@types/turndown@npm:5.0.5" - checksum: 10/8b5e246f4fec5135d3313954b2be32aa24d599fbae15d75a1627066e1279098f260eaaf191fd8a43bf4a0678f8cf5d120e7fb19839668c237d5569b0836324fd - languageName: node - linkType: hard - "@types/unist@npm:^2": version: 2.0.11 resolution: "@types/unist@npm:2.0.11" @@ -6777,13 +6214,6 @@ __metadata: languageName: node linkType: hard -"@xobotyi/scrollbar-width@npm:^1.9.5": - version: 1.9.5 - resolution: "@xobotyi/scrollbar-width@npm:1.9.5" - checksum: 10/026ccd174ec3ce032f42794c7e2ee9dab3cfee4f8f9d6ce4f2b4a2fe50cbf8be7406583fb2e203707c699690c5d40a13ee1611f1f67f6ceb01ac2a543acadc30 - languageName: node - linkType: hard - "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -7408,13 +6838,6 @@ __metadata: languageName: node linkType: hard -"blurhash@npm:2.0.5": - version: 2.0.5 - resolution: "blurhash@npm:2.0.5" - checksum: 10/ff0e156c1383e79b03ab2cef8de35ab560118cb70d311cf447f17311795ab1dad6261b1e0904b886fa4547f47ccd7984d5155c1404a19224533c79b46c153c55 - languageName: node - linkType: hard - "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": version: 4.12.2 resolution: "bn.js@npm:4.12.2" @@ -7723,13 +7146,6 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.3.1": - version: 5.3.1 - resolution: "camelcase@npm:5.3.1" - checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b - languageName: node - linkType: hard - "camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" @@ -7932,75 +7348,6 @@ __metadata: languageName: node linkType: hard -"ckeditor5@npm:45.2.2, ckeditor5@npm:^45.2.0": - version: 45.2.2 - resolution: "ckeditor5@npm:45.2.2" - dependencies: - "@ckeditor/ckeditor5-adapter-ckfinder": "npm:45.2.2" - "@ckeditor/ckeditor5-alignment": "npm:45.2.2" - "@ckeditor/ckeditor5-autoformat": "npm:45.2.2" - "@ckeditor/ckeditor5-autosave": "npm:45.2.2" - "@ckeditor/ckeditor5-basic-styles": "npm:45.2.2" - "@ckeditor/ckeditor5-block-quote": "npm:45.2.2" - "@ckeditor/ckeditor5-bookmark": "npm:45.2.2" - "@ckeditor/ckeditor5-ckbox": "npm:45.2.2" - "@ckeditor/ckeditor5-ckfinder": "npm:45.2.2" - "@ckeditor/ckeditor5-clipboard": "npm:45.2.2" - "@ckeditor/ckeditor5-cloud-services": "npm:45.2.2" - "@ckeditor/ckeditor5-code-block": "npm:45.2.2" - "@ckeditor/ckeditor5-core": "npm:45.2.2" - "@ckeditor/ckeditor5-easy-image": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-balloon": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-classic": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-decoupled": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-inline": "npm:45.2.2" - "@ckeditor/ckeditor5-editor-multi-root": "npm:45.2.2" - "@ckeditor/ckeditor5-emoji": "npm:45.2.2" - "@ckeditor/ckeditor5-engine": "npm:45.2.2" - "@ckeditor/ckeditor5-enter": "npm:45.2.2" - "@ckeditor/ckeditor5-essentials": "npm:45.2.2" - "@ckeditor/ckeditor5-find-and-replace": "npm:45.2.2" - "@ckeditor/ckeditor5-font": "npm:45.2.2" - "@ckeditor/ckeditor5-fullscreen": "npm:45.2.2" - "@ckeditor/ckeditor5-heading": "npm:45.2.2" - "@ckeditor/ckeditor5-highlight": "npm:45.2.2" - "@ckeditor/ckeditor5-horizontal-line": "npm:45.2.2" - "@ckeditor/ckeditor5-html-embed": "npm:45.2.2" - "@ckeditor/ckeditor5-html-support": "npm:45.2.2" - "@ckeditor/ckeditor5-icons": "npm:45.2.2" - "@ckeditor/ckeditor5-image": "npm:45.2.2" - "@ckeditor/ckeditor5-indent": "npm:45.2.2" - "@ckeditor/ckeditor5-language": "npm:45.2.2" - "@ckeditor/ckeditor5-link": "npm:45.2.2" - "@ckeditor/ckeditor5-list": "npm:45.2.2" - "@ckeditor/ckeditor5-markdown-gfm": "npm:45.2.2" - "@ckeditor/ckeditor5-media-embed": "npm:45.2.2" - "@ckeditor/ckeditor5-mention": "npm:45.2.2" - "@ckeditor/ckeditor5-minimap": "npm:45.2.2" - "@ckeditor/ckeditor5-page-break": "npm:45.2.2" - "@ckeditor/ckeditor5-paragraph": "npm:45.2.2" - "@ckeditor/ckeditor5-paste-from-office": "npm:45.2.2" - "@ckeditor/ckeditor5-remove-format": "npm:45.2.2" - "@ckeditor/ckeditor5-restricted-editing": "npm:45.2.2" - "@ckeditor/ckeditor5-select-all": "npm:45.2.2" - "@ckeditor/ckeditor5-show-blocks": "npm:45.2.2" - "@ckeditor/ckeditor5-source-editing": "npm:45.2.2" - "@ckeditor/ckeditor5-special-characters": "npm:45.2.2" - "@ckeditor/ckeditor5-style": "npm:45.2.2" - "@ckeditor/ckeditor5-table": "npm:45.2.2" - "@ckeditor/ckeditor5-theme-lark": "npm:45.2.2" - "@ckeditor/ckeditor5-typing": "npm:45.2.2" - "@ckeditor/ckeditor5-ui": "npm:45.2.2" - "@ckeditor/ckeditor5-undo": "npm:45.2.2" - "@ckeditor/ckeditor5-upload": "npm:45.2.2" - "@ckeditor/ckeditor5-utils": "npm:45.2.2" - "@ckeditor/ckeditor5-watchdog": "npm:45.2.2" - "@ckeditor/ckeditor5-widget": "npm:45.2.2" - "@ckeditor/ckeditor5-word-count": "npm:45.2.2" - checksum: 10/ad852a84e1c23b23e6097deccc21272f572c2b886a7215577c54e30aad9bc9c76370d0d59892a474417bdc8e92497baa9359e1450e6895ae088e5be381c1ef14 - languageName: node - linkType: hard - "class-variance-authority@npm:^0.7.1": version: 0.7.1 resolution: "class-variance-authority@npm:0.7.1" @@ -8047,7 +7394,7 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:2.0.1, color-convert@npm:^2.0.1": +"color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" dependencies: @@ -8063,15 +7410,6 @@ __metadata: languageName: node linkType: hard -"color-parse@npm:1.4.2": - version: 1.4.2 - resolution: "color-parse@npm:1.4.2" - dependencies: - color-name: "npm:^1.0.0" - checksum: 10/62ef7bd8b9cb3d650e27200d71b7551257f03ac8ccf97381ab44726fc3299f5f3c5fd47ed0773ffd3f03e7570f4b0ee988d3c35d4f30b0f7008b62f2ab9004fd - languageName: node - linkType: hard - "color-string@npm:^1.9.0": version: 1.9.1 resolution: "color-string@npm:1.9.1" @@ -8338,6 +7676,13 @@ __metadata: languageName: node linkType: hard +"crelt@npm:^1.0.0": + version: 1.0.6 + resolution: "crelt@npm:1.0.6" + checksum: 10/5ed326ca6bd243b1dba6b943f665b21c2c04be03271824bc48f20dba324b0f8233e221f8c67312526d24af2b1243c023dc05a41bd8bd05d1a479fd2c72fb39c3 + languageName: node + linkType: hard + "cross-env@npm:^10.1.0": version: 10.1.0 resolution: "cross-env@npm:10.1.0" @@ -8382,15 +7727,6 @@ __metadata: languageName: node linkType: hard -"css-in-js-utils@npm:^3.1.0": - version: 3.1.0 - resolution: "css-in-js-utils@npm:3.1.0" - dependencies: - hyphenate-style-name: "npm:^1.0.3" - checksum: 10/bd2f569f1870389004cfacfd7b798c0f40933d34af1f040c391a08322d097790b9a9524affb2ba4d26122e9cb8f4256afb59edb6077dbe607506944a9c673c67 - languageName: node - linkType: hard - "css-loader@npm:^6.7.1, css-loader@npm:^6.7.3": version: 6.11.0 resolution: "css-loader@npm:6.11.0" @@ -8441,16 +7777,6 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^1.1.2": - version: 1.1.3 - resolution: "css-tree@npm:1.1.3" - dependencies: - mdn-data: "npm:2.0.14" - source-map: "npm:^0.6.1" - checksum: 10/29710728cc4b136f1e9b23ee1228ec403ec9f3d487bc94a9c5dbec563c1e08c59bc917dd6f82521a35e869ff655c298270f43ca673265005b0cd05b292eb05ab - languageName: node - linkType: hard - "css-tree@npm:^2.3.1": version: 2.3.1 resolution: "css-tree@npm:2.3.1" @@ -8503,7 +7829,7 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2, csstype@npm:^3.1.2": +"csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" checksum: 10/f593cce41ff5ade23f44e77521e3a1bcc2c64107041e1bf6c3c32adc5187d0d60983292fda326154d20b01079e24931aa5b08e4467cc488b60bb1e7f6d478ade @@ -9205,19 +8531,7 @@ __metadata: is-callable: "npm:^1.2.7" is-date-object: "npm:^1.0.5" is-symbol: "npm:^1.0.4" - checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b - languageName: node - linkType: hard - -"es-toolkit@npm:1.32.0": - version: 1.32.0 - resolution: "es-toolkit@npm:1.32.0" - dependenciesMeta: - "@trivago/prettier-plugin-sort-imports@4.3.0": - unplugged: true - prettier-plugin-sort-re-exports@0.0.1: - unplugged: true - checksum: 10/98eaf802d5d2fbb2bd7ab69073034b7571b56abfa8e7cb046f22a501718b288c901b4f61a53f36b000549ab57c4265b2f8b86ba255e1a7d6e841ff86822e01be + checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b languageName: node linkType: hard @@ -9805,7 +9119,7 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": +"fast-deep-equal@npm:^3, fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" checksum: 10/e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d @@ -9866,13 +9180,6 @@ __metadata: languageName: node linkType: hard -"fast-shallow-equal@npm:^1.0.0": - version: 1.0.0 - resolution: "fast-shallow-equal@npm:1.0.0" - checksum: 10/ae89318ce43c0c46410d9511ac31520d59cfe675bad3d0b1cb5f900b2d635943d788b8370437178e91ae0d0412decc394229c03e69925ade929a8c02da241610 - languageName: node - linkType: hard - "fast-uri@npm:^3.0.1": version: 3.1.0 resolution: "fast-uri@npm:3.1.0" @@ -9880,13 +9187,6 @@ __metadata: languageName: node linkType: hard -"fastest-stable-stringify@npm:^2.0.2": - version: 2.0.2 - resolution: "fastest-stable-stringify@npm:2.0.2" - checksum: 10/41bb381c0eab1419eb353658c0d78cb79a4e99ef8f53ec5d36b131f076e62ff3cdca6d22888640c55ea3382ae2c93d8629a67f5734655442976708448a8c2500 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.19.1 resolution: "fastq@npm:1.19.1" @@ -10187,13 +9487,6 @@ __metadata: languageName: node linkType: hard -"fuzzysort@npm:3.1.0": - version: 3.1.0 - resolution: "fuzzysort@npm:3.1.0" - checksum: 10/6220e62ff00c0855c7e8d38714ed5ba9641e14ca2e2b793229965d7b6d03a14783bf04ecc169033a4429898ccb28b3c77e45ac73abd6d4647920705832b0cb71 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -10629,13 +9922,6 @@ __metadata: languageName: node linkType: hard -"hyphenate-style-name@npm:^1.0.3": - version: 1.1.0 - resolution: "hyphenate-style-name@npm:1.1.0" - checksum: 10/b9ed74e29181d96bd58a2d0e62fc4a19879db591dba268275829ff0ae595fcdf11faafaeaa63330a45c3004664d7db1f0fc7cdb372af8ee4615ed8260302c207 - languageName: node - linkType: hard - "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -10734,15 +10020,6 @@ __metadata: languageName: node linkType: hard -"inline-style-prefixer@npm:^7.0.1": - version: 7.0.1 - resolution: "inline-style-prefixer@npm:7.0.1" - dependencies: - css-in-js-utils: "npm:^3.1.0" - checksum: 10/a430c962693f32a36bcec0124c9798bcf3725bb90468d493108c0242446a9cc92ff1967bdf99b6ce5331e7a9b75e6836bc9ba1b3d4756876b8ef48036acb2509 - languageName: node - linkType: hard - "internal-slot@npm:^1.1.0": version: 1.1.0 resolution: "internal-slot@npm:1.1.0" @@ -11224,13 +10501,6 @@ __metadata: languageName: node linkType: hard -"js-cookie@npm:^2.2.1": - version: 2.2.1 - resolution: "js-cookie@npm:2.2.1" - checksum: 10/4387f5f5691cb96ca9ff8852c589d3012b53f484fda68630a39e20cabc6c5b740f09225e23233ba56cd9de6ebe300a23d20b2c7315f10c309ad5a89fd8c4990b - languageName: node - linkType: hard - "js-file-download@npm:^0.4.12": version: 0.4.12 resolution: "js-file-download@npm:0.4.12" @@ -11401,10 +10671,19 @@ __metadata: languageName: node linkType: hard -"load-script@npm:^2.0.0": - version: 2.0.0 - resolution: "load-script@npm:2.0.0" - checksum: 10/fdf2df0e6adafc4d853d9a8daea8996948909ba0bca697e28ca59ce069bb05e3ace388f7f5cf9692d2972221c900154b09b041c76bc2ef6a30f1df540d84f638 +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: "npm:^2.0.0" + checksum: 10/ef3b7609dda6ec0c0be8a7b879cea195f0d36387b0011660cd6711bba0ad82137f59b458b7e703ec74f11d88e7c1328e2ad9b855a8500c0ded67461a8c4519e6 + languageName: node + linkType: hard + +"linkifyjs@npm:^4.3.2": + version: 4.3.2 + resolution: "linkifyjs@npm:4.3.2" + checksum: 10/b03477486658d1e5531bf65ee1fdc0f79423594e689184c67b8a63c75d9f35d1cd0344edd97d5799502cde4f3163d620e2cbd9e72ad718c6a95084177c004386 languageName: node linkType: hard @@ -11467,20 +10746,6 @@ __metadata: languageName: node linkType: hard -"lodash.isempty@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.isempty@npm:4.4.0" - checksum: 10/b69de4e08038f3d802fa2f510fd97f6b1785a359a648382ba30fb59e17ce0bcdad9bef2cdb9f9501abb9064c74c6edbb8db86a6d827e0d380a50a6738e051ec3 - languageName: node - linkType: hard - -"lodash.mapkeys@npm:^4.6.0": - version: 4.6.0 - resolution: "lodash.mapkeys@npm:4.6.0" - checksum: 10/2b2066f04370e36020cdd5338d5d2d5000841140f54833aa0c2324c82ae20349b50774a288c45ad2f2301c1bcd6cd5b341f742ff682ff459c6d5f6a3615c7478 - languageName: node - linkType: hard - "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -11488,27 +10753,6 @@ __metadata: languageName: node linkType: hard -"lodash.omit@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.omit@npm:4.5.0" - checksum: 10/f5c67cd1df11f1275662060febb629a4d4e7b547c4bea66454508b5e6096162c2af882aab1ff8cb5dcf2b328f22252416de6ca9c1334588f6310edfac525e511 - languageName: node - linkType: hard - -"lodash.pick@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.pick@npm:4.4.0" - checksum: 10/5a76778aa1c245ce081d19c5a625a44cdf4853f421c8789ec962cb5d73dd21be7cf11ae3bc2123ff5f432326ed0176d674d22ca6e0e8f9eaba5b74b00f632c12 - languageName: node - linkType: hard - -"lodash.upperfirst@npm:^4.3.1": - version: 4.3.1 - resolution: "lodash.upperfirst@npm:4.3.1" - checksum: 10/3e849d4eb4dbf26faee6435edda8e707b65a5dbd2f10f8def5a16a57bbbf38d3b7506950f0dd455e9c46ba73af35f1de75df4ef83952106949413d64eed59333 - languageName: node - linkType: hard - "lodash@npm:^4.15.0, lodash@npm:^4.17.20, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -11647,12 +10891,19 @@ __metadata: languageName: node linkType: hard -"marked@npm:4.0.12": - version: 4.0.12 - resolution: "marked@npm:4.0.12" +"markdown-it@npm:^14.0.0": + version: 14.1.1 + resolution: "markdown-it@npm:14.1.1" + dependencies: + argparse: "npm:^2.0.1" + entities: "npm:^4.4.0" + linkify-it: "npm:^5.0.0" + mdurl: "npm:^2.0.0" + punycode.js: "npm:^2.3.1" + uc.micro: "npm:^2.1.0" bin: - marked: bin/marked.js - checksum: 10/151da6d88581f6e843ebd7e9982abf020aaad55ed6fb2dcb9593a3e922633a1906e25da3dfc954e94724277f4fff1945fd061db4682b2069bd8353601366bdbf + markdown-it: bin/markdown-it.mjs + checksum: 10/088822c8aa9346ba4af6a205f6ee0f4baae55e3314f040dc5c28c897d57d0f979840c71872b3582a6a6e572d8c851c54e323c82f4559011dfa2e96224fc20fc2 languageName: node linkType: hard @@ -11674,13 +10925,6 @@ __metadata: languageName: node linkType: hard -"mdn-data@npm:2.0.14": - version: 2.0.14 - resolution: "mdn-data@npm:2.0.14" - checksum: 10/64c629fcf14807e30d6dc79f97cbcafa16db066f53a294299f3932b3beb0eb0d1386d3a7fe408fc67348c449a4e0999360c894ba4c81eb209d7be4e36503de0e - languageName: node - linkType: hard - "mdn-data@npm:2.0.28": version: 2.0.28 resolution: "mdn-data@npm:2.0.28" @@ -11695,6 +10939,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 10/1720349d4a53e401aa993241368e35c0ad13d816ad0b28388928c58ca9faa0cf755fa45f18ccbf64f4ce54a845a50ddce5c84e4016897b513096a68dac4b0158 + languageName: node + linkType: hard + "memfs@npm:^3.4.1, memfs@npm:^3.4.12": version: 3.6.0 resolution: "memfs@npm:3.6.0" @@ -11930,25 +11181,6 @@ __metadata: languageName: node linkType: hard -"nano-css@npm:^5.6.2": - version: 5.6.2 - resolution: "nano-css@npm:5.6.2" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.15" - css-tree: "npm:^1.1.2" - csstype: "npm:^3.1.2" - fastest-stable-stringify: "npm:^2.0.2" - inline-style-prefixer: "npm:^7.0.1" - rtl-css-js: "npm:^1.16.1" - stacktrace-js: "npm:^2.0.2" - stylis: "npm:^4.3.0" - peerDependencies: - react: "*" - react-dom: "*" - checksum: 10/6ed9f36957b19fc2dcf1644a853030cce70775bec3fed596cab9156063d522d5cb52cb1479117e4390acbe45b69321c9eb33915d96414aabaf09bff40497bb4a - languageName: node - linkType: hard - "nanoid@npm:^3.3.11, nanoid@npm:^3.3.6": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -12447,6 +11679,13 @@ __metadata: languageName: node linkType: hard +"orderedmap@npm:^2.0.0": + version: 2.1.1 + resolution: "orderedmap@npm:2.1.1" + checksum: 10/082cf970b0b66d1c5a904b07880534092ce8a2f2eea7a52cf111f6c956210fa88226c13866aef4d22a3abe56924f21ead12f7ee8c1dfaf2f63d897a4e7c23328 + languageName: node + linkType: hard + "os-browserify@npm:^0.3.0": version: 0.3.0 resolution: "os-browserify@npm:0.3.0" @@ -12722,7 +11961,6 @@ __metadata: dependencies: "@auth/supabase-adapter": "npm:^1.8.0" "@chromatic-com/storybook": "npm:^3.2.4" - "@ckeditor/ckeditor5-react": "npm:^9.5.0" "@hono/zod-openapi": "npm:^0.19.9" "@hono/zod-validator": "npm:^0.4.3" "@hookform/resolvers": "npm:^5.1.1" @@ -12739,6 +11977,14 @@ __metadata: "@svgr/webpack": "npm:^8.1.0" "@tanstack/react-query": "npm:^5.87.4" "@tanstack/react-query-devtools": "npm:^5.87.4" + "@tiptap/core": "npm:^2.11.7" + "@tiptap/extension-image": "npm:^2.11.7" + "@tiptap/extension-link": "npm:^2.11.7" + "@tiptap/extension-text-align": "npm:^2.11.7" + "@tiptap/extension-underline": "npm:^2.11.7" + "@tiptap/pm": "npm:^2.11.7" + "@tiptap/react": "npm:^2.11.7" + "@tiptap/starter-kit": "npm:^2.11.7" "@types/navermaps": "npm:^3.9.1" "@types/node": "npm:^20" "@types/react": "npm:^18" @@ -12749,7 +11995,6 @@ __metadata: "@vitest/ui": "npm:^4.0.9" chart.js: "npm:^4.4.9" chartjs-plugin-datalabels: "npm:^2.2.0" - ckeditor5: "npm:^45.2.0" class-variance-authority: "npm:^0.7.1" clsx: "npm:^2.1.1" cross-env: "npm:^10.1.0" @@ -12769,7 +12014,6 @@ __metadata: react-dom: "npm:18" react-dropzone: "npm:^14.3.8" react-hook-form: "npm:^7.53.2" - react-naver-maps: "npm:^0.1.4" sanitize-html: "npm:^2.17.2" sharp: "npm:^0.33.5" storybook: "npm:^8.5.8" @@ -13161,7 +12405,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -13181,6 +12425,200 @@ __metadata: languageName: node linkType: hard +"prosemirror-changeset@npm:^2.3.0": + version: 2.4.0 + resolution: "prosemirror-changeset@npm:2.4.0" + dependencies: + prosemirror-transform: "npm:^1.0.0" + checksum: 10/86aa15bcb4bc164d929851b8148ebb93efe5a62990c7efc34e8d6dafb4fca441846e03ebaef3356c744ced28c6ce2c23953725d5ce106c849272cd69644f21b8 + languageName: node + linkType: hard + +"prosemirror-collab@npm:^1.3.1": + version: 1.3.1 + resolution: "prosemirror-collab@npm:1.3.1" + dependencies: + prosemirror-state: "npm:^1.0.0" + checksum: 10/6b1ccc52841fbb62a39ef0fb8da2d731381030609ea7a0ba7d533b1937d56fe4b91344e79c023e790bed5392efe9f917c41c8434e0a379dc1dc842ba83594e34 + languageName: node + linkType: hard + +"prosemirror-commands@npm:^1.0.0, prosemirror-commands@npm:^1.6.2": + version: 1.7.1 + resolution: "prosemirror-commands@npm:1.7.1" + dependencies: + prosemirror-model: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.10.2" + checksum: 10/60efe77a9a6b9f06d66442980946f49a4804c24a7aca06ee4d55333a6c55d0f1e3189613bc27f31d16496133969a454f29f071caa8ef2d38126aefbc36f81c4a + languageName: node + linkType: hard + +"prosemirror-dropcursor@npm:^1.8.1": + version: 1.8.2 + resolution: "prosemirror-dropcursor@npm:1.8.2" + dependencies: + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.1.0" + prosemirror-view: "npm:^1.1.0" + checksum: 10/02349c56152d0261c61d462b07684bb8179ab0ea488ab333dadcc9181b9ec8d5aa625feb2c54088090bd8f4e540321219938a0642d8ed043fea8a11f371ef058 + languageName: node + linkType: hard + +"prosemirror-gapcursor@npm:^1.3.2": + version: 1.4.1 + resolution: "prosemirror-gapcursor@npm:1.4.1" + dependencies: + prosemirror-keymap: "npm:^1.0.0" + prosemirror-model: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-view: "npm:^1.0.0" + checksum: 10/6dd89675a49e129493f4e458f9a4f96be98ee545b96024a4ac531251606061d2971d41b4014cea526c91806d4aae6b9a5459e321a8281a19504dff0bab9c77f8 + languageName: node + linkType: hard + +"prosemirror-history@npm:^1.0.0, prosemirror-history@npm:^1.4.1": + version: 1.5.0 + resolution: "prosemirror-history@npm:1.5.0" + dependencies: + prosemirror-state: "npm:^1.2.2" + prosemirror-transform: "npm:^1.0.0" + prosemirror-view: "npm:^1.31.0" + rope-sequence: "npm:^1.3.0" + checksum: 10/f59402632f786e0c757ea31494ab3fb99665b4ca5aaff50dc1918250aa558c23fcc938d1645b1b165c1a87ec9700cec3d53c6bc90e2c977872f144be425b76fd + languageName: node + linkType: hard + +"prosemirror-inputrules@npm:^1.4.0": + version: 1.5.1 + resolution: "prosemirror-inputrules@npm:1.5.1" + dependencies: + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.0.0" + checksum: 10/81f08415a39ad795a2a097c8667eb86e4473eca1389c793139d6bacbb69be740732e8ff8f21b2808130bacd7f53a1b0b7621b48a22ff44d902995b41654e4b80 + languageName: node + linkType: hard + +"prosemirror-keymap@npm:^1.0.0, prosemirror-keymap@npm:^1.2.2, prosemirror-keymap@npm:^1.2.3": + version: 1.2.3 + resolution: "prosemirror-keymap@npm:1.2.3" + dependencies: + prosemirror-state: "npm:^1.0.0" + w3c-keyname: "npm:^2.2.0" + checksum: 10/acb251b03f57920282342bf404755de6ff68e150aaf252aca9bbeb63bbba8f53e03091095d399def979c95db0649620a22a47a332418c3a2408a0f43737b18d9 + languageName: node + linkType: hard + +"prosemirror-markdown@npm:^1.13.1": + version: 1.13.4 + resolution: "prosemirror-markdown@npm:1.13.4" + dependencies: + "@types/markdown-it": "npm:^14.0.0" + markdown-it: "npm:^14.0.0" + prosemirror-model: "npm:^1.25.0" + checksum: 10/6c37908ab7f99825b799ac7e699efc9dc8691cf7b2f9379bb97e3b4034ecb6f24d6798b1edfc16d3a34213c7dfa56249dc7fa1201fd7baf380e79648709c2a5a + languageName: node + linkType: hard + +"prosemirror-menu@npm:^1.2.4": + version: 1.3.0 + resolution: "prosemirror-menu@npm:1.3.0" + dependencies: + crelt: "npm:^1.0.0" + prosemirror-commands: "npm:^1.0.0" + prosemirror-history: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + checksum: 10/f486cc7dd0193432d574cd1413530ef54cdce7028c0cc067b27183cc84be56d70f52b6e4e38197f797cbd54517cda9a51b43876f04d9994c0ec34f0aea6949c5 + languageName: node + linkType: hard + +"prosemirror-model@npm:^1.0.0, prosemirror-model@npm:^1.20.0, prosemirror-model@npm:^1.21.0, prosemirror-model@npm:^1.23.0, prosemirror-model@npm:^1.25.0, prosemirror-model@npm:^1.25.4": + version: 1.25.4 + resolution: "prosemirror-model@npm:1.25.4" + dependencies: + orderedmap: "npm:^2.0.0" + checksum: 10/63c5d6dd3b70e42650f07b4a2ed87e7442291b1f95a9930bf4ff2f7c6a1228e95db0e996cdabed667f4cbaf67c3ee290e19b91a31dd199da95477ac4d1b868e3 + languageName: node + linkType: hard + +"prosemirror-schema-basic@npm:^1.2.3": + version: 1.2.4 + resolution: "prosemirror-schema-basic@npm:1.2.4" + dependencies: + prosemirror-model: "npm:^1.25.0" + checksum: 10/51972732657b7eca6d0fc294d980f7ef0079acee962451280ef79e59790717a1f65e9227364de8780caa8e1e5c7d48a57b3b58488d24e23b8b035ea7ec0ec37a + languageName: node + linkType: hard + +"prosemirror-schema-list@npm:^1.4.1": + version: 1.5.1 + resolution: "prosemirror-schema-list@npm:1.5.1" + dependencies: + prosemirror-model: "npm:^1.0.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.7.3" + checksum: 10/eaf308093ecbce7fc7d0e3c3653faa120afb3679bdb20c99f79c75c90b4f57d61c6580ea162dd0af274013e98c5d4529ecbe6b9bd7e66e7d6d6b5be6aae0f052 + languageName: node + linkType: hard + +"prosemirror-state@npm:^1.0.0, prosemirror-state@npm:^1.2.2, prosemirror-state@npm:^1.4.3, prosemirror-state@npm:^1.4.4": + version: 1.4.4 + resolution: "prosemirror-state@npm:1.4.4" + dependencies: + prosemirror-model: "npm:^1.0.0" + prosemirror-transform: "npm:^1.0.0" + prosemirror-view: "npm:^1.27.0" + checksum: 10/90e66cbc49f2eceeb174f5184ed94df432a8b83866ab74ef602d21555dd151eb1ff9a0794b12d98384981601ebaa885d930f6c667165ff969874944a2f9e2488 + languageName: node + linkType: hard + +"prosemirror-tables@npm:^1.6.4": + version: 1.8.5 + resolution: "prosemirror-tables@npm:1.8.5" + dependencies: + prosemirror-keymap: "npm:^1.2.3" + prosemirror-model: "npm:^1.25.4" + prosemirror-state: "npm:^1.4.4" + prosemirror-transform: "npm:^1.10.5" + prosemirror-view: "npm:^1.41.4" + checksum: 10/06117482bf0053c0496cdbab4042a7566e8f1f91ba5116823e1ed24cfea48603b044f6ac1e522b046b17505132575f864e10a62e79fd1117374717a5a5161e84 + languageName: node + linkType: hard + +"prosemirror-trailing-node@npm:^3.0.0": + version: 3.0.0 + resolution: "prosemirror-trailing-node@npm:3.0.0" + dependencies: + "@remirror/core-constants": "npm:3.0.0" + escape-string-regexp: "npm:^4.0.0" + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + checksum: 10/044b199b8001373c1bd4c1573876597840df89e66c1f02497a8bb4f2885ebe830faa9764e1269ed6c24bf2fde06ad5f40322afde648ae331d4663f531000adaa + languageName: node + linkType: hard + +"prosemirror-transform@npm:^1.0.0, prosemirror-transform@npm:^1.1.0, prosemirror-transform@npm:^1.10.2, prosemirror-transform@npm:^1.10.5, prosemirror-transform@npm:^1.7.3": + version: 1.11.0 + resolution: "prosemirror-transform@npm:1.11.0" + dependencies: + prosemirror-model: "npm:^1.21.0" + checksum: 10/d6d8754e7d8b2c9059cc856e3fd04d3c4376533cfa74143c112e1dec76fcdcce8c5a260b2d241702e5d4ae0d87632df7d474ce47ff3c1825b9fdd76235a35920 + languageName: node + linkType: hard + +"prosemirror-view@npm:^1.0.0, prosemirror-view@npm:^1.1.0, prosemirror-view@npm:^1.27.0, prosemirror-view@npm:^1.31.0, prosemirror-view@npm:^1.37.0, prosemirror-view@npm:^1.41.4": + version: 1.41.7 + resolution: "prosemirror-view@npm:1.41.7" + dependencies: + prosemirror-model: "npm:^1.20.0" + prosemirror-state: "npm:^1.0.0" + prosemirror-transform: "npm:^1.1.0" + checksum: 10/457cb83cdd677b9ccc8e579e53c9f2fc10316b6627952bffc415c843ccd20069efab15da0ee651541191930ce7e31e790c08f6ddbacfda47dc280aa47dca42db + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -13202,6 +12640,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 10/f0e946d1edf063f9e3d30a32ca86d8ff90ed13ca40dad9c75d37510a04473340cfc98db23a905cc1e517b1e9deb0f6021dce6f422ace235c60d3c9ac47c5a16a + languageName: node + linkType: hard + "punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" @@ -13486,27 +12931,6 @@ __metadata: languageName: node linkType: hard -"react-naver-maps@npm:^0.1.4": - version: 0.1.4 - resolution: "react-naver-maps@npm:0.1.4" - dependencies: - camelcase: "npm:^5.3.1" - load-script: "npm:^2.0.0" - lodash.isempty: "npm:^4.4.0" - lodash.mapkeys: "npm:^4.6.0" - lodash.omit: "npm:^4.5.0" - lodash.pick: "npm:^4.4.0" - lodash.upperfirst: "npm:^4.3.1" - prop-types: "npm:^15.7.2" - react-use: "npm:^17.3.1" - suspend-react: "npm:^0.0.8" - peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - checksum: 10/4d7e83dc9311c20e29a23f17dc5e40f434614f26e36f2355ba45143fa873d00b44be32302bfa3c646e831e4235075bb0a84a058326a501e0c1af478be321749c - languageName: node - linkType: hard - "react-redux@npm:^9.2.0": version: 9.2.0 resolution: "react-redux@npm:9.2.0" @@ -13556,41 +12980,6 @@ __metadata: languageName: node linkType: hard -"react-universal-interface@npm:^0.6.2": - version: 0.6.2 - resolution: "react-universal-interface@npm:0.6.2" - peerDependencies: - react: "*" - tslib: "*" - checksum: 10/bded7a34f5e44223495a095b6c29fa3ea6c8338a166d0f307d21443c20c9fa54a95bdae11d05ab3e6f61a7fa1eaa027f6fbe7ca064a2fb2af46539653ebcb308 - languageName: node - linkType: hard - -"react-use@npm:^17.3.1": - version: 17.6.0 - resolution: "react-use@npm:17.6.0" - dependencies: - "@types/js-cookie": "npm:^2.2.6" - "@xobotyi/scrollbar-width": "npm:^1.9.5" - copy-to-clipboard: "npm:^3.3.1" - fast-deep-equal: "npm:^3.1.3" - fast-shallow-equal: "npm:^1.0.0" - js-cookie: "npm:^2.2.1" - nano-css: "npm:^5.6.2" - react-universal-interface: "npm:^0.6.2" - resize-observer-polyfill: "npm:^1.5.1" - screenfull: "npm:^5.1.0" - set-harmonic-interval: "npm:^1.0.1" - throttle-debounce: "npm:^3.0.1" - ts-easing: "npm:^0.2.0" - tslib: "npm:^2.1.0" - peerDependencies: - react: "*" - react-dom: "*" - checksum: 10/a817b74e82b481a39d3539bfe8d3b535c08d59d44a75ea91f65e56a7ccaedb0de185159e50b44ea4a635dda0c1c7159f07530e81a1d64b57130e0a715a107795 - languageName: node - linkType: hard - "react@npm:18": version: 18.3.1 resolution: "react@npm:18.3.1" @@ -13866,13 +13255,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-polyfill@npm:^1.5.1": - version: 1.5.1 - resolution: "resize-observer-polyfill@npm:1.5.1" - checksum: 10/e10ee50cd6cf558001de5c6fb03fee15debd011c2f694564b71f81742eef03fb30d6c2596d1d5bf946d9991cb692fcef529b7bd2e4057041377ecc9636c753ce - languageName: node - linkType: hard - "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -14101,12 +13483,10 @@ __metadata: languageName: node linkType: hard -"rtl-css-js@npm:^1.16.1": - version: 1.16.1 - resolution: "rtl-css-js@npm:1.16.1" - dependencies: - "@babel/runtime": "npm:^7.1.2" - checksum: 10/fa6a3e1f73e65bf5763b8a051942477a0852ee072d29ebad0999f02556a73715e72374d9a31ddec3fe023b09702b56f8be3a5a0404816e795ab86ea879183e02 +"rope-sequence@npm:^1.3.0": + version: 1.3.4 + resolution: "rope-sequence@npm:1.3.4" + checksum: 10/57b5dd8c28ece05bb5f33eea6ea56facb00d4893269bb83aa8656f69065c1bc0707ec9bb816bce0e5f4d489d88942c7f0f0a1c3655773753ef158c9dd0e9456d languageName: node linkType: hard @@ -14253,13 +13633,6 @@ __metadata: languageName: node linkType: hard -"screenfull@npm:^5.1.0": - version: 5.2.0 - resolution: "screenfull@npm:5.2.0" - checksum: 10/b8b4b8010f48889341ad1981ca9e6e02db1f10dec686244d95bd2bfde47451059f5ba4c744449913b10f021f14f79d374987a873b6086eb488295962ba50381e - languageName: node - linkType: hard - "semver@npm:^6.0.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -14322,13 +13695,6 @@ __metadata: languageName: node linkType: hard -"set-harmonic-interval@npm:^1.0.1": - version: 1.0.1 - resolution: "set-harmonic-interval@npm:1.0.1" - checksum: 10/14b9ce98625af9e0d80165a5c8ceb76ce1206df641197e020780e570f268f5427961138d3f47591962e2626b498a051a4488eaa646e5473373f843d7e9e468d4 - languageName: node - linkType: hard - "set-proto@npm:^1.0.0": version: 1.0.0 resolution: "set-proto@npm:1.0.0" @@ -14603,14 +13969,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:0.5.6": - version: 0.5.6 - resolution: "source-map@npm:0.5.6" - checksum: 10/c62fe98e106c762307eea3a982242c1a76a31bc762da10fe2dda12252d423c163e0cd45d313330c8bd040cc5121702511138252308f72b8a9273825e81e4db30 - languageName: node - linkType: hard - -"source-map@npm:0.6.1, source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": +"source-map@npm:0.6.1, source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff @@ -14654,15 +14013,6 @@ __metadata: languageName: node linkType: hard -"stack-generator@npm:^2.0.5": - version: 2.0.10 - resolution: "stack-generator@npm:2.0.10" - dependencies: - stackframe: "npm:^1.3.4" - checksum: 10/4fc3978a934424218a0aa9f398034e1f78153d5ff4f4ff9c62478c672debb47dd58de05b09fc3900530cbb526d72c93a6e6c9353bacc698e3b1c00ca3dda0c47 - languageName: node - linkType: hard - "stackback@npm:0.0.2": version: 0.0.2 resolution: "stackback@npm:0.0.2" @@ -14677,27 +14027,6 @@ __metadata: languageName: node linkType: hard -"stacktrace-gps@npm:^3.0.4": - version: 3.1.2 - resolution: "stacktrace-gps@npm:3.1.2" - dependencies: - source-map: "npm:0.5.6" - stackframe: "npm:^1.3.4" - checksum: 10/21cb60ce0990f7a661e964cf4bdef1e70dda2286fb628fbd0fd1e69e8925138433d08ed84969de2d396b3b91515e15336a502f777c26587db89f3933d6f63f9b - languageName: node - linkType: hard - -"stacktrace-js@npm:^2.0.2": - version: 2.0.2 - resolution: "stacktrace-js@npm:2.0.2" - dependencies: - error-stack-parser: "npm:^2.0.6" - stack-generator: "npm:^2.0.5" - stacktrace-gps: "npm:^3.0.4" - checksum: 10/e5f60a09852687e4a9206927fe1078e24d63e00a71a2dcddd67940e9504a54931a3454439d5b4e3e0e62aeb979be810573e8d3332fbef0dbfa335a8781b4b57c - languageName: node - linkType: hard - "std-env@npm:^3.10.0": version: 3.10.0 resolution: "std-env@npm:3.10.0" @@ -14971,13 +14300,6 @@ __metadata: languageName: node linkType: hard -"stylis@npm:^4.3.0": - version: 4.3.6 - resolution: "stylis@npm:4.3.6" - checksum: 10/6ebe8a37827124e0caf0704c13d39c121f6e6a8433eb8c67cfce508477b24a4434d1731198ba0b6e453655022bbf5beda93585f38ff420545e5356f925f83761 - languageName: node - linkType: hard - "sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -15021,15 +14343,6 @@ __metadata: languageName: node linkType: hard -"suspend-react@npm:^0.0.8": - version: 0.0.8 - resolution: "suspend-react@npm:0.0.8" - peerDependencies: - react: ">=17.0" - checksum: 10/cfab3da0c5960dc1ca318b70d2beaffc5709dddaf46fccc0bbb8668503967ab70b1f1f1727aa12d082d9013fba21d00e90333984550e2c7668ea1ee1ca7b729c - languageName: node - linkType: hard - "svg-parser@npm:^2.0.4": version: 2.0.4 resolution: "svg-parser@npm:2.0.4" @@ -15271,13 +14584,6 @@ __metadata: languageName: node linkType: hard -"throttle-debounce@npm:^3.0.1": - version: 3.0.1 - resolution: "throttle-debounce@npm:3.0.1" - checksum: 10/c2b591bc881c595d44d5ee82cc607747569a84cd9652e7d9613d92759d84ffd61eab1ca56c6a294316b8c9978ff6d46c2c94ed95de5847f3de4b6c30342cb947 - languageName: node - linkType: hard - "timers-browserify@npm:^2.0.12": version: 2.0.12 resolution: "timers-browserify@npm:2.0.12" @@ -15346,6 +14652,15 @@ __metadata: languageName: node linkType: hard +"tippy.js@npm:^6.3.7": + version: 6.3.7 + resolution: "tippy.js@npm:6.3.7" + dependencies: + "@popperjs/core": "npm:^2.9.0" + checksum: 10/9bd1c6ab68704dd10b8896fd66e28f3b4b4017a32b8802a9d57a565dee1704df45c249d8363bcaca235dbc0d9a7a90d6f1326f1e6b999f7809b36599a3f92eb3 + languageName: node + linkType: hard + "to-buffer@npm:^1.2.0, to-buffer@npm:^1.2.1, to-buffer@npm:^1.2.2": version: 1.2.2 resolution: "to-buffer@npm:1.2.2" @@ -15441,13 +14756,6 @@ __metadata: languageName: node linkType: hard -"ts-easing@npm:^0.2.0": - version: 0.2.0 - resolution: "ts-easing@npm:0.2.0" - checksum: 10/e67ee862acca3b2e2718e736f31999adcef862d0df76d76a0e138588728d8a87dfec9978556044640bd0e90203590ad88ac2fe8746d0e9959b8d399132315150 - languageName: node - linkType: hard - "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -15552,7 +14860,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.7.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.7.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -15566,22 +14874,6 @@ __metadata: languageName: node linkType: hard -"turndown-plugin-gfm@npm:1.0.2": - version: 1.0.2 - resolution: "turndown-plugin-gfm@npm:1.0.2" - checksum: 10/07d8520ad4d272da5b69ed8032bf10c4cf5ff605541da0df1ac2c61bd5e0df6e25cbd8395a4f66e5db03cb2c4421d202feeeebb39c59a0fe45966ffcde3c5564 - languageName: node - linkType: hard - -"turndown@npm:7.2.0": - version: 7.2.0 - resolution: "turndown@npm:7.2.0" - dependencies: - "@mixmark-io/domino": "npm:^2.2.0" - checksum: 10/ad373d9cff76e80196effe29c19c1a14fda428fa9f41ef7458daa9c0c8939f37bbc36d0a504940f95ba34a6db8fd4d9cafd1ae498f91d010805f8d540e9e71bb - languageName: node - linkType: hard - "tween-functions@npm:^1.2.0": version: 1.2.0 resolution: "tween-functions@npm:1.2.0" @@ -15694,6 +14986,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 10/37197358242eb9afe367502d4638ac8c5838b78792ab218eafe48287b0ed28aaca268ec0392cc5729f6c90266744de32c06ae938549aee041fc93b0f9672d6b2 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -15917,6 +15216,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.4.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" @@ -15978,13 +15286,6 @@ __metadata: languageName: node linkType: hard -"vanilla-colorful@npm:0.7.2": - version: 0.7.2 - resolution: "vanilla-colorful@npm:0.7.2" - checksum: 10/a087531f0d342eb6f4c93e81d9e26fc024ea434111a49486ca6efd996e84e3d816cbf3086157042cd6fc5e360c09db624b033b55e92f26b2211fa06bc9b728c0 - languageName: node - linkType: hard - "vite@npm:^6.0.0 || ^7.0.0": version: 7.2.2 resolution: "vite@npm:7.2.2" @@ -16106,6 +15407,13 @@ __metadata: languageName: node linkType: hard +"w3c-keyname@npm:^2.2.0": + version: 2.2.8 + resolution: "w3c-keyname@npm:2.2.8" + checksum: 10/95bafa4c04fa2f685a86ca1000069c1ec43ace1f8776c10f226a73296caeddd83f893db885c2c220ebeb6c52d424e3b54d7c0c1e963bbf204038ff1a944fbb07 + languageName: node + linkType: hard + "watchpack@npm:^2.4.1": version: 2.4.4 resolution: "watchpack@npm:2.4.4" From 9c47877f70c30c3ed5226af76543c9fcdeebddc1 Mon Sep 17 00:00:00 2001 From: kuan Date: Tue, 31 Mar 2026 19:00:41 +0900 Subject: [PATCH 12/39] =?UTF-8?q?feat:=ED=95=84=ED=84=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/page.tsx | 2 + src/app/perfumes/page.tsx | 7 +- src/client/hooks/query/useSearchQuery.ts | 1 + src/client/stores/perfumeStore.ts | 17 +++ src/client/utils/api/search.api.ts | 17 ++- .../commons/perfumeList/filter/FilterItem.tsx | 13 ++- .../commons/perfumeList/filter/FilterList.tsx | 7 +- .../perfumeList/filter/filter.constants.ts | 6 +- .../commons/perfumeList/modal/ItemModal.tsx | 10 +- .../commons/perfumeList/search/index.tsx | 6 +- src/components/commons/search/SearchBar.tsx | 15 ++- .../domains/perfumes/PageClient.tsx | 23 ++-- .../perfumes/hook/usePerfumeSearchState.ts | 6 + src/server/hono/schemas/search.schema.ts | 16 +++ .../__tests__/helpers/search.test.helpers.ts | 2 + .../services/__tests__/search.service.test.ts | 103 +++++++++++++++++- src/server/hono/services/search.service.ts | 47 +++++--- src/shared/types/search.types.ts | 3 + 18 files changed, 246 insertions(+), 55 deletions(-) diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index f48b929c..b84e9db3 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -3,6 +3,8 @@ import { MainContent } from "@/components/domains/main/mainContent/MainContent"; import { MainLogo } from "@/components/domains/main/MainLogo"; import { MainSearchBar } from "@/components/domains/main/MainSearchBar"; +export const dynamic = "force-dynamic"; + export default function Home() { return ( <> diff --git a/src/app/perfumes/page.tsx b/src/app/perfumes/page.tsx index 32c97075..fe1e96b5 100644 --- a/src/app/perfumes/page.tsx +++ b/src/app/perfumes/page.tsx @@ -3,6 +3,7 @@ import { Suspense } from "react"; import getQueryClient from "@/client/utils/getQueryClient"; import { brandApi } from "@/client/utils/api/brands.api"; import { perfumeApi } from "@/client/utils/api/perfumes.api"; +import { queryKeys } from "@/client/utils/queryKeys"; import PageClient from "@/components/domains/perfumes/PageClient"; import type { Metadata } from "next"; @@ -27,15 +28,15 @@ export default async function Page() { try { await Promise.all([ queryClient.prefetchQuery({ - queryKey: ["brand", "list"], + queryKey: queryKeys.filter.brands(), queryFn: () => brandApi.list(), }), queryClient.prefetchQuery({ - queryKey: ["perfume", "notes"], + queryKey: queryKeys.filter.notes(), queryFn: () => perfumeApi.notes(), }), queryClient.prefetchQuery({ - queryKey: ["perfume", "accords"], + queryKey: queryKeys.filter.accords(), queryFn: () => perfumeApi.accords(), }), ]); diff --git a/src/client/hooks/query/useSearchQuery.ts b/src/client/hooks/query/useSearchQuery.ts index 9a85e8f8..f8ca980f 100644 --- a/src/client/hooks/query/useSearchQuery.ts +++ b/src/client/hooks/query/useSearchQuery.ts @@ -44,6 +44,7 @@ interface PerfumeFilters { brands?: string[]; notes?: string[]; accords?: string[]; + gender?: string[]; } /** diff --git a/src/client/stores/perfumeStore.ts b/src/client/stores/perfumeStore.ts index 227dbcf4..4e6f7b51 100644 --- a/src/client/stores/perfumeStore.ts +++ b/src/client/stores/perfumeStore.ts @@ -85,6 +85,7 @@ interface FilterStore { pendingFilters: Record; handlePendingChange: (category: string, value: string) => void; + handlePendingSingleChange: (category: string, value: string) => void; resetPending: () => void; commitFilters: (category: string) => void; cancelPending: (category: string) => void; @@ -119,6 +120,22 @@ export const useFilterStore = create((set, get) => ({ return { pendingFilters: newPending }; }), + // 단일 선택 카테고리 (gender 등): 기존 값을 교체 + handlePendingSingleChange: (category, value) => + set((state) => { + const newPending = { ...state.pendingFilters }; + const current = newPending[category]; + + // 이미 같은 값이면 선택 해제 + if (current?.length === 1 && current[0] === value) { + delete newPending[category]; + } else { + newPending[category] = [value]; + } + + return { pendingFilters: newPending }; + }), + resetPending: () => set(() => ({ pendingFilters: {}, diff --git a/src/client/utils/api/search.api.ts b/src/client/utils/api/search.api.ts index 05f898b1..3219a84a 100644 --- a/src/client/utils/api/search.api.ts +++ b/src/client/utils/api/search.api.ts @@ -2,21 +2,31 @@ import { apiClient } from "./client"; import { ApiResponse } from "@/server/hono/schemas/common.schema"; import { PaginatedSearchResponse } from "@/server/hono/schemas/search.schema"; import { + GenderSort, SearchPerfumesParams, SearchPerfumesWithFiltersParams, } from "@/shared/types/search.types"; +const GENDER_SORT_VALUES: GenderSort[] = ["MASCULINE", "UNISEX", "FEMININE"]; + const formatFilters = (filters: Record) => { const result: { brandFilter?: string[]; notesFilter?: string[]; accordsFilter?: string[]; + genderSort?: GenderSort; } = {}; if (filters.brand?.length) result.brandFilter = filters.brand; if (filters.notes?.length) result.notesFilter = filters.notes; if (filters.accords?.length) result.accordsFilter = filters.accords; + // gender는 단일 선택 sort 값으로 처리 + const genderValue = filters.gender?.[0]; + if (genderValue && (GENDER_SORT_VALUES as string[]).includes(genderValue)) { + result.genderSort = genderValue as GenderSort; + } + return result; }; @@ -52,19 +62,22 @@ export const searchApi = { ): Promise => { try { const formattedFilters = formatFilters(filters); - const hasFilters = Object.keys(formattedFilters).length > 0; + const { genderSort, ...filterParams } = formattedFilters; + const hasFilters = Object.keys(filterParams).length > 0; const response = hasFilters ? await searchApi.perfumesWithFilters({ searchText: searchText || "", cursor: cursor || undefined, limit: 15, - ...formattedFilters, + genderSort, + ...filterParams, }) : await searchApi.perfumes({ searchText: searchText || undefined, cursor: cursor || undefined, limit: 15, + genderSort, }); if (!response) { diff --git a/src/components/commons/perfumeList/filter/FilterItem.tsx b/src/components/commons/perfumeList/filter/FilterItem.tsx index d495a7a6..9cfebfcd 100644 --- a/src/components/commons/perfumeList/filter/FilterItem.tsx +++ b/src/components/commons/perfumeList/filter/FilterItem.tsx @@ -19,15 +19,14 @@ const FilterItem = ({ }) => { const EMPTY_SET = new Set(); const committedFilters = useFilterStore((state) => state.committedFilters); - const handlePendingChange = useFilterStore( - (state) => state.handlePendingChange - ); + const handlePendingChange = useFilterStore((state) => state.handlePendingChange); + const handlePendingSingleChange = useFilterStore((state) => state.handlePendingSingleChange); const filterSet = committedFilters[category] ?? EMPTY_SET; const { selectedOption, currentOption } = getFilterOptionMeta( filterSet, - label + label, ); const [isSelected, setIsSelected] = useState(!!currentOption); @@ -62,7 +61,11 @@ const FilterItem = ({ options={options} currentOption={currentOption} handleChangeOption={(option: Option) => { - handlePendingChange(category, option.value); + if (category === "gender") { + handlePendingSingleChange(category, option.value); + } else { + handlePendingChange(category, option.value); + } }} /> )} diff --git a/src/components/commons/perfumeList/filter/FilterList.tsx b/src/components/commons/perfumeList/filter/FilterList.tsx index 15a4f1bb..0081bfae 100644 --- a/src/components/commons/perfumeList/filter/FilterList.tsx +++ b/src/components/commons/perfumeList/filter/FilterList.tsx @@ -12,7 +12,11 @@ type FilterOptions = { accords: Option[]; }; -const FilterList = ({ filterOptions }: { filterOptions: FilterOptions }) => { +const FilterList = ({ + filterOptions, +}: { + filterOptions: FilterOptions; +}) => { const committedFilters = useFilterStore((state) => state.committedFilters); const closeFilter = useFilterStore((state) => state.closeFilter); const resetFilters = useFilterStore((state) => state.resetFilters); @@ -52,6 +56,7 @@ const FilterList = ({ filterOptions }: { filterOptions: FilterOptions }) => { filterOptions={filterOptions} closeFilter={closeFilter} resetFilters={resetFilters} + /> ); diff --git a/src/components/commons/perfumeList/filter/filter.constants.ts b/src/components/commons/perfumeList/filter/filter.constants.ts index fffb2b12..9e842d57 100644 --- a/src/components/commons/perfumeList/filter/filter.constants.ts +++ b/src/components/commons/perfumeList/filter/filter.constants.ts @@ -7,7 +7,7 @@ export const FILTER_LABELS = { }; export const GENDER_OPTIONS = [ - { id: "masculine", name: { en: "masculine", ko: "남성적인" } }, - { id: "unisex", name: { en: "unisex", ko: "중성적인" } }, - { id: "feminine", name: { en: "feminine", ko: "여성적인" } }, + { id: "MASCULINE", name: { en: "masculine", ko: "남성적인" } }, + { id: "UNISEX", name: { en: "unisex", ko: "중성적인" } }, + { id: "FEMININE", name: { en: "feminine", ko: "여성적인" } }, ]; diff --git a/src/components/commons/perfumeList/modal/ItemModal.tsx b/src/components/commons/perfumeList/modal/ItemModal.tsx index 99a3f9d3..83ca535a 100644 --- a/src/components/commons/perfumeList/modal/ItemModal.tsx +++ b/src/components/commons/perfumeList/modal/ItemModal.tsx @@ -77,7 +77,7 @@ export default function FilterItemModal({ // pending이 비어있으면 (아무것도 선택 안 함) 모든 옵션 활성화 const hasPendingFilters = Object.keys(pendingFilters).length > 0; - const isBrandCategory = id === "brand"; + const isAlwaysActiveCategory = id === "brand" || id === "gender"; options.forEach((option) => { const isSelected = selectedSet.includes(option.value); @@ -86,14 +86,12 @@ export default function FilterItemModal({ if (isSelected) { selected.push(option); } else if ( - isBrandCategory || // 브랜드는 항상 활성화 + isAlwaysActiveCategory || // 브랜드·성별은 항상 활성화 !hasPendingFilters || isAvailable ) { - // pending이 없거나, 가능한 옵션이면 활성화 available.push(option); } else { - // pending이 있는데 가능하지 않으면 비활성화 disabled.push(option); } }); @@ -138,7 +136,9 @@ export default function FilterItemModal({ }; const handleReset = () => { - resetPending(); // pending 전체 초기화 + resetPending(); + commitFilters(id); // pending이 비었으므로 해당 카테고리 committed에서 제거 + close(id); }; return ( diff --git a/src/components/commons/perfumeList/search/index.tsx b/src/components/commons/perfumeList/search/index.tsx index 46313ac8..e71a4aab 100644 --- a/src/components/commons/perfumeList/search/index.tsx +++ b/src/components/commons/perfumeList/search/index.tsx @@ -40,7 +40,11 @@ export function SearchHeader({ /> )}
diff --git a/src/components/commons/search/SearchBar.tsx b/src/components/commons/search/SearchBar.tsx index 235204fb..861a3f60 100644 --- a/src/components/commons/search/SearchBar.tsx +++ b/src/components/commons/search/SearchBar.tsx @@ -36,6 +36,18 @@ export function SearchBar({ focus:outline-none focus:border-primary-300 text-label-1 tablet:text-body-1 `; + const { onKeyDown: restKeyDown, ...inputRest } = rest as { + onKeyDown?: React.KeyboardEventHandler; + [key: string]: unknown; + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + (onClick as unknown as (() => void) | undefined)?.(); + } + restKeyDown?.(e); + }; + return (
{matchedBrand && } { + setInputValue(""); + setSearchKeyword(""); + }; + return { inputValue, searchKeyword, matchedBrand, handleChange, handleSubmit, + clearSearchKeyword, }; }; diff --git a/src/server/hono/schemas/search.schema.ts b/src/server/hono/schemas/search.schema.ts index a6ffca9b..f537ac5c 100644 --- a/src/server/hono/schemas/search.schema.ts +++ b/src/server/hono/schemas/search.schema.ts @@ -13,8 +13,23 @@ import { * 검색 GET 요청 쿼리 파라미터 스키마 * @description 기본 검색 요청 시 사용되는 쿼리 파라미터 (검색어, 페이지네이션) */ +export const GenderSortSchema = z + .enum(["MASCULINE", "UNISEX", "FEMININE"]) + .default("UNISEX"); + +export type GenderSort = z.infer; + export const SearchGetQuerySchema = CursorPaginationSchema.extend({ searchText: z.string().optional().default(""), + // 커서 형식: "{gender_count}:{priority}:{perfume_id}" + cursor: z + .string() + .regex( + /^\d+:\d+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + "유효하지 않은 커서 형식입니다." + ) + .optional(), + genderSort: GenderSortSchema.optional(), }); /** @@ -49,6 +64,7 @@ export const SupabasePerfumeSchema = z.object({ brand_url: z.string().nullable(), image_url: z.string(), priority: z.number().int(), + gender_vote_count: z.number().int(), }); export type SearchGetQuery = z.infer; diff --git a/src/server/hono/services/__tests__/helpers/search.test.helpers.ts b/src/server/hono/services/__tests__/helpers/search.test.helpers.ts index b8d28991..3cbc1e87 100644 --- a/src/server/hono/services/__tests__/helpers/search.test.helpers.ts +++ b/src/server/hono/services/__tests__/helpers/search.test.helpers.ts @@ -15,6 +15,7 @@ export const getTestData = () => { brand_url: "test-brand", image_url: "https://example.com/perfume.jpg", priority: 100, + gender_vote_count: 0, }; const mockSupabasePerfumes = (count: number): SupabasePerfume[] => @@ -28,6 +29,7 @@ export const getTestData = () => { brand_url: "test-brand", image_url: `https://example.com/perfume-${i}.jpg`, priority: 100 - i, + gender_vote_count: 0, })); return { diff --git a/src/server/hono/services/__tests__/search.service.test.ts b/src/server/hono/services/__tests__/search.service.test.ts index 47dc495f..2c5dc649 100644 --- a/src/server/hono/services/__tests__/search.service.test.ts +++ b/src/server/hono/services/__tests__/search.service.test.ts @@ -216,6 +216,93 @@ describe("Search Service", () => { }) ); }); + it("notesFilter만 있는 POST 요청에서 필터가 올바르게 전달되어야 한다", async () => { + const { mockSupabasePerfumes } = getTestData(); + + const mockRpcResponse = { + data: mockSupabasePerfumes(5), + error: null, + }; + + const mockTotalResponse = { + data: 5, + error: null, + }; + + vi.mocked(supabase.rpc).mockImplementation((funcName: string) => { + if (funcName === "search_perfumes") { + return Promise.resolve(mockRpcResponse) as never; + } + if (funcName === "search_perfumes_total") { + return Promise.resolve(mockTotalResponse) as never; + } + return Promise.resolve({ data: null, error: null }) as never; + }); + + const noteId = "323e4567-e89b-12d3-a456-426614174002"; + + const postParams: SearchPostBody = { + searchText: "test", + notesFilter: [noteId], + limit: 15, + cursor: undefined, + }; + + await searchPerfumesService(postParams); + + expect(supabase.rpc).toHaveBeenCalledWith( + "search_perfumes", + expect.objectContaining({ + notes_filter: [noteId], + brand_filter: null, + accords_filter: null, + }) + ); + }); + + it("accordsFilter만 있는 POST 요청에서 필터가 올바르게 전달되어야 한다", async () => { + const { mockSupabasePerfumes } = getTestData(); + + const mockRpcResponse = { + data: mockSupabasePerfumes(5), + error: null, + }; + + const mockTotalResponse = { + data: 5, + error: null, + }; + + vi.mocked(supabase.rpc).mockImplementation((funcName: string) => { + if (funcName === "search_perfumes") { + return Promise.resolve(mockRpcResponse) as never; + } + if (funcName === "search_perfumes_total") { + return Promise.resolve(mockTotalResponse) as never; + } + return Promise.resolve({ data: null, error: null }) as never; + }); + + const accordId = "423e4567-e89b-12d3-a456-426614174003"; + + const postParams: SearchPostBody = { + searchText: "test", + accordsFilter: [accordId], + limit: 15, + cursor: undefined, + }; + + await searchPerfumesService(postParams); + + expect(supabase.rpc).toHaveBeenCalledWith( + "search_perfumes", + expect.objectContaining({ + accords_filter: [accordId], + brand_filter: null, + notes_filter: null, + }) + ); + }); }); describe("searchPerfumesService - 페이지네이션", () => { @@ -324,7 +411,8 @@ describe("Search Service", () => { return Promise.resolve({ data: null, error: null }) as never; }); - const cursor = "perfume-10"; + // 복합 커서 형식: "{gender_count}:{priority}:{perfume_id}" + const cursor = "5:90:perfume-10"; const params: SearchGetQuery = { searchText: "test", limit: 15, @@ -336,7 +424,9 @@ describe("Search Service", () => { expect(supabase.rpc).toHaveBeenCalledWith( "search_perfumes", expect.objectContaining({ - last_seen_id: cursor, + last_seen_id: "perfume-10", + last_seen_priority: 90, + last_seen_gender_count: 5, }) ); }); @@ -375,8 +465,13 @@ describe("Search Service", () => { expect(result.success).toBe(true); if (result.success) { // 16개가 반환되었으므로 hasMore = true - // nextCursor는 15번째(마지막 반환 데이터)의 id - expect(result.data.nextCursor).toBe("perfume-14"); + // nextCursor는 15번째(마지막 반환 데이터)의 "{gender_count}:{priority}:{perfume_id}" + // mockSupabasePerfumes(16)에서 index 14 → gender_vote_count: 0, priority: 86, perfume_id: "perfume-14" + const [cursorGenderCount, cursorPriority, cursorId] = result.data.nextCursor!.split(":"); + expect(result.data.nextCursor).toMatch(/^\d+:\d+:[a-z0-9-]+$/); + expect(cursorGenderCount).toBe("0"); + expect(cursorPriority).toBe("86"); + expect(cursorId).toBe("perfume-14"); expect(result.data.data).toHaveLength(15); } }); diff --git a/src/server/hono/services/search.service.ts b/src/server/hono/services/search.service.ts index b29450a7..274118d7 100644 --- a/src/server/hono/services/search.service.ts +++ b/src/server/hono/services/search.service.ts @@ -11,10 +11,8 @@ import { ServiceResult, serviceSuccess, } from "@/server/result"; -import { createCursorPaginationResult } from "../utils/pagination.utils"; - function transformSupabasePerfume( - p: SupabasePerfume + p: SupabasePerfume, ): ApiPerfumeSimpleResponse { return { id: p.perfume_id, @@ -33,9 +31,12 @@ function transformSupabasePerfume( // 필터 처리 함수 function processFilters(params: SearchGetQuery | SearchPostBody) { - const isPost = "searchText" in params; + const hasFilters = + "brandFilter" in params || + "notesFilter" in params || + "accordsFilter" in params; - if (!isPost) { + if (!hasFilters) { return { brandFilter: null, notesFilter: null, @@ -71,24 +72,33 @@ export async function searchPerfumesService( ): Promise> { try { const limit = params.limit ?? 15; - const cursor = params.cursor; const fetchLimit = limit + 1; + // 복합 커서 파싱 (형식: "{gender_count}:{priority}:{perfume_id}") + const [lastGenderCount, lastPriority, lastId] = params.cursor + ? params.cursor.split(":") + : [null, null, null]; + // 필터 처리 const { brandFilter, notesFilter, accordsFilter } = processFilters(params); + const genderSort = params.genderSort ?? "UNISEX"; + // Supabase RPC 함수가 예상하는 형태로 변환 const rpcParams = { - search_text: params.searchText || "", + search_text: params.searchText || null, brand_filter: brandFilter, notes_filter: notesFilter, accords_filter: accordsFilter, - last_seen_id: cursor ?? null, + last_seen_id: lastId ?? null, + last_seen_priority: lastPriority ? Number(lastPriority) : null, + last_seen_gender_count: lastGenderCount ? Number(lastGenderCount) : null, + gender_sort: genderSort, result_limit: fetchLimit, }; const rpcParamsTotal = { - search_text: params.searchText || "", + search_text: params.searchText || null, brand_filter: brandFilter, notes_filter: notesFilter, accords_filter: accordsFilter, @@ -111,17 +121,18 @@ export async function searchPerfumesService( const rawData: SupabasePerfume[] = perfumesResult.data || []; const totalCount: number = totalResult.data || 0; - const transformedData = rawData.map(transformSupabasePerfume); - const paginatedResult = createCursorPaginationResult( - transformedData, - totalCount, - limit - ); + const hasMore = rawData.length > limit; + const slicedRaw = hasMore ? rawData.slice(0, limit) : rawData; + const lastItem = slicedRaw[slicedRaw.length - 1]; + const nextCursor = + hasMore && lastItem + ? `${lastItem.gender_vote_count}:${lastItem.priority}:${lastItem.perfume_id.toLowerCase()}` + : null; return serviceSuccess({ - data: paginatedResult.data, - totalCount: paginatedResult.totalCount, - nextCursor: paginatedResult.nextCursor, + data: slicedRaw.map(transformSupabasePerfume), + totalCount, + nextCursor, }); } catch (error) { return serviceInternalError(error); diff --git a/src/shared/types/search.types.ts b/src/shared/types/search.types.ts index a22b773b..5b30ffba 100644 --- a/src/shared/types/search.types.ts +++ b/src/shared/types/search.types.ts @@ -1,7 +1,10 @@ +export type GenderSort = "MASCULINE" | "UNISEX" | "FEMININE"; + export interface SearchPerfumesParams { searchText?: string; cursor?: string; limit?: number; + genderSort?: GenderSort; } export interface SearchPerfumesWithFiltersParams extends SearchPerfumesParams { From a343b2447789a1ec7119b0b431c9c4d504336d56 Mon Sep 17 00:00:00 2001 From: kuan Date: Tue, 7 Apr 2026 01:10:17 +0900 Subject: [PATCH 13/39] =?UTF-8?q?feat:=20=EC=9C=84=EC=B9=98=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=A7=A4=EC=9E=A5=20=EC=B0=BE=EA=B8=B0=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B2=84=20->=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 2 +- prisma/schema.prisma | 6 +- src/app/brand/[name]/page.tsx | 25 +- src/app/brand/layout.tsx | 17 + src/client/stores/perfumeStore.ts | 20 +- src/client/utils/api/brands.api.ts | 26 + .../perfumeList/section/BrandSection.tsx | 49 +- src/components/commons/search/SearchBar.tsx | 2 + src/components/domains/brandDetail/Tabs.tsx | 98 + .../domains/brandDetail/image/index.tsx | 3 - .../domains/brandDetail/info/index.tsx | 61 - .../domains/brandDetail/map/StorePanel.tsx | 84 + .../domains/brandDetail/map/index.tsx | 447 +- .../domains/brandDetail/map/useBrandMap.tsx | 173 + .../brandDetail/map/useNearbyStores.ts | 56 + .../brandDetail/perfumes/PageClient.tsx | 3 +- .../domains/brandDetail/title/index.tsx | 13 + .../domains/perfumes/PageClient.tsx | 4 +- .../perfumes/hook/usePerfumeSearchState.ts | 18 +- src/server/hono/routes/v1/brand.handler.ts | 26 +- src/server/hono/schemas/brand.schema.ts | 1 + .../services/__tests__/brand.service.test.ts | 246 +- src/server/hono/services/brand.service.ts | 169 +- src/shared/types/kakao.d.ts | 72 + tsconfig.json | 1 + yarn.lock | 5024 +++++++---------- 26 files changed, 2879 insertions(+), 3767 deletions(-) create mode 100644 src/app/brand/layout.tsx create mode 100644 src/components/domains/brandDetail/Tabs.tsx delete mode 100644 src/components/domains/brandDetail/info/index.tsx create mode 100644 src/components/domains/brandDetail/map/StorePanel.tsx create mode 100644 src/components/domains/brandDetail/map/useBrandMap.tsx create mode 100644 src/components/domains/brandDetail/map/useNearbyStores.ts create mode 100644 src/components/domains/brandDetail/title/index.tsx create mode 100644 src/shared/types/kakao.d.ts diff --git a/next.config.ts b/next.config.ts index 898d5463..d891626e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -124,7 +124,7 @@ const nextConfig: NextConfig = { }, { key: "Permissions-Policy", - value: "camera=(), microphone=(), geolocation=()", + value: "camera=(), microphone=(), geolocation=(self)", }, ], }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3a0e7f3e..d538507e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -5,9 +5,9 @@ generator client { generator zod { provider = "zod-prisma-types" output = "./zod" - createInputTypes = "false" - createModelSchema = "true" - useMultipleFiles = "true" + createInputTypes = false + createModelSchema = true + useMultipleFiles = true } datasource db { diff --git a/src/app/brand/[name]/page.tsx b/src/app/brand/[name]/page.tsx index abd53069..bdb4bef2 100644 --- a/src/app/brand/[name]/page.tsx +++ b/src/app/brand/[name]/page.tsx @@ -1,15 +1,14 @@ import { cache } from "react"; import type { Metadata } from "next"; +import { notFound } from "next/navigation"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; import getQueryClient from "@/client/utils/getQueryClient"; import { brandApi } from "@/client/utils/api/brands.api"; import { perfumeApi } from "@/client/utils/api/perfumes.api"; -import { BrandDetailImage } from "@/components/domains/brandDetail/image"; -import { BrandDetailInfo } from "@/components/domains/brandDetail/info"; +import { BrandDetailTitle } from "@/components/domains/brandDetail/title"; import { PageClient } from "@/components/domains/brandDetail/perfumes/PageClient"; import { BrandDetailSearchBar } from "@/components/domains/brandDetail/searchBar"; -import { notFound } from "next/navigation"; -import { BrandMap } from "@/components/domains/brandDetail/map"; +import BrandDetailTabs from "@/components/domains/brandDetail/Tabs"; import { generateSeo } from "@/shared/utils/generateSeo"; type Props = { @@ -85,24 +84,26 @@ export default async function BrandDetailPage({ ]); const brandUrl = brandData.brandUrl; - const brandStores = `https://map.naver.com/p/search/${brandData.nameKo}`; const brandImages = { - src: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=1200&h=400&q=80", - alt: "1", + src: + brandData.imageUrl ?? + "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=1200&h=400&q=80", + alt: brandData.nameKo ?? brandData.nameEn, }; return (
- + - -
diff --git a/src/app/brand/layout.tsx b/src/app/brand/layout.tsx new file mode 100644 index 00000000..f35534f5 --- /dev/null +++ b/src/app/brand/layout.tsx @@ -0,0 +1,17 @@ +import Script from "next/script"; + +export default function BrandLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + {children} +