Skip to content

Commit e4377f8

Browse files
sumi-0011claudesumi
authored
refactor: @egjs/react-flicking → Embla Carousel 마이그레이션 (git-goods#378)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: sumi <sumi@sumiui-MacBookAir.local>
1 parent 5f732be commit e4377f8

10 files changed

Lines changed: 101 additions & 241 deletions

File tree

apps/web/package.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
},
1515
"dependencies": {
1616
"@air/react-drag-to-select": "^5.0.11",
17-
"@gitanimals/ui-tailwind": "workspace:*",
18-
"@egjs/flicking-plugins": "^4.7.1",
19-
"@egjs/react-flicking": "^4.11.3",
2017
"@gitanimals/api": "workspace:*",
2118
"@gitanimals/asset-font": "workspace:*",
2219
"@gitanimals/dayjs": "workspace:*",
@@ -25,6 +22,7 @@
2522
"@gitanimals/react-query": "workspace:*",
2623
"@gitanimals/ui-icon": "workspace:*",
2724
"@gitanimals/ui-panda": "workspace:*",
25+
"@gitanimals/ui-tailwind": "workspace:*",
2826
"@gitanimals/util-common": "workspace:*",
2927
"@next/third-parties": "^14.2.5",
3028
"@octokit/core": "^6.1.2",
@@ -60,9 +58,6 @@
6058
},
6159
"devDependencies": {
6260
"@chromatic-com/storybook": "^1.3.3",
63-
"autoprefixer": "^10.4.21",
64-
"postcss": "^8.5.6",
65-
"tailwindcss": "^3.4.17",
6661
"@gitanimals/eslint-config": "workspace:*",
6762
"@gitanimals/typescript-config": "workspace:*",
6863
"@gitanimals/util-common": "workspace:*",
@@ -87,11 +82,14 @@
8782
"@types/react": "*",
8883
"@types/react-dom": "^18",
8984
"@typescript-eslint/parser": "^7.7.1",
85+
"autoprefixer": "^10.4.21",
9086
"eslint": "^8",
9187
"husky": "^9.0.11",
9288
"lint-staged": "^15.2.2",
89+
"postcss": "^8.5.6",
9390
"prettier": "*",
9491
"storybook": "^8.0.9",
92+
"tailwindcss": "^3.4.17",
9593
"typescript": "*"
9694
}
9795
}

apps/web/src/app/[locale]/event/(common)/BackgroundSlider/BackgroundSlider.tsx

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,45 @@
11
'use client';
22

3-
import { Children, useRef, useState } from 'react';
4-
import type { ChangedEvent, FlickingOptions, FlickingProps } from '@egjs/react-flicking';
5-
import Flicking from '@egjs/react-flicking';
3+
import { Children, useCallback, useState } from 'react';
4+
import useEmblaCarousel from 'embla-carousel-react';
65
import useIsMobile from '@gitanimals/react/src/hooks/useIsMobile/useIsMobile';
76

7+
import { usePrevNextButtons } from '@/components/EmblaCarousel/EmblaCarouselArrowButtons';
8+
89
import { ArrowButton } from './Arrow';
910

1011
function BackgroundSlider({ children }: { children: React.ReactNode }) {
1112
const isMobile = useIsMobile();
12-
const flicking = useRef<Flicking | null>(null);
13-
const [currentPanelIndex, setCurrentPanelIndex] = useState(0);
14-
15-
const panelsPerView = isMobile ? 1 : 2;
16-
17-
const isFirstPanel = currentPanelIndex === 0;
18-
const isLastPanel = currentPanelIndex === Math.ceil(Children.count(children) / panelsPerView) - 1;
19-
20-
const moveToNextPanel = async () => {
21-
if (!flicking.current || isLastPanel || flicking.current.animating) return;
22-
try {
23-
flicking.current.next();
24-
} catch (error) {}
25-
};
26-
27-
const moveToPrevPanel = async () => {
28-
if (!flicking.current || isFirstPanel || flicking.current.animating) return;
29-
try {
30-
flicking.current.prev();
31-
} catch (error) {}
32-
};
13+
const slidesPerView = isMobile ? 1 : 2;
14+
const slideWidth = slidesPerView === 1 ? '100%' : 'calc(50% - 10px)';
3315

34-
const onPanelChanged = (e: ChangedEvent<Flicking>) => {
35-
setCurrentPanelIndex(e.index);
36-
};
16+
const [emblaRef, emblaApi] = useEmblaCarousel({
17+
align: 'start',
18+
loop: false,
19+
skipSnaps: false,
20+
slidesToScroll: slidesPerView,
21+
});
3722

38-
const sliderOptions: Partial<FlickingProps & FlickingOptions> = {
39-
panelsPerView,
40-
align: 'prev',
41-
gap: 20,
42-
onChanged: onPanelChanged,
43-
};
23+
const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = usePrevNextButtons(emblaApi);
4424

4525
return (
4626
<div className="w-full max-w-[1200px] mx-auto px-[60px] relative max-mobile:px-[35px]">
4727
<div className="relative w-full mt-5 max-mobile:mt-0">
48-
<ArrowButton onClick={moveToPrevPanel} direction="prev" disabled={isFirstPanel} />
49-
<ArrowButton onClick={moveToNextPanel} direction="next" disabled={isLastPanel} />
50-
<Flicking ref={flicking} {...sliderOptions}>
51-
{Children.map(children, (child, idx) => (
52-
<div
53-
className="w-[calc(50%-10px)] p-[10px] text-center [&_img]:w-full [&_img]:h-auto [&_img]:rounded-lg [&_p]:mt-[10px] [&_p]:text-base"
54-
key={idx}
55-
>
56-
{child}
57-
</div>
58-
))}
59-
</Flicking>
28+
<ArrowButton onClick={onPrevButtonClick} direction="prev" disabled={prevBtnDisabled} />
29+
<ArrowButton onClick={onNextButtonClick} direction="next" disabled={nextBtnDisabled} />
30+
<div className="overflow-hidden" ref={emblaRef}>
31+
<div className="flex gap-5">
32+
{Children.map(children, (child, idx) => (
33+
<div
34+
className="min-w-0 p-[10px] text-center [&_img]:w-full [&_img]:h-auto [&_img]:rounded-lg [&_p]:mt-[10px] [&_p]:text-base"
35+
style={{ flex: `0 0 ${slideWidth}` }}
36+
key={idx}
37+
>
38+
{child}
39+
</div>
40+
))}
41+
</div>
42+
</div>
6043
</div>
6144
</div>
6245
);

apps/web/src/app/[locale]/event/(common)/CardList.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import { AnimatePresence, motion } from 'framer-motion';
55

66
import { PerspectiveCenterSlider } from '@/components/Slider';
77

8-
import '@egjs/react-flicking/dist/flicking.css';
9-
import '@egjs/react-flicking/dist/flicking-inline.css';
10-
118
interface CardListProps {
129
renderCard: (type: string) => React.ReactNode;
1310
persona: string[];

apps/web/src/app/[locale]/landing/AvailablePetSection/AnimalSliderContainer.tsx

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,36 @@
11
'use client';
22

3-
import React, { useEffect, useState } from 'react';
4-
import { useRef } from 'react';
3+
import React, { useCallback } from 'react';
54
import Image from 'next/image';
6-
import { Arrow } from '@egjs/flicking-plugins';
7-
import type { FlickingOptions, FlickingProps } from '@egjs/react-flicking';
8-
import Flicking from '@egjs/react-flicking';
5+
import useEmblaCarousel from 'embla-carousel-react';
96
import { cn } from '@gitanimals/ui-tailwind';
107

11-
// TODO: 후에 공통으로 사용할 수 있을 것 같다.
128
function AnimalSliderContainer({ children }: { children: React.ReactNode }) {
13-
const flicking = useRef<Flicking | null>(null);
9+
const [emblaRef, emblaApi] = useEmblaCarousel({
10+
align: 'start',
11+
loop: false,
12+
skipSnaps: false,
13+
});
1414

15-
// TODO: useMounted hook으로 분리
16-
const [isMounted, setIsMounted] = useState(false);
17-
useEffect(() => setIsMounted(true), []);
18-
19-
const wrapperRef = useRef<HTMLDivElement>(null);
20-
const plugins = [new Arrow({ parentEl: wrapperRef.current })];
21-
22-
const sliderOptions: Partial<FlickingProps & FlickingOptions> = {
23-
panelsPerView: 1,
24-
plugins: isMounted ? plugins : undefined,
25-
};
15+
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
16+
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
2617

2718
return (
2819
<div>
29-
<div ref={wrapperRef} className={sliderContainer}>
30-
<Flicking ref={flicking} {...sliderOptions}>
31-
{children}
32-
</Flicking>
33-
<span className={cn('flicking-arrow-prev', 'is-outside', sliderArrowStyle, prevArrowStyle)}>
20+
<div className={sliderContainer}>
21+
<div className="overflow-hidden" ref={emblaRef}>
22+
<div className="flex">
23+
{React.Children.map(children, (child, index) => (
24+
<div key={index} className="flex-[0_0_100%] min-w-0">
25+
{child}
26+
</div>
27+
))}
28+
</div>
29+
</div>
30+
<span className={cn(sliderArrowStyle, prevArrowStyle)} onClick={scrollPrev}>
3431
<Image src="/icon/circle-arrow.svg" alt="arrow" width={40} height={40} />
3532
</span>
36-
<span className={cn('flicking-arrow-next', 'is-outside', sliderArrowStyle, nextArrowStyle)}>
33+
<span className={cn(sliderArrowStyle, nextArrowStyle)} onClick={scrollNext}>
3734
<Image src="/icon/circle-arrow.svg" alt="arrow" width={40} height={40} />
3835
</span>
3936
</div>
@@ -44,10 +41,9 @@ function AnimalSliderContainer({ children }: { children: React.ReactNode }) {
4441
export default AnimalSliderContainer;
4542

4643
const sliderArrowStyle = cn(
47-
'translate-x-10 w-10 h-10',
44+
'w-10 h-10 cursor-pointer',
4845
'absolute block top-0 bottom-0 my-auto z-floating',
49-
'[&.flicking-arrow-disabled]:translate-x-9 [&.flicking-arrow-disabled]:w-9 [&.flicking-arrow-disabled]:h-9',
50-
'[&.flicking-arrow-disabled]:cursor-not-allowed [&.flicking-arrow-disabled]:brightness-50',
46+
'[&_img]:w-full [&_img]:h-full',
5147
);
5248

5349
const prevArrowStyle = cn('rotate-180 -left-6');

apps/web/src/app/[locale]/landing/AvailablePetSection/AnimalSliderContainerMobile.tsx

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,37 @@
11
'use client';
22

3-
import React, { Children, useState } from 'react';
4-
import { useRef } from 'react';
3+
import React, { Children, useCallback } from 'react';
54
import Image from 'next/image';
6-
import { Fade, Perspective } from '@egjs/flicking-plugins';
7-
import type { ChangedEvent, FlickingOptions, FlickingProps } from '@egjs/react-flicking';
8-
import Flicking from '@egjs/react-flicking';
5+
import useEmblaCarousel from 'embla-carousel-react';
96
import { cn } from '@gitanimals/ui-tailwind';
107

8+
import { usePrevNextButtons } from '@/components/EmblaCarousel/EmblaCarouselArrowButtons';
9+
1110
import { sliderContainer } from '../MainSection/MainSlider.style';
1211

1312
function AnimalSliderContainerMobile({ children }: { children: React.ReactNode }) {
14-
const flicking = useRef<Flicking | null>(null);
15-
16-
const [currentPanelIndex, setCurrentPanelIndex] = useState(0);
17-
18-
const isFirstPanel = currentPanelIndex === 0;
19-
const isLastPanel = Children.count(children) - 1 === currentPanelIndex;
20-
21-
// TODO: arrow plugin을 사용
22-
const moveToNextPanel = async () => {
23-
if (!flicking.current) return;
24-
if (isLastPanel) return;
25-
if (flicking.current.animating) return;
26-
27-
try {
28-
flicking.current.next();
29-
} catch (error) {}
30-
};
31-
32-
const moveToPrevPanel = async () => {
33-
if (!flicking.current) return;
34-
if (isFirstPanel) return;
35-
if (flicking.current.animating) return;
36-
37-
try {
38-
flicking.current.prev();
39-
} catch (error) {}
40-
};
41-
const onPanelChanged = (e: ChangedEvent<Flicking>) => {
42-
setCurrentPanelIndex(e.index);
43-
};
44-
const _plugins = [new Perspective({ rotate: 0.5, scale: 0.2 }), new Fade()];
45-
46-
const sliderOptions: Partial<FlickingProps & FlickingOptions> = {
47-
onChanged: onPanelChanged,
13+
const [emblaRef, emblaApi] = useEmblaCarousel({
4814
align: 'center',
49-
plugins: _plugins,
50-
};
15+
loop: false,
16+
skipSnaps: false,
17+
});
18+
19+
const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = usePrevNextButtons(emblaApi);
5120

5221
return (
5322
<div>
5423
<div className={cn(sliderContainer, 'slider-container')}>
55-
<ArrowButton onClick={moveToPrevPanel} direction="prev" disabled={isFirstPanel} />
56-
<ArrowButton onClick={moveToNextPanel} direction="next" disabled={isLastPanel} />
57-
<Flicking ref={flicking} {...sliderOptions}>
58-
{children}
59-
</Flicking>
24+
<ArrowButton onClick={onPrevButtonClick} direction="prev" disabled={prevBtnDisabled} />
25+
<ArrowButton onClick={onNextButtonClick} direction="next" disabled={nextBtnDisabled} />
26+
<div className="overflow-hidden" ref={emblaRef}>
27+
<div className="flex">
28+
{Children.map(children, (child, index) => (
29+
<div key={index} className="flex-[0_0_100%] min-w-0">
30+
{child}
31+
</div>
32+
))}
33+
</div>
34+
</div>
6035
</div>
6136
</div>
6237
);

apps/web/src/app/[locale]/layout.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import { config } from '@/shared/config/config';
88
import type { Locale } from '@/shared/i18n/routing';
99
import { LOCALE_LIST } from '@/shared/i18n/routing';
1010

11-
import '@egjs/react-flicking/dist/flicking.css';
12-
import '@egjs/react-flicking/dist/flicking-inline.css';
13-
1411
export function generateMetadata({ params: { locale } }: { params: { locale: Locale } }): Metadata {
1512
const baseUrl = config.url;
1613

apps/web/src/app/[locale]/mypage/my-pet/(merge)/MergePersona.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ export function MergePersona({ isOpen, onClose, targetPersona: initTargetPersona
8080
</DialogV2.Body>
8181
</SelectPersonaList>
8282

83-
8483
<DialogV2.Footer className="justify-center">
8584
<Button variant="secondary" onClick={onClose}>
8685
{t('cancel')}

apps/web/src/app/[locale]/page.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ import {
1414
RankingServerSide,
1515
} from './landing';
1616

17-
import '@egjs/react-flicking/dist/flicking.css';
18-
import '@egjs/react-flicking/dist/flicking-inline.css';
19-
2017
export async function generateMetadata(): Promise<Metadata> {
2118
const t = await getTranslations('page');
2219

0 commit comments

Comments
 (0)