From 9a2f413b1bad2456c7fc7691300e6e83e04ac68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=BD=EC=A7=80=EC=9A=B1=28Hanji=29?= <99489686+gwagjiug@users.noreply.github.com> Date: Sat, 29 Nov 2025 01:15:58 +0900 Subject: [PATCH 1/3] Fix(client): Encountered two children with the same key Error fix (#715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: update slide key generation and add typeId to SlideData * fix: ci 에러 해결 --- .../performance-carousel/components/performance-carousel.tsx | 4 ++-- .../performance-carousel/types/performance-carousel-types.ts | 1 + .../components/performance-carousel/utils/slide-transform.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/pages/home/components/performance-carousel/components/performance-carousel.tsx b/apps/client/src/pages/home/components/performance-carousel/components/performance-carousel.tsx index 464d8669..9b741d0d 100644 --- a/apps/client/src/pages/home/components/performance-carousel/components/performance-carousel.tsx +++ b/apps/client/src/pages/home/components/performance-carousel/components/performance-carousel.tsx @@ -26,9 +26,9 @@ const PerformanceCarousel = ({ return (
- {visibleSlides.map((slide) => ( + {visibleSlides.map((slide, index) => ( Date: Sat, 29 Nov 2025 01:59:35 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Refactor(client):=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=ED=8E=98=EC=9D=B4=EC=A7=80=20v2=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B0=8F=20API?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=20(#692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 아티스트 선택 및 완료 단계에서 버튼 추가 및 상태 관리 개선 * refactor: 아티스트 선택 컴포넌트에서 관련 아티스트 조회 방식 개선 * fix: 온보딩 페이지에서 TOP 아티스트 데이터 로드 실패 시 오류 처리 추가 * feat: 아티스트 선택 컴포넌트에 애니메이션 효과 추가 및 스타일 수정 * fix: 아바타 그리드 섹션의 오버플로우 스타일 수정 * fix: 아바타 그리드 섹션에 패딩 추가 * fix: 아티스트 선택 컴포넌트에서 검색 바와 아바타 그리드 섹션의 렌더링 로직 개선 * feat: 아티스트 선택 컴포넌트에 선택된 아티스트 미리보기 섹션 추가 및 관련 스타일 개선 * feat: 아티스트 선택 컴포넌트에서 아티스트 검색 로직 개선 및 관련 API 수정 * feat: 아티스트 선택 컴포넌트에 중첩된 아티스트 선택 및 검색 기능 추가, 관련 스타일 및 API 수정 * fix: 아티스트 선택 컴포넌트에서 완료 버튼의 비활성화 조건 수정 및 애니메이션 효과 개선 * feat: 아티스트 선택 컴포넌트에 선택된 아티스트를 업데이트하는 뮤테이션 추가 및 쿼리 무효화 로직 구현 * fix: update artist selection logic and disable navigation guard * refactor: update artist selection components to use search params and improve query handling --- .../components/artist-search.css.ts | 1 - .../onboarding/components/artist-search.tsx | 7 +- .../artist-select-nested-delete.css.ts | 84 ++++++++ .../artist-select-nested-delete.tsx | 101 +++++++++ .../artist-select-nested-search.tsx | 88 ++++++++ .../artist-select-nested-select.tsx | 193 ++++++++++++++++++ .../components/artist-select.css.ts | 41 +++- .../onboarding/components/artist-select.tsx | 96 ++++----- .../components/onboarding-chip.css.ts | 20 ++ .../onboarding/components/onboarding-chip.tsx | 19 ++ .../components/onboarding-complete.css.ts | 4 + .../components/onboarding-complete.tsx | 19 +- .../onboarding/hooks/use-onboard-mutation.ts | 40 ---- .../src/pages/onboarding/page/onboarding.tsx | 71 +------ .../shared/apis/onboard/onboard-mutation.ts | 13 -- .../client/src/shared/apis/onboard/queries.ts | 86 ++++++-- .../components/layout/detail-header.tsx | 14 +- apps/client/src/shared/constants/api.ts | 12 +- .../src/shared/constants/mutation-key.ts | 10 + apps/client/src/shared/constants/query-key.ts | 9 +- .../src/shared/hooks/use-debounce-keyword.ts | 9 + .../src/shared/types/onboard-response.ts | 4 + .../src/components/avatar/avatar.css.ts | 12 ++ .../src/components/avatar/avatar.tsx | 2 +- .../src/components/search-bar/search-bar.tsx | 5 +- .../search-suggestion-list.css.ts | 2 +- .../search-suggestion-list.tsx | 19 +- 27 files changed, 741 insertions(+), 240 deletions(-) create mode 100644 apps/client/src/pages/onboarding/components/artist-select-nested-delete.css.ts create mode 100644 apps/client/src/pages/onboarding/components/artist-select-nested-delete.tsx create mode 100644 apps/client/src/pages/onboarding/components/artist-select-nested-search.tsx create mode 100644 apps/client/src/pages/onboarding/components/artist-select-nested-select.tsx create mode 100644 apps/client/src/pages/onboarding/components/onboarding-chip.css.ts create mode 100644 apps/client/src/pages/onboarding/components/onboarding-chip.tsx diff --git a/apps/client/src/pages/onboarding/components/artist-search.css.ts b/apps/client/src/pages/onboarding/components/artist-search.css.ts index dcc3dfd8..253555bb 100644 --- a/apps/client/src/pages/onboarding/components/artist-search.css.ts +++ b/apps/client/src/pages/onboarding/components/artist-search.css.ts @@ -4,7 +4,6 @@ import { themeVars } from '@confeti/design-system/styles'; export const searchBarContainer = style({ ...themeVars.display.flexJustifyAlignCenter, - padding: '0.8rem 2rem', width: '100%', gap: '0.8rem', backgroundColor: themeVars.color.white, diff --git a/apps/client/src/pages/onboarding/components/artist-search.tsx b/apps/client/src/pages/onboarding/components/artist-search.tsx index 5218b003..9e9c182f 100644 --- a/apps/client/src/pages/onboarding/components/artist-search.tsx +++ b/apps/client/src/pages/onboarding/components/artist-search.tsx @@ -11,13 +11,9 @@ import * as styles from './artist-search.css'; interface ArtistSearchProps { onArtistSelect: (artistId: string) => void; - handleSearchParams: () => void; } -const ArtistSearch = ({ - onArtistSelect, - handleSearchParams, -}: ArtistSearchProps) => { +const ArtistSearch = ({ onArtistSelect }: ArtistSearchProps) => { const { keyword, debouncedKeyword, handleInputChange } = useDebouncedKeyword(); @@ -52,7 +48,6 @@ const ArtistSearch = ({ profileUrl: artist.profileUrl, }))} onSelectArtistId={onArtistSelect} - handleSearchParams={handleSearchParams} /> ) : (
diff --git a/apps/client/src/pages/onboarding/components/artist-select-nested-delete.css.ts b/apps/client/src/pages/onboarding/components/artist-select-nested-delete.css.ts new file mode 100644 index 00000000..58a1ef07 --- /dev/null +++ b/apps/client/src/pages/onboarding/components/artist-select-nested-delete.css.ts @@ -0,0 +1,84 @@ +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +import { themeVars } from '@confeti/design-system/styles'; + +export const wrapper = style({ + display: 'flex', + flexDirection: 'column', + height: '100vh', +}); + +export const container = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '2rem', + padding: '2rem', + flex: 1, + overflowY: 'auto', +}); + +export const selectedArtistItem = style({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + maxHeight: '3rem', +}); + +export const selectedArtistItemContent = style({ + display: 'flex', + alignItems: 'center', + gap: '1.5rem', +}); + +export const artistName = style({ + display: 'webkit-box', + overflow: 'hidden', + WebkitLineClamp: 1, + WebkitBoxOrient: 'vertical', + + ...themeVars.fontStyles.body2_r_15, + color: themeVars.color.black, +}); + +export const checkbox = recipe({ + base: { + display: 'flex', + width: '2.4rem', + height: '2.4rem', + justifyContent: 'center', + alignItems: 'center', + flexShrink: 0, + aspectRatio: '1/1', + borderRadius: '30px', + cursor: 'pointer', + transition: 'all 0.2s ease-in-out', + }, + variants: { + checked: { + true: { + border: 'none', + background: '#B5F602', + }, + false: { + border: '1px solid #C5C6CB', + background: '#F9FAFE', + }, + }, + }, + defaultVariants: { + checked: false, + }, +}); + +export const confirmButtonWrapper = style({ + display: 'flex', + padding: '2rem', + width: '100%', + position: 'sticky', + bottom: 0, + background: + 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)', +}); diff --git a/apps/client/src/pages/onboarding/components/artist-select-nested-delete.tsx b/apps/client/src/pages/onboarding/components/artist-select-nested-delete.tsx new file mode 100644 index 00000000..1ee0c3b5 --- /dev/null +++ b/apps/client/src/pages/onboarding/components/artist-select-nested-delete.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; +import { useSearchParams } from 'react-router-dom'; + +import { Avatar, Button } from '@confeti/design-system'; +import { Icon } from '@confeti/design-system/icon'; + +import { + ONBOARD_MUTATION_OPTIONS, + ONBOARD_QUERY_OPTIONS, +} from '@shared/apis/onboard/queries'; +import { DetailHeader } from '@shared/components'; +import { ONBOARD_QUERY_KEY } from '@shared/constants/query-key'; + +import * as styles from './artist-select-nested-delete.css'; + +interface ArtistSelectNestedDeleteProps { + onBack: () => void; +} + +const ArtistSelectNestedDelete = ({ + onBack, +}: ArtistSelectNestedDeleteProps) => { + const [_searchParams, setSearchParams] = useSearchParams(); + const queryClient = useQueryClient(); + const { data: selectedArtistData } = useSuspenseQuery({ + ...ONBOARD_QUERY_OPTIONS.SELECTED_ARTIST(), + }); + const { mutate: mutatePatchSelectedArtist } = useMutation({ + ...ONBOARD_MUTATION_OPTIONS.PATCH_SELECTED_ARTIST(), + }); + const [checkedIds, setCheckedIds] = useState([]); + + const handleCheckboxClick = (artistId: string) => { + setCheckedIds((prev) => { + if (prev.includes(artistId)) { + return prev.filter((id) => id !== artistId); + } else { + return [...prev, artistId]; + } + }); + }; + + const handleCompleteClick = () => { + mutatePatchSelectedArtist(checkedIds, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ONBOARD_QUERY_KEY.SELECTED_ARTIST(), + }); + setSearchParams({}); + }, + }); + }; + + return ( +
+ +
+ {selectedArtistData.data.artists.map((artist) => ( +
handleCheckboxClick(artist.artistId)} + > +
+ +

{artist.name}

+
+
+ +
+
+ ))} +
+
+
+
+ ); +}; + +export default ArtistSelectNestedDelete; diff --git a/apps/client/src/pages/onboarding/components/artist-select-nested-search.tsx b/apps/client/src/pages/onboarding/components/artist-select-nested-search.tsx new file mode 100644 index 00000000..d6632b98 --- /dev/null +++ b/apps/client/src/pages/onboarding/components/artist-select-nested-search.tsx @@ -0,0 +1,88 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useSearchParams } from 'react-router-dom'; + +import { SearchBar, SearchSuggestionList } from '@confeti/design-system'; + +import { + ONBOARD_MUTATION_OPTIONS, + ONBOARD_QUERY_OPTIONS, +} from '@shared/apis/onboard/queries'; +import { ONBOARD_QUERY_KEY } from '@shared/constants/query-key'; +import { useDebouncedKeyword } from '@shared/hooks/use-debounce-keyword'; +import { queryClient } from '@shared/utils/query-client'; + +import { ONBOARD_LIMIT } from '../constants/limit'; + +import * as searchStyles from './artist-search.css'; +import * as styles from './artist-select.css'; + +interface ArtistSelectNestedSearchProps { + onBack: () => void; +} + +const ArtistSelectNestedSearch = ({ + onBack, +}: ArtistSelectNestedSearchProps) => { + const { keyword, debouncedKeyword, handleInputChange } = + useDebouncedKeyword(); + const [_searchParams, setSearchParams] = useSearchParams(); + + const { data: relatedKeywordsData } = useQuery({ + ...ONBOARD_QUERY_OPTIONS.ARTIST_RELATED_KEYWORDS( + debouncedKeyword, + ONBOARD_LIMIT.RELATED_KEYWORD, + ), + enabled: !!debouncedKeyword.trim(), + }); + + const { mutate: mutateSelectedArtist } = useMutation({ + ...ONBOARD_MUTATION_OPTIONS.SELECTED_ARTIST(), + }); + + const hasArtistResults = (relatedKeywordsData?.artists?.length ?? 0) > 0; + + const handleArtistSelect = (artistId: string) => { + mutateSelectedArtist([artistId], { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ONBOARD_QUERY_KEY.SELECTED_ARTIST(), + }); + setSearchParams({ artist: artistId }); + }, + }); + }; + + return ( +
+
+
+ +
+
+ {hasArtistResults ? ( + ({ + id: artist.artistId, + title: artist.name, + profileUrl: artist.profileUrl, + }))} + onSelectArtistId={handleArtistSelect} + /> + ) : ( +
+

+ 선호하는 아티스트를 검색해보세요 +

+
+ )} +
+ ); +}; + +export default ArtistSelectNestedSearch; diff --git a/apps/client/src/pages/onboarding/components/artist-select-nested-select.tsx b/apps/client/src/pages/onboarding/components/artist-select-nested-select.tsx new file mode 100644 index 00000000..2d4a6183 --- /dev/null +++ b/apps/client/src/pages/onboarding/components/artist-select-nested-select.tsx @@ -0,0 +1,193 @@ +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; +import { AnimatePresence, LayoutGroup, motion } from 'motion/react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +import { + Avatar, + Button, + Description, + SearchBar, + toast, +} from '@confeti/design-system'; +import { Icon } from '@confeti/design-system/icon'; + +import { + ONBOARD_MUTATION_OPTIONS, + ONBOARD_QUERY_OPTIONS, +} from '@shared/apis/onboard/queries'; +import { ONBOARD_QUERY_KEY } from '@shared/constants/query-key'; +import { routePath } from '@shared/router/path'; + +import { ONBOARD_LIMIT } from '../constants/limit'; +import OnboardingChip from './onboarding-chip'; + +import * as styles from './artist-select.css'; + +interface ArtistSelectNestedSelectProps { + onSearchFocus: () => void; + onEditClick: () => void; + onNextClick: () => void; +} + +const ArtistSelectNestedSelect = ({ + onSearchFocus, + onEditClick, + onNextClick, +}: ArtistSelectNestedSelectProps) => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [searchParams, setSearchParams] = useSearchParams(); + const targetArtistId = searchParams.get('artist'); + + const { data: artistData } = useSuspenseQuery({ + ...ONBOARD_QUERY_OPTIONS.TOP_ARTIST( + ONBOARD_LIMIT.TOP_ARTIST, + targetArtistId, + ), + }); + + const { data: selectedArtistData } = useSuspenseQuery({ + ...ONBOARD_QUERY_OPTIONS.SELECTED_ARTIST(), + }); + + const { mutate: mutateAuthOnboard } = useMutation({ + ...ONBOARD_MUTATION_OPTIONS.AUTH_ONBOARD(), + }); + + const { mutate: mutateSelectedArtist } = useMutation({ + ...ONBOARD_MUTATION_OPTIONS.SELECTED_ARTIST(), + }); + + if (!artistData) { + throw new Error( + '아티스트 데이터를 불러오는 중 오류가 발생했어요. 다시 시도해주세요.', + ); + } + + const handleArtistClick = (artistId: string) => { + mutateSelectedArtist([artistId], { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ONBOARD_QUERY_KEY.SELECTED_ARTIST(), + }); + setSearchParams({ artist: artistId }); + }, + }); + }; + + const handleNextClick = () => { + mutateAuthOnboard( + selectedArtistData.data.artists.map((artist) => artist.artistId), + { + onSuccess: () => { + onNextClick(); + }, + onError: () => { + toast({ + text: '온보딩 처리 중 오류가 발생했어요. 다시 시도해주세요.', + icon: , + }); + navigate(routePath.ROOT); + }, + }, + ); + }; + + return ( +
+ +
+ +
+
+
+ +
+ + {selectedArtistData.data.artists.map((artist) => ( + + + + ))} + +
+ + {selectedArtistData.data.artists.length > 0 && ( + + + + )} + +
+
+
+
+ {artistData.artists.map((artist, index) => ( + + + handleArtistClick(artist.artistId)} + /> + +

{artist.name}

+
+ ))} +
+
+ ); +}; + +export default ArtistSelectNestedSelect; diff --git a/apps/client/src/pages/onboarding/components/artist-select.css.ts b/apps/client/src/pages/onboarding/components/artist-select.css.ts index cb566419..1ce0a0f1 100644 --- a/apps/client/src/pages/onboarding/components/artist-select.css.ts +++ b/apps/client/src/pages/onboarding/components/artist-select.css.ts @@ -1,12 +1,7 @@ -import { keyframes, style } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; import { themeVars } from '@confeti/design-system/styles'; -const fadeInScale = keyframes({ - '0%': { opacity: 0, transform: 'scale(0.5)' }, - '100%': { opacity: 1, transform: 'scale(1)' }, -}); - export const onboardingContentSection = style({ height: `100dvh`, padding: '2rem', @@ -19,8 +14,37 @@ export const searchBarSection = style({ marginBottom: '2.4rem', }); +export const selectedArtistPriviewSection = style({ + display: 'flex', + padding: '2rem 0', + flexDirection: 'column', + alignItems: 'flex-start', +}); + +export const selectedArtistPreview = style({ + display: 'flex', + alignItems: 'center', + width: '100%', + gap: '0', +}); + +export const selectedArtistList = style({ + display: 'flex', + alignItems: 'center', + overflowX: 'hidden', +}); + +export const selectedArtistItem = style({ + selectors: { + '&:not(:first-child)': { + margin: '0 0 0 -1.6rem', + }, + }, +}); + export const avatarGridSection = style({ width: '100%', + padding: '2rem 0 0 0 ', overflowY: 'scroll', display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', @@ -35,7 +59,6 @@ export const avatar = style({ ...themeVars.display.flexColumn, alignItems: 'center', gap: '1.2rem', - animation: `${fadeInScale} 0.8s cubic-bezier(0, 0.71, 0.2, 1.01) 0.5s both`, }); export const artistName = style({ @@ -48,3 +71,7 @@ export const artistName = style({ textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); + +export const button = style({ + flexShrink: 0, +}); diff --git a/apps/client/src/pages/onboarding/components/artist-select.tsx b/apps/client/src/pages/onboarding/components/artist-select.tsx index fd4a50f7..4ec42fda 100644 --- a/apps/client/src/pages/onboarding/components/artist-select.tsx +++ b/apps/client/src/pages/onboarding/components/artist-select.tsx @@ -1,74 +1,52 @@ -import { ReactNode } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { useState } from 'react'; -import { Avatar, Description, SearchBar } from '@confeti/design-system'; +import SwitchCase from '@shared/components/switch-case'; -import { onboard } from '@shared/types/onboard-response'; +import ArtistSelectNestedDelete from './artist-select-nested-delete'; +import ArtistSelectNestedSearch from './artist-select-nested-search'; +import ArtistSelectNestedSelect from './artist-select-nested-select'; -import ArtistSearch from './artist-search'; +type ViewState = 'select' | 'search' | 'edit'; -import * as styles from './artist-select.css'; - -interface artistSelectProps { - artists: onboard[] | undefined; - onArtistSelect: (artistId: string) => void; - children: ReactNode; +interface ArtistSelectProps { + setStep: (step: number) => void; } -const ArtistSelect = ({ - children, - artists, - onArtistSelect, -}: artistSelectProps) => { - const [searchParams, setSearchParams] = useSearchParams(); - const isFocused = searchParams.get('search') === 'true'; +const ArtistSelect = ({ setStep }: ArtistSelectProps) => { + const [viewState, setViewState] = useState('select'); + + const handleSearchFocus = () => { + setViewState('search'); + }; + + const handleEditClick = () => { + setViewState('edit'); + }; - const handleSearchBarFocus = () => { - setSearchParams({ search: 'true' }); + const handleBackToSelect = () => { + setViewState('select'); }; - const handleSearchArtistSelect = () => { - setSearchParams({}); + const handleNextClick = () => { + setStep(1); }; - if (isFocused) { - return ( - - ); - } else { - return ( -
- -
- , + edit: () => , + select: () => ( + -
-
- {artists?.map((artist) => ( -
- onArtistSelect(artist?.artistId)} - /> -

{artist.name}

-
- ))} -
- {children} -
- ); - } + ), + }} + /> + ); }; export default ArtistSelect; diff --git a/apps/client/src/pages/onboarding/components/onboarding-chip.css.ts b/apps/client/src/pages/onboarding/components/onboarding-chip.css.ts new file mode 100644 index 00000000..c40632e4 --- /dev/null +++ b/apps/client/src/pages/onboarding/components/onboarding-chip.css.ts @@ -0,0 +1,20 @@ +import { style } from '@vanilla-extract/css'; + +import { themeVars } from '@confeti/design-system/styles'; + +export const onboardingCountChipContainer = style({ + display: 'inline-flex', + padding: '0.6rem 1.4rem', + justifyContent: 'center', + alignItems: 'center', + gap: '0.4rem', + borderRadius: '5rem', + backgroundColor: themeVars.color.gray900, + backdropFilter: 'blur(2px)', + cursor: 'pointer', +}); + +export const onboardingCountChip = style({ + ...themeVars.fontStyles.title2_b_20, + color: themeVars.color.confeti_lime, +}); diff --git a/apps/client/src/pages/onboarding/components/onboarding-chip.tsx b/apps/client/src/pages/onboarding/components/onboarding-chip.tsx new file mode 100644 index 00000000..617f43d9 --- /dev/null +++ b/apps/client/src/pages/onboarding/components/onboarding-chip.tsx @@ -0,0 +1,19 @@ +import { Icon } from '@confeti/design-system/icon'; + +import * as styles from './onboarding-chip.css'; + +interface Props { + count: number; + onClick: () => void; +} + +const OnboardingChip = ({ count, onClick }: Props) => { + return ( +
+

{count}

+ +
+ ); +}; + +export default OnboardingChip; diff --git a/apps/client/src/pages/onboarding/components/onboarding-complete.css.ts b/apps/client/src/pages/onboarding/components/onboarding-complete.css.ts index 87a71f62..29e5d9f1 100644 --- a/apps/client/src/pages/onboarding/components/onboarding-complete.css.ts +++ b/apps/client/src/pages/onboarding/components/onboarding-complete.css.ts @@ -33,3 +33,7 @@ export const logoImage = style({ width: '18rem', height: '18rem', }); + +export const button = style({ + flexShrink: 0, +}); diff --git a/apps/client/src/pages/onboarding/components/onboarding-complete.tsx b/apps/client/src/pages/onboarding/components/onboarding-complete.tsx index 4742251b..23fdc41f 100644 --- a/apps/client/src/pages/onboarding/components/onboarding-complete.tsx +++ b/apps/client/src/pages/onboarding/components/onboarding-complete.tsx @@ -1,7 +1,7 @@ -import { ReactNode, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { AnimatePresence, motion } from 'motion/react'; -import { Description } from '@confeti/design-system'; +import { Button, Description } from '@confeti/design-system'; import { SwitchCase } from '@shared/components'; import Loading from '@shared/pages/loading/loading'; @@ -9,7 +9,7 @@ import Loading from '@shared/pages/loading/loading'; import * as styles from './onboarding-complete.css'; interface OnBoardingCompleteProps { - children: ReactNode; + setStep: (step: number) => void; } type Phase = 'loading' | 'description' | 'cta'; @@ -45,7 +45,7 @@ const DescriptionContent = () => ( ); -const CtaContent = ({ children }: { children: ReactNode }) => ( +const CtaContent = ({ setStep }: OnBoardingCompleteProps) => ( ( animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, delay: 0.5 }} > - {children} +
@@ -100,7 +105,7 @@ const CtaContent = ({ children }: { children: ReactNode }) => ( * - `exit`: 컴포넌트가 퇴장할 때의 상태입니다. * - `transition`: 애니메이션의 지속시간 및 지연 시간을 설정합니다. */ -const OnBoardingComplete = ({ children }: OnBoardingCompleteProps) => { +const OnBoardingComplete = ({ setStep }: OnBoardingCompleteProps) => { const [phase, setPhase] = useState('loading'); useEffect(() => { @@ -118,7 +123,7 @@ const OnBoardingComplete = ({ children }: OnBoardingCompleteProps) => { caseBy={{ loading: () => , description: () => , - cta: () => {children}, + cta: () => , }} /> diff --git a/apps/client/src/pages/onboarding/hooks/use-onboard-mutation.ts b/apps/client/src/pages/onboarding/hooks/use-onboard-mutation.ts index 78523e4f..e69de29b 100644 --- a/apps/client/src/pages/onboarding/hooks/use-onboard-mutation.ts +++ b/apps/client/src/pages/onboarding/hooks/use-onboard-mutation.ts @@ -1,40 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { authTokenHandler, getRefreshToken } from '@confeti/core/auth'; -import { BaseResponse } from '@confeti/core/http'; - -import { postReissueToken } from '@shared/apis/auth/auth-mutations'; -import { postAuthOnboarding } from '@shared/apis/onboard/onboard-mutation'; -import { getArtistRelatedArtist } from '@shared/apis/onboard/queries'; -import { onboardResponse } from '@shared/types/onboard-response'; - -export const useArtistRelatedArtist = () => { - return useMutation< - BaseResponse, - Error, - { artistId: string; limit: number } - >({ - mutationFn: ({ artistId, limit }) => { - return getArtistRelatedArtist(artistId, limit); - }, - }); -}; - -export const usePostAuthOnboarding = () => { - return useMutation({ - mutationFn: async (favoriteArtists: string[]) => { - try { - await postAuthOnboarding(favoriteArtists); - } catch (error) { - console.log('error', error); - } - - try { - const { data } = await postReissueToken(getRefreshToken()); - authTokenHandler('set', data.accessToken, data.refreshToken); - } catch (error) { - console.log('error', error); - } - }, - }); -}; diff --git a/apps/client/src/pages/onboarding/page/onboarding.tsx b/apps/client/src/pages/onboarding/page/onboarding.tsx index ee17a852..86747622 100644 --- a/apps/client/src/pages/onboarding/page/onboarding.tsx +++ b/apps/client/src/pages/onboarding/page/onboarding.tsx @@ -1,87 +1,20 @@ -import { useState } from 'react'; -import { useSuspenseQuery } from '@tanstack/react-query'; - -import { Button } from '@confeti/design-system'; - -import { ONBOARD_QUERY_OPTIONS } from '@shared/apis/onboard/queries'; import { useFunnel } from '@shared/hooks/use-funnel'; import { routePath } from '@shared/router/path'; -import { onboard } from '@shared/types/onboard-response'; import ArtistSelect from '../components/artist-select'; import OnBoardingComplete from '../components/onboarding-complete'; -import { ONBOARD_LIMIT } from '../constants/limit'; -import { - useArtistRelatedArtist, - usePostAuthOnboarding, -} from '../hooks/use-onboard-mutation'; - -import * as styles from './onboarding.css'; const Onboarding = () => { const TOTAL_STEPS = 2; const { Funnel, Step, setStep } = useFunnel(TOTAL_STEPS, routePath.ROOT); - const { data: topArtistData } = useSuspenseQuery({ - ...ONBOARD_QUERY_OPTIONS.TOP_ARTIST(ONBOARD_LIMIT.TOP_ARTIST), - }); - - const [artists, setArtists] = useState(topArtistData?.artists || []); - const [selectedArtistIds, setSelectedArtistIds] = useState([]); - const { mutateAsync } = useArtistRelatedArtist(); - const { mutate: mutateAuthOnboard } = usePostAuthOnboarding(); - - const handleArtistSelect = async (artistId: string) => { - const updatedArtists = await fetchRelatedArtists( - artistId, - ONBOARD_LIMIT.RELATED_ARTIST, - ); - updateRenderArtists(updatedArtists); - updateSelectedArtistIds(artistId); - }; - - const fetchRelatedArtists = async (artistId: string, limit: number) => { - const relatedArtist = await mutateAsync({ - artistId, - limit, - }); - return relatedArtist?.data?.artists || []; - }; - - const updateRenderArtists = (updatedArtists: onboard[]) => { - setArtists(updatedArtists); - }; - - const updateSelectedArtistIds = (artistId: string) => { - setSelectedArtistIds((prev) => - prev.includes(artistId) ? prev : [...prev, artistId], - ); - }; return ( - -