Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/(with-header)/components/BannerSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function BannerSection() {
<div className='absolute inset-0 bg-gradient-to-r from-black to-transparent' />

{/* 텍스트 콘텐츠 */}
<div className='relative z-10 flex flex-col items-start w-220 max-w-1152 md:w-440 lg:w-full pl-24 pt-74 md:pl-32 lg:pl-0 md:pt-144 lg:pt-159 lg:ml-auto lg:mr-auto gap-8 lg:gap-20 h-full text-white font-bold break-keep'>
<div className='relative z-10 flex flex-col items-start w-220 max-w-1200 md:w-440 lg:w-full pl-24 pt-74 md:pl-32 lg:pl-0 md:pt-144 lg:pt-159 lg:ml-auto lg:mr-auto gap-8 lg:gap-20 h-full text-white font-bold break-keep'>
<h2 className='text-2xl md:text-[54px] md:leading-[64px] lg:text-[68px] lg:leading-[78px]'>
함께 배우면 즐거운<br />
스트릿 댄스
Expand Down
26 changes: 26 additions & 0 deletions src/app/(with-header)/components/ExperienceCard.tsx
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>
);
}
56 changes: 56 additions & 0 deletions src/app/(with-header)/components/PopularExperiences.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref를 사용하는 UI에 대해서는 아직 부족한데 덕분에 배워갑니다!
추후에 motion라이브러리까지 적용하면 될것같네요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵~! API 연동까지 다 되면 motion 라이브러리 도입해보려구요. 감사합니다!

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

스크롤 함수의 안정성을 개선하세요.

함수가 전반적으로 잘 구현되어 있지만 몇 가지 개선 사항이 있습니다:

  1. parseInt에서 NaN 처리가 필요합니다
  2. .card 클래스에 의존하는 구조로 인한 결합도가 높습니다

다음과 같이 개선할 수 있습니다:

  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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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',
});
};
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 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',
});
};
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx around lines 13 to
29, improve the scrollByCard function by adding a check to handle NaN results
from parseInt when reading the gap style, defaulting to 0 if NaN. Also, reduce
coupling by avoiding direct dependency on the '.card' class; instead, consider
passing the card element or its width as a parameter or using a more flexible
selector strategy. Implement these changes to enhance function stability and
maintainability.


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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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>
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'>
<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>
</div>
{/* …the rest of the section… */}
</section>
);
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx around lines 31 to
40, the arrow icons used for scrolling are clickable but lack accessibility
support. To fix this, wrap the IconArrowLeft and IconArrowRight components in
accessible button elements or add appropriate role="button" and tabindex="0"
attributes, and include keyboard event handlers to support keyboard navigation.
Also, add descriptive aria-labels to these elements to clearly convey their
purpose to screen readers.


{/* 가로 슬라이드 카드 리스트 */}
<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'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 클래스를 지정해준다음에 querySelector로 찾도록 하신거군요!
하나 배워갑니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 코드가 도움이 되었다니 다행입니다!

<ExperienceCard />
</div>
))}
Comment on lines +47 to +52
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{[...Array(4)].map((_, idx) => (
// 카드 wrapper: flex-shrink-0으로 크기 고정 + 'card' 클래스로 식별
<div key={idx} className='flex-shrink-0 card'>
<ExperienceCard />
</div>
))}
// Add a props interface so this component can receive data and a limit
interface PopularExperiencesProps {
experiences?: ExperienceData[];
maxItems?: number;
}
export default function PopularExperiences({
experiences = [],
maxItems = 4,
}: PopularExperiencesProps) {
// ... existing code ...
{(experiences.length > 0 ? experiences : [...Array(maxItems)])
.slice(0, maxItems)
.map((experience, idx) => (
// 카드 wrapper: flex-shrink-0으로 크기 고정 + 'card' 클래스로 식별
<div key={idx} className='flex-shrink-0 card'>
<ExperienceCard data={experience} />
</div>
))}
}
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx around lines 47 to
52, the code currently renders a fixed array of 4 cards, which limits
scalability. Modify the component to accept an array of card data as a prop and
map over this dynamic data to render ExperienceCard components accordingly. This
change will make the component more flexible and reusable with varying numbers
of cards.

</div>
</section>
Comment on lines +42 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

🛠️ Refactor suggestion

키보드 네비게이션 지원을 추가하고 데이터 구조를 개선하세요.

현재 구현에서 개선이 필요한 부분들:

  1. 키보드 네비게이션 미지원
  2. 하드코딩된 4개 카드 배열

키보드 이벤트 핸들러 추가:

+  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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* 가로 슬라이드 카드 리스트 */}
<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'>
<ExperienceCard />
</div>
))}
</div>
</section>
// (inside your PopularExperiences component)
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'ArrowLeft') {
scrollByCard('left');
} else if (event.key === 'ArrowRight') {
scrollByCard('right');
}
};
return (
<>
{/* 가로 슬라이드 카드 리스트 */}
<div
ref={sliderRef}
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='인기 체험 목록'
>
{[...Array(4)].map((_, idx) => (
// 카드 wrapper: flex-shrink-0으로 크기 고정 + 'card' 클래스로 식별
<div key={idx} className='flex-shrink-0 card'>
<ExperienceCard />
</div>
))}
</div>
</>
);
🤖 Prompt for AI Agents
In src/app/(with-header)/components/PopularExperiences.tsx around lines 42 to
54, the current horizontal slider lacks keyboard navigation support and uses a
hardcoded array of 4 cards. To fix this, add keyboard event handlers to enable
arrow key navigation for accessibility, and replace the hardcoded array with
dynamic data passed via props or fetched from an API. This will improve
usability and make the component data-driven.

);
}
2 changes: 1 addition & 1 deletion src/app/(with-header)/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function SearchBar() {
};

return (
<section className='flex lg:w-full lg:max-w-1152 lg:ml-auto lg:mr-auto justify-center px-16 lg:px-0'>
<section className='flex lg:w-full lg:max-w-1200 lg:ml-auto lg:mr-auto justify-center px-16 lg:px-0'>
<div className='flex flex-col w-full gap-15 md:gap-32 px-24 py-16 md:px-24 md:py-32 rounded-[16px] bg-white shadow-md'>
<div className='flex items-start gap-2 mb-4'>
<h3 className='text-lg md:text-xl font-bold text-left'>
Expand Down
2 changes: 2 additions & 0 deletions src/app/(with-header)/page.tsx
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>
);
}
8 changes: 8 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,11 @@

--spacing: 0.0625rem;
}

.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}