Skip to content

Commit f76882b

Browse files
authored
Merge pull request #37 from geulDa/feat/#13/container-componets
✨Feat & ⚙️Setting : (행사,장소,주소 복사 ,주소 말풍선) 컴포넌트 구현 & 오버레이 파일 & 디자인 시스템 weight 값 변경
2 parents 9f63a86 + 70124e4 commit f76882b

File tree

9 files changed

+514
-20
lines changed

9 files changed

+514
-20
lines changed

src/pages/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { ControlBar, Header, Tag } from '@/shared/components';
22
import { Icon } from '@/shared/icons';
3+
import LocationBubble from '@/shared/components/container/LocationBubble';
4+
import LocationCard from '@/shared/components/container/LocationCard';
5+
import EventCard from '@/shared/components/container/EventCard';
6+
import AddressCopy from '@/shared/components/button/AddressCopy';
37

48
export default function Home() {
59
return (

src/shared/common/Overlay.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
interface OverlayProps {
2+
onClick?: () => void;
3+
opacity?: number;
4+
className?: string;
5+
}
6+
7+
const Overlay = ({ onClick, opacity = 40, className }: OverlayProps) => {
8+
return (
9+
<div
10+
onClick={onClick}
11+
className={`
12+
fixed inset-0 z-[40]
13+
transition-opacity duration-300
14+
${className ?? ''}
15+
`}
16+
style={{
17+
backgroundColor: `rgba(0,0,0,${opacity / 100})`,
18+
}}
19+
/>
20+
);
21+
};
22+
export default Overlay;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use client';
2+
import { Icon } from '@/shared/icons';
3+
import { cn } from '@/shared/lib';
4+
import { cva, type VariantProps } from 'class-variance-authority';
5+
import { useState } from 'react';
6+
7+
8+
const addressCopyStyle = cva(
9+
'flex items-center justify-start flex-shrink-0 rounded-full transition-all duration-200',
10+
{
11+
variants: {
12+
variant: {
13+
gray: 'border border-gray-300 bg-gray-50 text-gray-400',
14+
mint: 'border border-mint-300 bg-mint-50 text-mint-500',
15+
},
16+
},
17+
defaultVariants: {
18+
variant: 'gray',
19+
},
20+
},
21+
);
22+
23+
interface AddressCopyProps
24+
extends React.HTMLAttributes<HTMLDivElement>,
25+
VariantProps<typeof addressCopyStyle> {
26+
label?: string;
27+
value?: string;
28+
}
29+
30+
const AddressCopy = ({
31+
label = 'address copy',
32+
value = label,
33+
variant = 'gray',
34+
className,
35+
...props
36+
}: AddressCopyProps) => {
37+
const [copied, setCopied] = useState(false);
38+
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+
}
47+
};
48+
49+
return (
50+
<div
51+
onClick={handleCopy}
52+
className={cn(
53+
addressCopyStyle({ variant }),
54+
'w-[35.4rem] h-[4rem] px-[1.3rem] py-[1rem] gap-[0.4rem]',
55+
'cursor-pointer select-none',
56+
className,
57+
)}
58+
{...props}
59+
>
60+
<Icon
61+
name='CopySimple'
62+
size={18}
63+
color={variant === 'mint' ? 'mint-400' : 'gray-400'}
64+
/>
65+
<span
66+
className={cn(
67+
'text-label-lg',
68+
variant === 'mint' ? 'text-mint-400' : 'text-gray-400',
69+
)}
70+
>
71+
{label}
72+
</span>
73+
</div>
74+
);
75+
};
76+
77+
export default AddressCopy;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { cva, type VariantProps } from 'class-variance-authority';
2+
import { cn } from '@/shared/lib';
3+
4+
const cardStyle = cva(
5+
'flex flex-shrink-0 rounded-[1.6rem] transition-all duration-200',
6+
{
7+
variants: {
8+
variant: {
9+
gray: 'border border-gray-300 bg-gray-50',
10+
mint: 'border border-mint-300 bg-mint-50',
11+
},
12+
size: {
13+
medium:
14+
'w-[17rem] h-[17rem] flex-col items-center justify-center p-[1.2rem]',
15+
large:
16+
'w-[35.4rem] h-[12rem] flex-row items-start justify-start px-[1.3rem] pt-[1.1rem] pb-[3.3rem]',
17+
},
18+
},
19+
defaultVariants: {
20+
variant: 'gray',
21+
size: 'medium',
22+
},
23+
},
24+
);
25+
26+
interface CardProps
27+
extends React.HTMLAttributes<HTMLDivElement>,
28+
VariantProps<typeof cardStyle> {
29+
customHeight?: string;
30+
}
31+
32+
export const Card = ({
33+
className,
34+
variant,
35+
size,
36+
customHeight,
37+
...props
38+
}: CardProps) => {
39+
const customPadding =
40+
customHeight && size === 'large' ? 'px-[1.5rem] py-[1.4rem]' : '';
41+
return (
42+
<div
43+
className={cn(cardStyle({ variant, size }), customPadding, className)}
44+
style={customHeight ? { height: customHeight } : undefined}
45+
{...props}
46+
/>
47+
);
48+
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Icon } from '@/shared/icons';
2+
import { Card } from '@/shared/components/container/Card';
3+
import { cn } from '@/shared/lib';
4+
import Image from 'next/image';
5+
6+
interface EventCardProps {
7+
name: string;
8+
address: string;
9+
description: string;
10+
variant?: 'gray' | 'mint';
11+
size?: 'medium' | 'large';
12+
imageSrc?: string;
13+
}
14+
15+
const EventCard = ({
16+
name,
17+
address,
18+
description,
19+
variant = 'gray',
20+
size = 'medium',
21+
imageSrc = '',
22+
}: EventCardProps) => {
23+
return (
24+
<Card
25+
variant={variant}
26+
size={size}
27+
customHeight={size === 'large' ? '13rem' : undefined}
28+
>
29+
{/* Medium 카드 */}
30+
{size === 'medium' ? (
31+
<div className='flex flex-col justify-between w-full'>
32+
{/* 행사 사진 */}
33+
<div className='relative w-full h-[9rem] rounded-[2rem] mb-[1rem] overflow-hidden'>
34+
{imageSrc ? (
35+
<Image
36+
src={imageSrc}
37+
alt={name}
38+
fill
39+
className='object-cover'
40+
//성능향상 - 이미지 최적화
41+
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 142px'
42+
loading='lazy'
43+
/>
44+
) : (
45+
<div className='w-full h-full bg-gray-200' />
46+
)}
47+
</div>
48+
49+
{/* 행사 이름 + 하트 */}
50+
<div className='flex items-center justify-between w-full mb-[0.2rem]'>
51+
<span
52+
className={cn(
53+
'text-label-lg truncate', //1줄 초과
54+
variant === 'mint' ? 'text-mint-800' : 'text-gray-900',
55+
)}
56+
>
57+
{name}
58+
</span>
59+
<Icon
60+
name='HeartStraight'
61+
size={20}
62+
color={variant === 'mint' ? 'mint-400' : 'gray-300'}
63+
/>
64+
</div>
65+
{/* 행사 주소 */}
66+
<div
67+
className={cn(
68+
'text-body-md truncate', //1줄 초과
69+
variant === 'mint' ? 'text-mint-500' : 'text-gray-500',
70+
)}
71+
>
72+
{address}
73+
</div>
74+
</div>
75+
) : (
76+
// Large 카드
77+
<div className='flex items-start justify-between w-full gap-[1.2rem]'>
78+
{/* 행사 이미지 */}
79+
<div className='relative w-[14.2rem] h-[10rem] rounded-[2rem] flex-shrink-0 overflow-hidden'>
80+
{imageSrc ? (
81+
<Image
82+
src={imageSrc}
83+
alt={name}
84+
fill
85+
className='object-cover'
86+
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 14.2rem'
87+
loading='lazy'
88+
/>
89+
) : (
90+
<div className='absolute inset-0 bg-gray-200' />
91+
)}
92+
</div>
93+
94+
{/* 행사 제목 + 하트 */}
95+
<div className='flex flex-col justify-between flex-1'>
96+
<div className='flex items-start justify-between w-full'>
97+
<span
98+
className={cn(
99+
'text-label-lg truncate', //1줄 초과
100+
variant === 'mint' ? 'text-mint-800' : 'text-gray-900',
101+
)}
102+
>
103+
{name}
104+
</span>
105+
<Icon
106+
name='HeartStraight'
107+
size={20}
108+
color={variant === 'mint' ? 'mint-400' : 'gray-300'}
109+
/>
110+
</div>
111+
112+
{/* 행사 설명 */}
113+
<p
114+
className={cn(
115+
'text-body-md mt-[1rem] line-clamp-4', //4줄 초과
116+
variant === 'mint' ? 'text-mint-500' : 'text-gray-500',
117+
)}
118+
>
119+
{description}
120+
</p>
121+
</div>
122+
</div>
123+
)}
124+
</Card>
125+
);
126+
};
127+
128+
export default EventCard;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Icon } from '@/shared/icons';
2+
import { cva } from 'class-variance-authority';
3+
import { cn } from '@/shared/lib';
4+
import Image from 'next/image';
5+
import { useRouter } from 'next/router';
6+
7+
interface LocationBubbleProps {
8+
name: string;
9+
imageSrc?: string;
10+
className?: string;
11+
}
12+
13+
const bubbleVariants = cva(
14+
'relative flex flex-col w-[20.4rem] h-[15.5rem] px-[1.1rem] pt-[1.35rem] bg-white rounded-[2rem] shadow-[0_0.7rem_0.7rem_0_rgba(0,0,0,0.25)]',
15+
);
16+
17+
const LocationBubble = ({ name, imageSrc, className }: LocationBubbleProps) => {
18+
const router = useRouter();
19+
// 말풍선 클릭시 - 장소 세부 페이지로 이동
20+
// 추후 페이지 작업시 맞는 경로 바꾸면 됩니다.
21+
const handleClick = () => {
22+
router.push('/location');
23+
};
24+
25+
return (
26+
<div onClick={handleClick} className={cn(bubbleVariants(), className)}>
27+
{/* 장소 사진 */}
28+
<div className='relative h-[10.3rem] w-full rounded-[0.8rem] overflow-hidden'>
29+
{imageSrc ? (
30+
<Image
31+
src={imageSrc}
32+
alt={name}
33+
fill
34+
className='object-cover'
35+
sizes='(max-width: 768px) 100vw, 204px'
36+
loading='lazy'
37+
/>
38+
) : (
39+
<div className='absolute inset-0 bg-gray-200' />
40+
)}
41+
</div>
42+
43+
{/* 지도핀 + 장소 이름 + 아이콘 */}
44+
<div className='flex items-center justify-between w-full mt-[0.6rem]'>
45+
<div className='flex items-center gap-[0.6rem] min-w-0'>
46+
<Icon name='MapPin' size={24} color='gray-300' />
47+
<span className='text-label-lg truncate'>{name}</span>
48+
</div>
49+
50+
<div className='flex-shrink-0'>
51+
<Icon name='backto' size={24} color='gray-300' rotate={180} />
52+
</div>
53+
</div>
54+
55+
{/* 말풍선 꼬리 */}
56+
<div
57+
className='
58+
absolute left-1/2 -bottom-[1.8rem] -translate-x-1/2
59+
w-[2.4rem] h-[2.7rem] bg-white
60+
rounded-b-[2rem]
61+
[clip-path:polygon(50%_100%,68%_88%,100%_30%,0_30%,32%_88%)]
62+
'
63+
/>
64+
</div>
65+
);
66+
};
67+
68+
export default LocationBubble;

0 commit comments

Comments
 (0)