-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/45-2 메인페이지 인기 체험 섹션 UI 구현 #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a1c23a
a860255
b2c3f65
12ca5a3
218ddca
e1e05ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import Image from 'next/image'; | ||
|
|
||
| export default function ExperienceCard() { | ||
| return ( | ||
| <div className='relative w-186 h-186 md:w-384 md:h-384 rounded-[20px] overflow-hidden shadow-md bg-white'> | ||
| {/* 배경 이미지 */} | ||
| <Image | ||
| src='/test/image1.png' | ||
| alt='체험 이미지' | ||
| className='w-full object-cover' | ||
| fill | ||
| /> | ||
| {/* 어두운 오버레이 */} | ||
| <div className='absolute inset-0 bg-gradient-to-r from-black to-transparent' /> | ||
| {/* 텍스트 정보 블록 (카드 하단 위치 고정) */} | ||
| <div className='absolute bottom-12 flex flex-col gap-6 md:gap-20 px-20 py-12 text-white'> | ||
| {/* 별점 정보 */} | ||
| <span className='text-md'>⭐ 4.9 (293)</span> | ||
| {/* 체험명 (줄바꿈 포함, 반응형 크기) */} | ||
| <p className='text-2lg md:text-3xl font-semibold'>함께 배우면 즐거운<br />스트릿 댄스</p> | ||
| {/* 가격 정보 */} | ||
| <p className='text-lg md:text-xl'>₩ 38,000 <span className='text-gray-600 text-md'>/ 인</span></p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRef } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ExperienceCard from '@/app/(with-header)/components/ExperienceCard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import IconArrowRight from '@assets/svg/right-arrow'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import IconArrowLeft from '@assets/svg/left-arrow'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function PopularExperiences() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 카드 슬라이더를 참조할 DOM ref | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sliderRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 좌우 버튼 클릭 시 한 장씩 슬라이드 이동 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollByCard = (direction: 'left' | 'right') => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!sliderRef.current) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 첫 번째 카드 요소를 찾아서 너비 측정 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const card = sliderRef.current.querySelector('.card'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!(card instanceof HTMLElement)) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cardWidth = card.offsetWidth; // 카드 너비 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const gap = parseInt(getComputedStyle(sliderRef.current).gap) || 0; // gap 값 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const distance = cardWidth + gap; // 한 번에 이동할 거리 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 슬라이더 스크롤 이동 (좌/우 방향에 따라) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sliderRef.current.scrollBy({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| left: direction === 'left' ? -distance : distance, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| behavior: 'smooth', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 스크롤 함수의 안정성을 개선하세요. 함수가 전반적으로 잘 구현되어 있지만 몇 가지 개선 사항이 있습니다:
다음과 같이 개선할 수 있습니다: const scrollByCard = (direction: 'left' | 'right') => {
if (!sliderRef.current) return;
const card = sliderRef.current.querySelector('.card');
if (!(card instanceof HTMLElement)) return;
const cardWidth = card.offsetWidth;
- const gap = parseInt(getComputedStyle(sliderRef.current).gap) || 0;
+ const gapValue = getComputedStyle(sliderRef.current).gap;
+ const gap = gapValue ? parseInt(gapValue) || 0 : 0;
const distance = cardWidth + gap;
sliderRef.current.scrollBy({
left: direction === 'left' ? -distance : distance,
behavior: 'smooth',
});
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <section className='pt-24 md:pt-34 pl-24 lg:pl-0 pb-40 lg:pb-33 lg:max-w-1200 lg:w-full mx-auto'> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* 섹션 제목 + 좌우 화살표 버튼 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className='flex justify-between items-center pb-16 md:pb-32 mb-6'> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 className='text-xl md:text-3xl font-bold'>🔥 인기 체험</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className='flex gap-2'> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <IconArrowLeft size={32} onClick={() => scrollByCard('left')} className='text-2xl px-3' /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <IconArrowRight size={32} onClick={() => scrollByCard('right')} className='text-2xl px-3' /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 접근성 개선이 필요합니다. 화살표 아이콘을 클릭 가능한 요소로 사용하고 있지만 접근성 지원이 부족합니다. 다음과 같이 개선하세요: <div className='flex gap-2'>
- <IconArrowLeft size={32} onClick={() => scrollByCard('left')} className='text-2xl px-3' />
- <IconArrowRight size={32} onClick={() => scrollByCard('right')} className='text-2xl px-3' />
+ <button
+ onClick={() => scrollByCard('left')}
+ className='text-2xl px-3 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded'
+ aria-label='이전 체험 보기'
+ >
+ <IconArrowLeft size={32} />
+ </button>
+ <button
+ onClick={() => scrollByCard('right')}
+ className='text-2xl px-3 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded'
+ aria-label='다음 체험 보기'
+ >
+ <IconArrowRight size={32} />
+ </button>
</div>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* 가로 슬라이드 카드 리스트 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={sliderRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className='flex gap-16 md:gap-32 lg:gap-24 overflow-x-auto scroll-smooth no-scrollbar' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {[...Array(4)].map((_, idx) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 카드 wrapper: flex-shrink-0으로 크기 고정 + 'card' 클래스로 식별 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div key={idx} className='flex-shrink-0 card'> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서 클래스를 지정해준다음에 querySelector로 찾도록 하신거군요!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제 코드가 도움이 되었다니 다행입니다! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ExperienceCard /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 카드 데이터를 동적으로 처리하도록 개선하세요. 현재 하드코딩된 4개 카드 배열은 확장성이 떨어집니다. 컴포넌트를 더 유연하게 만들기 위해 props를 사용하는 것을 고려해보세요: +interface PopularExperiencesProps {
+ experiences?: ExperienceData[];
+ maxItems?: number;
+}
-export default function PopularExperiences() {
+export default function PopularExperiences({
+ experiences = [],
+ maxItems = 4
+}: PopularExperiencesProps) {
// ... existing code ...
- {[...Array(4)].map((_, idx) => (
+ {(experiences.length > 0 ? experiences : [...Array(maxItems)]).slice(0, maxItems).map((experience, idx) => (
<div key={idx} className='flex-shrink-0 card'>
- <ExperienceCard />
+ <ExperienceCard data={experience} />
</div>
))}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 🛠️ Refactor suggestion 키보드 네비게이션 지원을 추가하고 데이터 구조를 개선하세요. 현재 구현에서 개선이 필요한 부분들:
키보드 이벤트 핸들러 추가: + const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'ArrowLeft') {
+ scrollByCard('left');
+ } else if (event.key === 'ArrowRight') {
+ scrollByCard('right');
+ }
+ };
<div
ref={sliderRef}
- className='flex gap-16 md:gap-32 lg:gap-24 overflow-x-auto scroll-smooth no-scrollbar'
+ className='flex gap-16 md:gap-32 lg:gap-24 overflow-x-auto scroll-smooth no-scrollbar'
+ tabIndex={0}
+ onKeyDown={handleKeyDown}
+ role='region'
+ aria-label='인기 체험 목록'
>하드코딩된 카드 배열을 props나 API 데이터로 대체하는 코드를 생성해드릴까요? 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| import BannerSection from '@/app/(with-header)/components/BannerSection'; | ||
| import PopularExperiences from '@/app/(with-header)/components/PopularExperiences'; | ||
|
|
||
| export default function HomePage() { | ||
| return ( | ||
| <main> | ||
| <BannerSection /> | ||
| <PopularExperiences /> | ||
| </main> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ref를 사용하는 UI에 대해서는 아직 부족한데 덕분에 배워갑니다!
추후에 motion라이브러리까지 적용하면 될것같네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵~! API 연동까지 다 되면 motion 라이브러리 도입해보려구요. 감사합니다!