-
Notifications
You must be signed in to change notification settings - Fork 0
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
[feat] 로그인 온보딩 디자인 구현 #39
base: develop
Are you sure you want to change the base?
Changes from all commits
2a8aa8f
69dcdcb
26d41cf
aa501a5
9fb9dcc
34b2641
2c4eb1e
4cc9d6f
2df95e5
b4342b8
ea3daec
08f571e
8048241
c874a94
fbbcc30
4ab73be
201e8a5
78ecbf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,30 @@ | ||
import KakaoLoginButton from '@/features/auth/ui/KakaoLoginButton'; | ||
'use client'; | ||
|
||
import { useState } from 'react'; | ||
|
||
import { LoginFeature } from '@/features/auth/login/ui/LoginFeature'; | ||
import { OnboardingFeature } from '@/features/onboarding/ui/OnboardingFeature'; | ||
|
||
export default function LoginPage() { | ||
const [showOnboarding, setShowOnboarding] = useState(true); | ||
|
||
const handleOnboardingComplete = () => { | ||
setShowOnboarding(false); | ||
}; | ||
|
||
function page() { | ||
return ( | ||
<div className="space-y-4"> | ||
<KakaoLoginButton /> | ||
<div className="flex h-dvh w-full justify-center"> | ||
<div | ||
className="h-full w-full max-w-[600px] flex-col items-center justify-center bg-black" | ||
style={{ | ||
backgroundImage: `url(${'/images/login/onboarding/onboarding-bg.png'})`, | ||
backgroundSize: 'contain', | ||
backgroundPosition: 'bottom', | ||
backgroundRepeat: 'no-repeat', | ||
}} | ||
> | ||
{showOnboarding ? <OnboardingFeature onComplete={handleOnboardingComplete} /> : <LoginFeature />} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default page; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import Image from 'next/image'; | ||
|
||
import KakaoLoginButton from '../../ui/KakaoLoginButton'; | ||
|
||
export function LoginFeature() { | ||
const imgUrl = '/images/login/onboarding/onboarding-login.png'; | ||
return ( | ||
<div className="flex h-full w-full flex-col"> | ||
<div className="relative flex h-full w-full flex-col items-center justify-between"> | ||
{/* 이미지 배치가 안되서 임시로 top-[45%]사용했습니다. */} | ||
<div className="absolute left-1/2 top-[45%] h-[23%] w-full -translate-x-1/2 -translate-y-1/2 transform"> | ||
{/* 이미지 가운데 배치가 안되서 임시로 pl-[1rem]사용했습니다. */} | ||
<Image src={imgUrl} alt="온보딩 이미지" fill className="object-contain pl-[1rem]" priority /> | ||
Comment on lines
+10
to
+13
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. 넵넵 확인했습니다. 추후 온보딩 디자인이 변경될 가능성이 높아서, 우선은 이와같이 진행해도 될 것 같습니다. 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. 넵 일단 진행하겠습니다! |
||
</div> | ||
|
||
<div className="absolute bottom-0 w-full space-y-4 px-6 pb-10"> | ||
<KakaoLoginButton /> | ||
<KakaoLoginButton /> {/* 구글 , 애플 로그인 버튼으로 바꿀 예정 */} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export const slides = [ | ||
{ | ||
id: 1, | ||
description: ['과한 시작정보는 빼고', '내가 원하는 정보만'], | ||
imageUrl: '/images/login/onboarding/onboarding-1.png', | ||
}, | ||
{ | ||
id: 2, | ||
description: ['QR로 쉽게 공유하고', '블루투스로 쉽게 아카이빙하는'], | ||
imageUrl: '/images/login/onboarding/onboarding-2.png', | ||
}, | ||
{ | ||
id: 3, | ||
description: ['손쉽게 툭 공유하는', '나만의 명함'], | ||
imageUrl: '/images/login/onboarding/onboarding-3.png', | ||
}, | ||
]; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,85 @@ | ||||||||||||||||||||||
'use client'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import React, { useState, useRef } from 'react'; | ||||||||||||||||||||||
import { Swiper as SwiperType } from 'swiper'; | ||||||||||||||||||||||
import { Navigation, Autoplay, Pagination } from 'swiper/modules'; | ||||||||||||||||||||||
import { Swiper, SwiperSlide } from 'swiper/react'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import 'swiper/css'; | ||||||||||||||||||||||
import 'swiper/css/pagination'; | ||||||||||||||||||||||
import 'swiper/css/navigation'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import { slides } from '@/features/onboarding/config/slides'; | ||||||||||||||||||||||
import { Button } from '@/shared/ui/button'; | ||||||||||||||||||||||
import { Typography } from '@/shared/ui/typography'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import OnboardingSlide from './OnboardingSlide'; | ||||||||||||||||||||||
|
||||||||||||||||||||||
interface OnboardingCarouselProps { | ||||||||||||||||||||||
onComplete: () => void; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
function OnboardingCarousel({ onComplete }: OnboardingCarouselProps) { | ||||||||||||||||||||||
const [activeIndex, setActiveIndex] = useState(0); | ||||||||||||||||||||||
const swiperRef = useRef<SwiperType | null>(null); | ||||||||||||||||||||||
|
||||||||||||||||||||||
const handleNext = () => { | ||||||||||||||||||||||
if (activeIndex === slides.length - 1) { | ||||||||||||||||||||||
onComplete(); | ||||||||||||||||||||||
} else if (swiperRef.current) { | ||||||||||||||||||||||
swiperRef.current.slideNext(); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
const handleSlideChange = (swiper: SwiperType) => { | ||||||||||||||||||||||
setActiveIndex(swiper.realIndex); | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
return ( | ||||||||||||||||||||||
<div className="flex h-full w-full flex-col items-center"> | ||||||||||||||||||||||
<div className="relative w-full flex-1"> | ||||||||||||||||||||||
{/*module Pagination사용으로 수정 */} | ||||||||||||||||||||||
<Swiper | ||||||||||||||||||||||
modules={[Pagination, Navigation, Autoplay]} | ||||||||||||||||||||||
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. modules 배열에 잘못된 컴포넌트가 포함되어 있습니다. modules 배열에 로컬에서 정의한 다음과 같이 수정해주세요: - modules={[Pagination, Navigation, Autoplay]}
+ modules={[Navigation, Autoplay]} 📝 Committable suggestion
Suggested change
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. 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. 🧩 Analysis chain@semnil5202 감사합니다! 제가 제안한 이전 리뷰가 잘못되었네요. 스와이퍼의 현재 코드를 보니 제 리뷰 제안을 취소합니다. 올바른 수정은 아래와 같을 것입니다: // Pagination 모듈 추가로 import
import { Navigation, Autoplay, Pagination } from 'swiper/modules';
// 그리고 modules 배열에 Pagination 추가
modules={[Pagination, Navigation, Autoplay]} 또는 현재 커스텀 Pagination을 사용하고 있다면, modules 배열에서 제거하는 것이 맞습니다: modules={[Navigation, Autoplay]} 제공해주신 레퍼런스 링크를 확인하겠습니다. 혼란을 드려 죄송합니다. 🏁 Script executed:
Length of output: 827 🌐 Web query:
💡 Result: To implement pagination in Swiper React correctly, follow these key steps: Core Implementation
npm install swiper
import { Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/pagination';
export default () => {
return (
<Swiper
modules={[Pagination]}
pagination={{ clickable: true }}
>
<SwiperSlide>Slide 1</SwiperSlide>
<SwiperSlide>Slide 2</SwiperSlide>
<SwiperSlide>Slide 3</SwiperSlide>
</Swiper>
);
}; Key Configuration Options
Advanced Usage Example: <Swiper
modules={[Pagination]}
pagination={{
type: 'progressbar',
clickable: true,
dynamicBullets: true,
renderBullet: (index, className) => {
return `<span class="${className}">${index + 1}</span>`;
}
}}
> Common Issues & Solutions
.swiper-pagination-bullet {
background: #000;
opacity: 0.5;
width: 12px;
height: 12px;
}
.swiper-pagination-bullet-active {
background: #ff0000;
opacity: 1;
}
<Swiper
pagination={{
el: '.custom-pagination',
...
}}
className="my-swiper"
>
<div className="custom-pagination"></div> Pro Tip: For TypeScript users, utilize the The pagination module works with Swiper's responsive breakpoints system and supports touch/swipe navigation out of the box. For complex implementations, combine with other modules like Navigation and Scrollbar using the same pattern[7][9]. Citations:
@semnil5202 죄송합니다! 제 리뷰가 부정확했습니다. 코드를 다시 검토해보니 현재 코드에서는 커스텀 제가 살펴본 결과:
올바른 접근 방식은 다음 두 가지 중 하나입니다:
import { Navigation, Autoplay, Pagination } from 'swiper/modules';
// 그리고 modules={[Pagination, Navigation, Autoplay]}
modules={[Navigation, Autoplay]} // Pagination 제외
// 커스텀 Pagination 컴포넌트는 JSX에서 별도로 렌더링 제공해주신 레퍼런스를 통해 올바른 사용법을 확인할 수 있었습니다. 잘못된 리뷰로 혼란을 드려 죄송합니다. ✏️ Learnings added
|
||||||||||||||||||||||
onSwiper={(swiper) => { | ||||||||||||||||||||||
swiperRef.current = swiper; | ||||||||||||||||||||||
}} | ||||||||||||||||||||||
onSlideChange={handleSlideChange} | ||||||||||||||||||||||
pagination={{ | ||||||||||||||||||||||
clickable: true, | ||||||||||||||||||||||
el: '.custom-pagination', | ||||||||||||||||||||||
bulletClass: 'inline-block h-2 w-2 rounded-full mx-1 bg-white opacity-50', | ||||||||||||||||||||||
bulletActiveClass: 'opacity-100', | ||||||||||||||||||||||
}} | ||||||||||||||||||||||
navigation={false} | ||||||||||||||||||||||
slidesPerView={1} | ||||||||||||||||||||||
autoplay={{ delay: 5000, disableOnInteraction: false }} | ||||||||||||||||||||||
loop={true} | ||||||||||||||||||||||
className="h-full w-full" | ||||||||||||||||||||||
> | ||||||||||||||||||||||
{slides.map((slide) => ( | ||||||||||||||||||||||
<SwiperSlide key={slide.id}> | ||||||||||||||||||||||
<OnboardingSlide description={slide.description} imageUrl={slide.imageUrl} /> | ||||||||||||||||||||||
</SwiperSlide> | ||||||||||||||||||||||
))} | ||||||||||||||||||||||
</Swiper> | ||||||||||||||||||||||
|
||||||||||||||||||||||
{/** | ||||||||||||||||||||||
* 커스텀 페이지네이션 위치 | ||||||||||||||||||||||
* 반응형으로 위치 조정 | ||||||||||||||||||||||
*/} | ||||||||||||||||||||||
<div className="absolute bottom-28 left-0 right-0 z-10 sm:bottom-36 md:bottom-40 lg:bottom-48"> | ||||||||||||||||||||||
<div className="custom-pagination flex justify-center"></div> | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
|
||||||||||||||||||||||
<div className="mt-auto w-full bg-black p-6"> | ||||||||||||||||||||||
<Button onClick={handleNext} className="w-full"> | ||||||||||||||||||||||
<Typography variant="body-2">다음</Typography> | ||||||||||||||||||||||
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. 만든 타이포컴포넌트 사용하신부분 좋습니다!! 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. 너무 편리합니다 ㅎㅎ 체고 |
||||||||||||||||||||||
</Button> | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
export default OnboardingCarousel; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
'use client'; | ||
|
||
import OnboardingCarousel from './OnboardingCarousel'; | ||
import { OnboardingHeader } from './OnboardingHeader'; | ||
|
||
/** | ||
* OnboardingFeatureProps 설명 | ||
* onComplete , showOnboarding 상태를 false로 바구는 함수입니다. | ||
* 헤더의 건너뛰기 , 모든 온보딩 페이지를 다 확인했을 때 사용합니다. | ||
* onComplete = 온보딩 마지막 페이지에서 사용 | ||
*/ | ||
|
||
interface OnboardingFeatureProps { | ||
onComplete: () => void; | ||
} | ||
|
||
export function OnboardingFeature({ onComplete }: OnboardingFeatureProps) { | ||
return ( | ||
<div className="flex h-full w-full flex-col"> | ||
<OnboardingHeader onComplete={onComplete} /> | ||
<OnboardingCarousel onComplete={onComplete} /> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Typography } from '@/shared/ui/typography'; | ||
|
||
interface OnboardingHeaderProps { | ||
onComplete: () => void; | ||
} | ||
|
||
export function OnboardingHeader({ onComplete }: OnboardingHeaderProps) { | ||
return ( | ||
<header className="flex items-center justify-between p-4"> | ||
<div></div> | ||
<button onClick={onComplete} className="font-medium text-gray-500"> | ||
<Typography variant="body-2">건너뛰기</Typography> | ||
</button> | ||
</header> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import Image from 'next/image'; | ||
|
||
import { Typography } from '@/shared/ui/typography'; | ||
|
||
interface OnboardingSlideProps { | ||
description: string[]; | ||
imageUrl: string; | ||
} | ||
|
||
function OnboardingSlide({ description, imageUrl }: OnboardingSlideProps) { | ||
return ( | ||
<div className="relative flex h-full w-full flex-col items-center"> | ||
<div className="absolute inset-0 flex w-full items-center justify-center"> | ||
<div className="relative h-[75%] w-[65%]"> | ||
<Image src={imageUrl} alt="온보딩 이미지" fill className="object-contain" priority /> | ||
</div> | ||
</div> | ||
|
||
<div className="absolute bottom-0 left-0 right-0 h-2/5 bg-gradient-to-t from-black via-black to-transparent pt-32"> | ||
<div className="flex flex-col items-center px-6 pt-16"> | ||
{description.map((line, index) => ( | ||
<Typography key={index} variant="title-2"> | ||
{line} | ||
</Typography> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default OnboardingSlide; |
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.
../
이상의 폴더 접근은 @shared/ui로 절대경로로 접근해주시면 더 좋을거 같습니다.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.
엇 수정하겠습니다. 🥲