Skip to content

Commit 5ca327d

Browse files
authored
Merge pull request #50 from geulDa/feat/#43/login
✨Feat : 로그인 컴포넌트 & 로그인 레이아웃
2 parents 95b63c9 + 2e5b2a3 commit 5ca327d

File tree

18 files changed

+274
-20
lines changed

18 files changed

+274
-20
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"icons": "pnpm icons:clean && pnpm icons:gen"
1313
},
1414
"dependencies": {
15+
"@tanstack/react-query": "^5.90.5",
1516
"axios": "^1.12.2",
1617
"clsx": "^2.1.1",
1718
"next": "15.5.4",
@@ -28,13 +29,13 @@
2829
"@types/react": "^19",
2930
"@types/react-dom": "^19",
3031
"autoprefixer": "^10.4.21",
32+
"class-variance-authority": "^0.7.1",
3133
"eslint": "^9",
3234
"eslint-config-next": "15.5.4",
3335
"postcss": "^8.5.6",
3436
"svg-sprite-loader": "^6.0.11",
3537
"tailwindcss": "4.1.14",
3638
"typescript": "^5",
37-
"class-variance-authority": "^0.7.1",
3839
"@radix-ui/react-progress": "^1.1.7"
3940
}
4041
}

pnpm-lock.yaml

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/svgs/GoogleIcon.svg

Lines changed: 6 additions & 0 deletions
Loading

public/svgs/KakaoIcon.svg

Lines changed: 3 additions & 0 deletions
Loading

src/pages/_app.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
import '@/styles/globals.css';
22
import '@/shared/icons';
3+
import { useState } from 'react';
34
import type { AppProps } from 'next/app';
5+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
46

57
export default function App({ Component, pageProps }: AppProps) {
6-
return <Component {...pageProps} />;
8+
const [queryClient] = useState(
9+
() =>
10+
new QueryClient({
11+
defaultOptions: {
12+
queries: {
13+
staleTime: 60 * 1000,
14+
},
15+
},
16+
}),
17+
);
18+
19+
return (
20+
<QueryClientProvider client={queryClient}>
21+
<Component {...pageProps} />
22+
</QueryClientProvider>
23+
);
724
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SVGO 해결후 Image → Icon으로 교체 예정
2+
'use client';
3+
import Image from 'next/image';
4+
import { cva, type VariantProps } from 'class-variance-authority';
5+
import { cn } from '@/shared/lib';
6+
7+
8+
const loginButtonVariants = cva(
9+
`
10+
flex justify-center items-center flex-shrink-0
11+
w-[5rem] h-[5rem] rounded-full
12+
shadow-[0_0_4px_rgba(0,0,0,0.30)]
13+
transition-all duration-150 active:scale-95
14+
`,
15+
{
16+
variants: {
17+
platform: {
18+
google: 'bg-white',
19+
kakao: 'bg-[#FEE500]',
20+
},
21+
},
22+
defaultVariants: {
23+
platform: 'google',
24+
},
25+
}
26+
);
27+
28+
interface LoginButtonProps extends VariantProps<typeof loginButtonVariants> {
29+
onClick: () => void;
30+
className?: string;
31+
}
32+
33+
export default function LoginButton({
34+
onClick,
35+
platform,
36+
className,
37+
}: LoginButtonProps) {
38+
const iconData = {
39+
google: {
40+
src: '/svgs/GoogleIcon.svg',
41+
alt: 'Google Logo',
42+
width: 36,
43+
height: 36,
44+
label: '구글 로그인',
45+
},
46+
kakao: {
47+
src: '/svgs/KakaoIcon.svg',
48+
alt: 'Kakao Logo',
49+
width: 28,
50+
height: 28,
51+
label: '카카오 로그인',
52+
},
53+
};
54+
55+
const { src, alt, width, height, label } = iconData[platform ?? 'google'];
56+
57+
return (
58+
<button
59+
type="button"
60+
onClick={onClick}
61+
aria-label={label}
62+
className={cn(loginButtonVariants({ platform }), className)}
63+
>
64+
<Image src={src} alt={alt} width={width} height={height} priority />
65+
</button>
66+
);
67+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client';
2+
import { cva } from 'class-variance-authority';
3+
import { cn } from '@/shared/lib';
4+
5+
interface RecentLoginBubbleProps {
6+
className?: string;
7+
}
8+
9+
const bubbleVariants = cva(
10+
'relative inline-flex justify-center items-center px-[0.8rem] py-[0.4rem] bg-pink-200 rounded-[2rem]',
11+
);
12+
13+
const RecentLoginBubble = ({ className }: RecentLoginBubbleProps) => {
14+
return (
15+
<div className={cn(bubbleVariants(), className)}>
16+
<span className='text-label-sm text-white'>최근 로그인</span>
17+
{/* 말풍선 꼬리 */}
18+
<div
19+
className='
20+
absolute left-1/2 -bottom-[0.7rem] -translate-x-1/2
21+
w-[0.6rem] h-[0.8rem] bg-pink-200
22+
[clip-path:polygon(50%_100%,100%_0,0_0)]
23+
'
24+
/>
25+
</div>
26+
);
27+
};
28+
29+
export default RecentLoginBubble;

src/pages/auth/index.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use client';
2+
import { Icon } from '@/shared/icons';
3+
import { cn } from '@/shared/lib';
4+
import LoginButton from '@/pages/auth/components/LoginButton';
5+
import RecentLoginBubble from '@/pages/auth/components/RecentLoginBubble';
6+
7+
export default function LoginPage() {
8+
const handleLoginClick = (platform: string) => alert(`${platform} 로그인`);
9+
10+
return (
11+
<main className='w-full flex flex-col items-center bg-white'>
12+
{/* 그라데이션 영역 */}
13+
<div className='relative w-full h-[22vw] min-h-[14rem] max-h-[28rem]'>
14+
<svg
15+
xmlns='http://www.w3.org/2000/svg'
16+
viewBox='0 0 402 194'
17+
className='absolute inset-0 w-full h-full'
18+
preserveAspectRatio='none'
19+
>
20+
<path
21+
d='M402 180.922C257.925 56.9753 160.195 270.883 0 161.93V0H402V180.922Z'
22+
fill='url(#paint0_linear_mint)'
23+
/>
24+
<defs>
25+
<linearGradient
26+
id='paint0_linear_mint'
27+
x1='201.53'
28+
y1='0'
29+
x2='201'
30+
y2='163'
31+
gradientUnits='userSpaceOnUse'
32+
>
33+
<stop stopColor='#D9FAFB' />
34+
<stop offset='1' stopColor='#8CEFF2' />
35+
</linearGradient>
36+
</defs>
37+
</svg>
38+
</div>
39+
40+
{/* 콘텐츠 */}
41+
<section
42+
className={cn(
43+
'w-full flex flex-col items-center text-center',
44+
'px-[6.8rem] pt-[5.2rem]',
45+
)}
46+
>
47+
{/* 타이틀 */}
48+
<div className='flex flex-col items-center gap-[3.3rem] mb-[3.2rem]'>
49+
<h1 className='text-headline-lg-serif text-mint-900'>글다</h1>
50+
<p className='text-label-md text-mint-900'>
51+
만화 속 부천 여행
52+
<br />
53+
8개 명소를 탐험하고 엽서를 모아보세요!
54+
</p>
55+
</div>
56+
57+
{/* 로고 */}
58+
<div className='p-[3.2rem] mb-[3.2rem]'>
59+
<Icon name='Stamp' size={132} color='mint-400' />
60+
</div>
61+
62+
{/* 로그인 버튼 */}
63+
<div className='flex flex-col items-center gap-[2.1rem] relative'>
64+
<p className='text-label-lg text-gray-400'>start with</p>
65+
66+
{/* 최근 로그인 말풍선 */}
67+
<div className='absolute -translate-x-1/2 z-50'>
68+
<RecentLoginBubble />
69+
</div>
70+
71+
{/* 로그인 버튼 */}
72+
<div className='flex gap-[1.5rem]'>
73+
<LoginButton
74+
platform='kakao'
75+
onClick={() => handleLoginClick('카카오')}
76+
/>
77+
<LoginButton
78+
platform='google'
79+
onClick={() => handleLoginClick('구글')}
80+
/>
81+
</div>
82+
83+
<p className='text-label-md text-gray-400 cursor-pointer underline underline-offset-[0.25rem]'>
84+
비회원 로그인
85+
</p>
86+
</div>
87+
88+
{/* 안내문 */}
89+
<p className='mt-[5rem] text-label-sm text-gray-400'>
90+
비회원은 스탬프 저장과 공유 기능을 사용할 수 없습니다.
91+
</p>
92+
</section>
93+
</main>
94+
);
95+
}

src/shared/components/button/AddressCopy.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Icon } from '@/shared/icons';
33
import { cn } from '@/shared/lib';
44
import { cva, type VariantProps } from 'class-variance-authority';
55
import { useState } from 'react';
6-
6+
import { Copy } from '@/shared/utils/copy';
77

88
const addressCopyStyle = cva(
99
'flex items-center justify-start flex-shrink-0 rounded-full transition-all duration-200',
@@ -23,8 +23,8 @@ const addressCopyStyle = cva(
2323
interface AddressCopyProps
2424
extends React.HTMLAttributes<HTMLDivElement>,
2525
VariantProps<typeof addressCopyStyle> {
26-
label?: string;
27-
value?: string;
26+
label: string;
27+
value: string;
2828
}
2929

3030
const AddressCopy = ({
@@ -36,14 +36,15 @@ const AddressCopy = ({
3636
}: AddressCopyProps) => {
3737
const [copied, setCopied] = useState(false);
3838

39-
const handleCopy = async () => {
40-
try {
41-
await navigator.clipboard.writeText(value);
42-
setCopied(true);
43-
setTimeout(() => setCopied(false), 1500);
44-
} catch (err) {
45-
console.error('주소 복사 실패:', err);
46-
}
39+
const handleCopy = () => {
40+
Copy(
41+
value,
42+
() => {
43+
setCopied(true);
44+
setTimeout(() => setCopied(false), 1500);
45+
},
46+
undefined,
47+
);
4748
};
4849

4950
return (

0 commit comments

Comments
 (0)