@jk-core/components 1.1.18 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1302 -1000
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +8 -8
- package/dist/index.umd.cjs.map +1 -1
- package/dist/src/common/Carousel/index.d.ts +15 -0
- package/dist/src/common/RollingBanner/index.d.ts +11 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/utils/ts/formatMoney.d.ts +1 -1
- package/package.json +1 -1
- package/src/common/Carousel/Carousel.module.scss +222 -0
- package/src/common/Carousel/index.tsx +417 -0
- package/src/common/RollingBanner/RollingBanner.module.scss +126 -0
- package/src/common/RollingBanner/index.tsx +140 -0
- package/src/common/Skeleton/Skeleton.module.scss +5 -5
- package/src/index.tsx +4 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
interface CarouselProps {
|
|
3
|
+
/**
|
|
4
|
+
* 캐러셀에 표시할 슬라이드 요소들입니다.
|
|
5
|
+
* 일반적으로 이미지나 기타 콘텐츠 요소들을 자식으로 전달합니다.
|
|
6
|
+
*/
|
|
7
|
+
children: ReactNode[];
|
|
8
|
+
autoPlay?: boolean;
|
|
9
|
+
autoPlayInterval?: number;
|
|
10
|
+
showIndicators?: boolean;
|
|
11
|
+
showNavigation?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export default function Carousel({ children, autoPlay, autoPlayInterval, showIndicators, showNavigation, className, }: CarouselProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface RollingBannerProps {
|
|
2
|
+
speed?: number;
|
|
3
|
+
reverse?: boolean;
|
|
4
|
+
children?: React.ReactNode | React.ReactNode[];
|
|
5
|
+
gradientColor?: string;
|
|
6
|
+
gradient?: boolean;
|
|
7
|
+
gradientWidth?: number | string;
|
|
8
|
+
direction?: 'row' | 'column';
|
|
9
|
+
}
|
|
10
|
+
export default function RollingBanner({ speed, gradient, gradientColor, gradientWidth, reverse, children, direction }: RollingBannerProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { default as Breadcrumbs, BreadcrumbsItem } from './common/Breadcrumbs';
|
|
2
2
|
import { default as Button } from './common/Button';
|
|
3
3
|
import { default as Pagination } from './common/Pagination';
|
|
4
|
+
import { default as RollingBanner } from './common/RollingBanner';
|
|
4
5
|
import { default as Skeleton } from './common/Skeleton';
|
|
5
6
|
import { default as SwitchButton } from './common/SwitchButton';
|
|
6
7
|
import { default as Table } from './common/Table';
|
|
@@ -8,7 +9,8 @@ import { default as Calendar } from './Calendar';
|
|
|
8
9
|
import { CalendarRange, CalendarView } from './Calendar/type';
|
|
9
10
|
import { default as Accordion } from './common/Accordion';
|
|
10
11
|
import { default as Card } from './common/Card';
|
|
12
|
+
import { default as Carousel } from './common/Carousel';
|
|
11
13
|
import { default as DropDown } from './common/DropDown';
|
|
12
14
|
import { default as SegmentButton } from './common/SegmentButton';
|
|
13
|
-
export { Calendar, Accordion, Breadcrumbs, Button, Pagination, Skeleton, SwitchButton, Card, DropDown, SegmentButton, BreadcrumbsItem, Table, };
|
|
15
|
+
export { Calendar, Accordion, Breadcrumbs, Button, Pagination, Skeleton, SwitchButton, Card, DropDown, SegmentButton, BreadcrumbsItem, Table, Carousel, RollingBanner, };
|
|
14
16
|
export type { CalendarView, CalendarRange };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const formatMoney: (value?: number) =>
|
|
1
|
+
declare const formatMoney: (value?: number) => (string | number)[] | "- 원";
|
|
2
2
|
export default formatMoney;
|
package/package.json
CHANGED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
.carousel {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
border: 1px solid var(--G-20);
|
|
6
|
+
|
|
7
|
+
&:focus {
|
|
8
|
+
outline: 2px solid #007acc;
|
|
9
|
+
outline-offset: 2px;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.carouselContainer {
|
|
14
|
+
position: relative;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
cursor: grab;
|
|
19
|
+
user-select: none;
|
|
20
|
+
|
|
21
|
+
&:active {
|
|
22
|
+
cursor: grabbing;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.carouselSlides {
|
|
27
|
+
display: flex;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
will-change: transform;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.carouselSlide {
|
|
34
|
+
flex: 0 0 100%;
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 100%;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Navigation Buttons */
|
|
43
|
+
.carouselButton {
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: 50%;
|
|
46
|
+
transform: translateY(-50%);
|
|
47
|
+
background: rgb(255, 255, 255, 0.9);
|
|
48
|
+
border: none;
|
|
49
|
+
border-radius: 50%;
|
|
50
|
+
width: 40px;
|
|
51
|
+
height: 40px;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
font-size: 20px;
|
|
56
|
+
font-weight: bold;
|
|
57
|
+
color: #333;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
transition: all 0.2s ease;
|
|
60
|
+
z-index: 2;
|
|
61
|
+
box-shadow: 0 2px 8px rgb(0, 0, 0, 0.3);
|
|
62
|
+
|
|
63
|
+
&:hover {
|
|
64
|
+
background: rgb(255, 255, 255, 1);
|
|
65
|
+
transform: translateY(-50%) scale(1.1);
|
|
66
|
+
box-shadow: 0 4px 12px rgb(0, 0, 0, 0.15);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&:active {
|
|
70
|
+
transform: translateY(-50%) scale(0.95);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.carouselButtonPrev {
|
|
75
|
+
left: 16px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.carouselButtonNext {
|
|
79
|
+
right: 16px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Indicators */
|
|
83
|
+
.carouselIndicators {
|
|
84
|
+
position: absolute;
|
|
85
|
+
bottom: 16px;
|
|
86
|
+
left: 50%;
|
|
87
|
+
transform: translateX(-50%);
|
|
88
|
+
display: flex;
|
|
89
|
+
gap: 8px;
|
|
90
|
+
z-index: 2;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.carouselIndicator {
|
|
94
|
+
width: 12px;
|
|
95
|
+
height: 12px;
|
|
96
|
+
border: none;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
background: rgb(255, 255, 255, 0.5);
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: all 0.2s ease;
|
|
101
|
+
box-shadow: 1px 1px 4px 2px rgb(0, 0, 0, 0.2);
|
|
102
|
+
|
|
103
|
+
&:hover {
|
|
104
|
+
background: rgb(255, 255, 255, 0.8);
|
|
105
|
+
transform: scale(1.2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&:focus {
|
|
109
|
+
outline: 2px solid #007acc;
|
|
110
|
+
outline-offset: 2px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&.active {
|
|
114
|
+
background: var(--P-70);
|
|
115
|
+
transform: scale(1.3);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Responsive Design */
|
|
120
|
+
@media (width <= 768px) {
|
|
121
|
+
.carouselButton {
|
|
122
|
+
width: 36px;
|
|
123
|
+
height: 36px;
|
|
124
|
+
font-size: 18px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.carouselButtonPrev {
|
|
128
|
+
left: 12px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.carouselButtonNext {
|
|
132
|
+
right: 12px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.carouselIndicators {
|
|
136
|
+
bottom: 12px;
|
|
137
|
+
gap: 6px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.carouselIndicator {
|
|
141
|
+
width: 10px;
|
|
142
|
+
height: 10px;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media (width <= 480px) {
|
|
147
|
+
.carouselButton {
|
|
148
|
+
width: 32px;
|
|
149
|
+
height: 32px;
|
|
150
|
+
font-size: 16px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.carouselButtonPrev {
|
|
154
|
+
left: 8px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.carouselButtonNext {
|
|
158
|
+
right: 8px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.carouselIndicators {
|
|
162
|
+
bottom: 8px;
|
|
163
|
+
gap: 4px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.carouselIndicator {
|
|
167
|
+
width: 8px;
|
|
168
|
+
height: 8px;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Touch device optimizations */
|
|
173
|
+
@media (hover: none) and (pointer: coarse) {
|
|
174
|
+
.carouselContainer {
|
|
175
|
+
cursor: default;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.carouselButton {
|
|
179
|
+
&:hover {
|
|
180
|
+
background: rgb(255, 255, 255, 0.9);
|
|
181
|
+
transform: translateY(-50%);
|
|
182
|
+
box-shadow: 0 2px 8px rgb(0, 0, 0, 0.1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.carouselIndicator {
|
|
187
|
+
&:hover {
|
|
188
|
+
background: rgb(255, 255, 255, 0.5);
|
|
189
|
+
transform: none;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Accessibility improvements */
|
|
195
|
+
@media (prefers-reduced-motion: reduce) {
|
|
196
|
+
.carouselSlides {
|
|
197
|
+
transition: none !important;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.carouselButton,
|
|
201
|
+
.carouselIndicator {
|
|
202
|
+
transition: none;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* High contrast mode support */
|
|
207
|
+
@media (prefers-contrast: high) {
|
|
208
|
+
.carouselButton {
|
|
209
|
+
background: white;
|
|
210
|
+
color: black;
|
|
211
|
+
border: 2px solid black;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.carouselIndicator {
|
|
215
|
+
background: white;
|
|
216
|
+
border: 2px solid black;
|
|
217
|
+
|
|
218
|
+
&.active {
|
|
219
|
+
background: black;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import styles from './Carousel.module.scss';
|
|
4
|
+
|
|
5
|
+
interface CarouselProps {
|
|
6
|
+
/**
|
|
7
|
+
* 캐러셀에 표시할 슬라이드 요소들입니다.
|
|
8
|
+
* 일반적으로 이미지나 기타 콘텐츠 요소들을 자식으로 전달합니다.
|
|
9
|
+
*/
|
|
10
|
+
children: ReactNode[];
|
|
11
|
+
autoPlay?: boolean;
|
|
12
|
+
autoPlayInterval?: number;
|
|
13
|
+
showIndicators?: boolean;
|
|
14
|
+
showNavigation?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function Carousel({
|
|
19
|
+
children,
|
|
20
|
+
autoPlay = false,
|
|
21
|
+
autoPlayInterval = 3000,
|
|
22
|
+
showIndicators = true,
|
|
23
|
+
showNavigation = true,
|
|
24
|
+
className = '',
|
|
25
|
+
}: CarouselProps) {
|
|
26
|
+
// 무한 루프를 위해 1부터 시작 (복제된 마지막 슬라이드 다음)
|
|
27
|
+
const [currentIndex, setCurrentIndex] = useState(1);
|
|
28
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
29
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
30
|
+
const [startPos, setStartPos] = useState(0);
|
|
31
|
+
const [currentTranslate, setCurrentTranslate] = useState(0);
|
|
32
|
+
const [prevTranslate, setPrevTranslate] = useState(0);
|
|
33
|
+
|
|
34
|
+
const carouselRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const autoPlayRef = useRef<NodeJS.Timeout | null>(null);
|
|
37
|
+
|
|
38
|
+
// 총 슬라이드 개수 (원본)
|
|
39
|
+
const totalSlides = children.length;
|
|
40
|
+
|
|
41
|
+
// 복제 슬라이드 포함 (앞뒤로 하나씩 추가)
|
|
42
|
+
const extendedSlides = [
|
|
43
|
+
children[totalSlides - 1], // 마지막 슬라이드 복제 (앞에)
|
|
44
|
+
...children, // 원본 슬라이드들
|
|
45
|
+
children[0], // 첫 번째 슬라이드 복제 (뒤에)
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// 실제 표시되는 슬라이드 인덱스 (인디케이터용)
|
|
49
|
+
// 무한 루프 시 첫/끝 슬라이드에서 인디케이터가 즉시 업데이트되도록 처리
|
|
50
|
+
const displayIndex = currentIndex === 0
|
|
51
|
+
? totalSlides - 1 // 복제된 마지막 슬라이드(인덱스 0)에 있을 때 마지막 인디케이터 활성화
|
|
52
|
+
: currentIndex === totalSlides + 1
|
|
53
|
+
? 0 // 복제된 첫 번째 슬라이드(인덱스 totalSlides+1)에 있을 때 첫 번째 인디케이터 활성화
|
|
54
|
+
: currentIndex - 1; // 일반적인 경우
|
|
55
|
+
|
|
56
|
+
// 다음 슬라이드로 이동
|
|
57
|
+
const goToNext = useCallback(() => {
|
|
58
|
+
if (isTransitioning) return;
|
|
59
|
+
setIsTransitioning(true);
|
|
60
|
+
setCurrentIndex((prev) => prev + 1);
|
|
61
|
+
}, [isTransitioning]);
|
|
62
|
+
|
|
63
|
+
// 이전 슬라이드로 이동
|
|
64
|
+
const goToPrev = useCallback(() => {
|
|
65
|
+
if (isTransitioning) return;
|
|
66
|
+
setIsTransitioning(true);
|
|
67
|
+
setCurrentIndex((prev) => prev - 1);
|
|
68
|
+
}, [isTransitioning]);
|
|
69
|
+
|
|
70
|
+
// 특정 슬라이드로 이동
|
|
71
|
+
const goToSlide = useCallback((index: number) => {
|
|
72
|
+
if (isTransitioning) return;
|
|
73
|
+
if (index === displayIndex) return; // 이미 같은 슬라이드에 있으면 리턴
|
|
74
|
+
|
|
75
|
+
setIsTransitioning(true);
|
|
76
|
+
const targetIndex = index + 1; // 복제 슬라이드 오프셋 적용
|
|
77
|
+
setCurrentIndex(targetIndex);
|
|
78
|
+
}, [isTransitioning, displayIndex]);
|
|
79
|
+
|
|
80
|
+
// 자동재생 시작
|
|
81
|
+
const startAutoPlay = useCallback(() => {
|
|
82
|
+
// 이미 실행 중이면 중복 실행 방지
|
|
83
|
+
if (autoPlayRef.current) {
|
|
84
|
+
clearInterval(autoPlayRef.current);
|
|
85
|
+
autoPlayRef.current = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (autoPlay) {
|
|
89
|
+
autoPlayRef.current = setInterval(() => {
|
|
90
|
+
goToNext();
|
|
91
|
+
}, autoPlayInterval);
|
|
92
|
+
}
|
|
93
|
+
}, [autoPlay, autoPlayInterval, goToNext]);
|
|
94
|
+
|
|
95
|
+
// 자동재생 중지
|
|
96
|
+
const stopAutoPlay = useCallback(() => {
|
|
97
|
+
if (autoPlayRef.current) {
|
|
98
|
+
clearInterval(autoPlayRef.current);
|
|
99
|
+
autoPlayRef.current = null;
|
|
100
|
+
}
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
// 드래그 시작
|
|
104
|
+
const handleDragStart = (clientX: number) => {
|
|
105
|
+
setIsDragging(true);
|
|
106
|
+
setStartPos(clientX);
|
|
107
|
+
setCurrentTranslate(prevTranslate);
|
|
108
|
+
stopAutoPlay();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// 드래그 중
|
|
112
|
+
const handleDragMove = (clientX: number) => {
|
|
113
|
+
if (!isDragging) return;
|
|
114
|
+
|
|
115
|
+
const diff = clientX - startPos;
|
|
116
|
+
setCurrentTranslate(prevTranslate + diff);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// 드래그 종료
|
|
120
|
+
const handleDragEnd = () => {
|
|
121
|
+
if (!isDragging) return;
|
|
122
|
+
|
|
123
|
+
setIsDragging(false);
|
|
124
|
+
|
|
125
|
+
const containerWidth = containerRef.current?.offsetWidth || 0;
|
|
126
|
+
const threshold = containerWidth * 0.2; // 20% 이상 드래그시 슬라이드 변경
|
|
127
|
+
const diff = currentTranslate - prevTranslate;
|
|
128
|
+
|
|
129
|
+
if (Math.abs(diff) > threshold) {
|
|
130
|
+
if (diff > 0) {
|
|
131
|
+
goToPrev();
|
|
132
|
+
} else {
|
|
133
|
+
goToNext();
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// 원래 위치로 복귀
|
|
137
|
+
setCurrentTranslate(prevTranslate);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 드래그 종료 후 autoplay 재시작 (지연 실행으로 안정성 확보)
|
|
141
|
+
if (autoPlay) {
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
startAutoPlay();
|
|
144
|
+
}, 100);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 마우스 이벤트 핸들러
|
|
149
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
handleDragStart(e.clientX);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
155
|
+
handleDragMove(e.clientX);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleMouseUp = () => {
|
|
159
|
+
handleDragEnd();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleMouseLeave = () => {
|
|
163
|
+
handleDragEnd();
|
|
164
|
+
// 마우스가 carousel을 벗어나면 autoplay 재시작
|
|
165
|
+
if (autoPlay && !isDragging) {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
startAutoPlay();
|
|
168
|
+
}, 100);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// 터치 이벤트 핸들러
|
|
173
|
+
const handleTouchStart = (e: React.TouchEvent) => {
|
|
174
|
+
handleDragStart(e.touches[0].clientX);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleTouchMove = (e: React.TouchEvent) => {
|
|
178
|
+
handleDragMove(e.touches[0].clientX);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleTouchEnd = () => {
|
|
182
|
+
handleDragEnd();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// 키보드 이벤트 핸들러
|
|
186
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
187
|
+
if (e.key === 'ArrowLeft') {
|
|
188
|
+
goToPrev();
|
|
189
|
+
} else if (e.key === 'ArrowRight') {
|
|
190
|
+
goToNext();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// 트랜지션 완료 처리
|
|
195
|
+
const handleTransitionEnd = () => {
|
|
196
|
+
setIsTransitioning(false);
|
|
197
|
+
|
|
198
|
+
// 무한 루프 처리: 복제된 슬라이드에서 실제 슬라이드로 순간이동
|
|
199
|
+
if (carouselRef.current) {
|
|
200
|
+
if (currentIndex === 0) {
|
|
201
|
+
// 맨 앞의 복제된 슬라이드(마지막 아이템)에 있을 때 실제 마지막 슬라이드로 이동
|
|
202
|
+
carouselRef.current.style.transition = 'none';
|
|
203
|
+
setCurrentIndex(totalSlides);
|
|
204
|
+
const containerWidth = containerRef.current?.offsetWidth || 0;
|
|
205
|
+
const newTranslate = -totalSlides * containerWidth;
|
|
206
|
+
setPrevTranslate(newTranslate);
|
|
207
|
+
setCurrentTranslate(newTranslate);
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
if (carouselRef.current) {
|
|
210
|
+
carouselRef.current.style.transition = 'transform 0.3s ease-in-out';
|
|
211
|
+
}
|
|
212
|
+
}, 10);
|
|
213
|
+
} else if (currentIndex === totalSlides + 1) {
|
|
214
|
+
// 맨 뒤의 복제된 슬라이드(첫 번째 아이템)에 있을 때 실제 첫 번째 슬라이드로 이동
|
|
215
|
+
carouselRef.current.style.transition = 'none';
|
|
216
|
+
setCurrentIndex(1);
|
|
217
|
+
const containerWidth = containerRef.current?.offsetWidth || 0;
|
|
218
|
+
const newTranslate = -1 * containerWidth;
|
|
219
|
+
setPrevTranslate(newTranslate);
|
|
220
|
+
setCurrentTranslate(newTranslate);
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
if (carouselRef.current) {
|
|
223
|
+
carouselRef.current.style.transition = 'transform 0.3s ease-in-out';
|
|
224
|
+
}
|
|
225
|
+
}, 10);
|
|
226
|
+
} else {
|
|
227
|
+
// 일반적인 경우
|
|
228
|
+
const containerWidth = containerRef.current?.offsetWidth || 0;
|
|
229
|
+
const newTranslate = -currentIndex * containerWidth;
|
|
230
|
+
setPrevTranslate(newTranslate);
|
|
231
|
+
setCurrentTranslate(newTranslate);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 트랜지션 완료 후 autoplay 재시작 확인
|
|
236
|
+
if (autoPlay && !isDragging && !autoPlayRef.current) {
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
startAutoPlay();
|
|
239
|
+
}, 100);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// 자동재생 효과
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (autoPlay) {
|
|
246
|
+
startAutoPlay();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return () => {
|
|
250
|
+
stopAutoPlay();
|
|
251
|
+
};
|
|
252
|
+
}, [autoPlay, startAutoPlay, stopAutoPlay]);
|
|
253
|
+
|
|
254
|
+
// 초기 로드 시 올바른 위치 설정
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (containerRef.current) {
|
|
257
|
+
const containerWidth = containerRef.current.offsetWidth;
|
|
258
|
+
const newTranslate = -currentIndex * containerWidth;
|
|
259
|
+
setPrevTranslate(newTranslate);
|
|
260
|
+
setCurrentTranslate(newTranslate);
|
|
261
|
+
}
|
|
262
|
+
}, [currentIndex]);
|
|
263
|
+
|
|
264
|
+
// 슬라이드 위치 업데이트
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (!isDragging && containerRef.current) {
|
|
267
|
+
const containerWidth = containerRef.current.offsetWidth;
|
|
268
|
+
const newTranslate = -currentIndex * containerWidth;
|
|
269
|
+
setPrevTranslate(newTranslate);
|
|
270
|
+
setCurrentTranslate(newTranslate);
|
|
271
|
+
}
|
|
272
|
+
}, [currentIndex, isDragging]);
|
|
273
|
+
|
|
274
|
+
// 윈도우 리사이즈 처리
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
const handleResize = () => {
|
|
277
|
+
if (containerRef.current) {
|
|
278
|
+
const containerWidth = containerRef.current.offsetWidth;
|
|
279
|
+
const newTranslate = -currentIndex * containerWidth;
|
|
280
|
+
setPrevTranslate(newTranslate);
|
|
281
|
+
setCurrentTranslate(newTranslate);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
window.addEventListener('resize', handleResize);
|
|
286
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
287
|
+
}, [currentIndex]);
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div
|
|
291
|
+
className={`${styles.carousel} ${className}`}
|
|
292
|
+
onKeyDown={handleKeyDown}
|
|
293
|
+
tabIndex={0}
|
|
294
|
+
>
|
|
295
|
+
<div
|
|
296
|
+
ref={containerRef}
|
|
297
|
+
className={styles.carouselContainer}
|
|
298
|
+
onMouseDown={handleMouseDown}
|
|
299
|
+
onMouseMove={handleMouseMove}
|
|
300
|
+
onMouseUp={handleMouseUp}
|
|
301
|
+
onMouseLeave={handleMouseLeave}
|
|
302
|
+
onTouchStart={handleTouchStart}
|
|
303
|
+
onTouchMove={handleTouchMove}
|
|
304
|
+
onTouchEnd={handleTouchEnd}
|
|
305
|
+
onMouseEnter={() => {
|
|
306
|
+
// 마우스 호버 시 autoplay 일시정지 (드래그 중이 아닐 때만)
|
|
307
|
+
if (autoPlay && !isDragging) {
|
|
308
|
+
stopAutoPlay();
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<div
|
|
313
|
+
ref={carouselRef}
|
|
314
|
+
className={styles.carouselSlides}
|
|
315
|
+
style={{
|
|
316
|
+
transform: `translateX(${currentTranslate}px)`,
|
|
317
|
+
transition: isDragging ? 'none' : 'transform 0.3s ease-in-out',
|
|
318
|
+
}}
|
|
319
|
+
onTransitionEnd={handleTransitionEnd}
|
|
320
|
+
>
|
|
321
|
+
{extendedSlides.map((child, index) => (
|
|
322
|
+
<div key={`slide-${index}`} className={styles.carouselSlide}>
|
|
323
|
+
{child}
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
{/* Navigation Buttons */}
|
|
330
|
+
{showNavigation && (
|
|
331
|
+
<>
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
className={`${styles.carouselButton} ${styles.carouselButtonPrev}`}
|
|
335
|
+
onClick={() => {
|
|
336
|
+
goToPrev();
|
|
337
|
+
// 버튼 클릭 후 autoplay 재시작
|
|
338
|
+
if (autoPlay) {
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
startAutoPlay();
|
|
341
|
+
}, 100);
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
344
|
+
onMouseEnter={() => autoPlay && stopAutoPlay()}
|
|
345
|
+
onMouseLeave={() => {
|
|
346
|
+
if (autoPlay) {
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
startAutoPlay();
|
|
349
|
+
}, 100);
|
|
350
|
+
}
|
|
351
|
+
}}
|
|
352
|
+
aria-label="이전 슬라이드"
|
|
353
|
+
>
|
|
354
|
+
‹
|
|
355
|
+
</button>
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
className={`${styles.carouselButton} ${styles.carouselButtonNext}`}
|
|
359
|
+
onClick={() => {
|
|
360
|
+
goToNext();
|
|
361
|
+
// 버튼 클릭 후 autoplay 재시작
|
|
362
|
+
if (autoPlay) {
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
startAutoPlay();
|
|
365
|
+
}, 100);
|
|
366
|
+
}
|
|
367
|
+
}}
|
|
368
|
+
onMouseEnter={() => autoPlay && stopAutoPlay()}
|
|
369
|
+
onMouseLeave={() => {
|
|
370
|
+
if (autoPlay) {
|
|
371
|
+
setTimeout(() => {
|
|
372
|
+
startAutoPlay();
|
|
373
|
+
}, 100);
|
|
374
|
+
}
|
|
375
|
+
}}
|
|
376
|
+
aria-label="다음 슬라이드"
|
|
377
|
+
>
|
|
378
|
+
›
|
|
379
|
+
</button>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* Indicators */}
|
|
384
|
+
{showIndicators && (
|
|
385
|
+
<div className={styles.carouselIndicators}>
|
|
386
|
+
{children.map((_, index) => (
|
|
387
|
+
<button
|
|
388
|
+
key={index}
|
|
389
|
+
type="button"
|
|
390
|
+
className={`${styles.carouselIndicator} ${
|
|
391
|
+
index === displayIndex ? styles.active : ''
|
|
392
|
+
}`}
|
|
393
|
+
onClick={() => {
|
|
394
|
+
goToSlide(index);
|
|
395
|
+
// 인디케이터 클릭 후 autoplay 재시작
|
|
396
|
+
if (autoPlay) {
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
startAutoPlay();
|
|
399
|
+
}, 100);
|
|
400
|
+
}
|
|
401
|
+
}}
|
|
402
|
+
onMouseEnter={() => autoPlay && stopAutoPlay()}
|
|
403
|
+
onMouseLeave={() => {
|
|
404
|
+
if (autoPlay) {
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
startAutoPlay();
|
|
407
|
+
}, 100);
|
|
408
|
+
}
|
|
409
|
+
}}
|
|
410
|
+
aria-label={`슬라이드 ${index + 1}로 이동`}
|
|
411
|
+
/>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|