Skip to content

Commit 2362a0a

Browse files
authored
✨feat: 이미지 업로드 구현 및 홈 페이지 애니메이션 효과 추가
✨feat: 이미지 업로드 구현 및 홈 페이지 애니메이션 효과 추가
2 parents 81ef47f + 318e937 commit 2362a0a

File tree

9 files changed

+151
-25
lines changed

9 files changed

+151
-25
lines changed

src/api/postUploadImage.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { api } from './api';
2+
3+
export default async function uploadImage(file) {
4+
const formData = new FormData();
5+
formData.append('image', file);
6+
7+
try {
8+
const res = await api.post(
9+
`https://api.imgbb.com/1/upload?key=${import.meta.env.VITE_API_KEY}`,
10+
formData,
11+
{
12+
headers: {
13+
'Content-Type': 'multipart/form-data',
14+
},
15+
},
16+
);
17+
return res.data.data.url;
18+
} catch (error) {
19+
console.error('이미지 업로드 실패', error);
20+
throw error;
21+
}
22+
}

src/assets/images/img-02.png

83.3 KB
Loading

src/assets/images/img_02.png

-28.3 KB
Binary file not shown.

src/components/UserProfileSelector/UserProfileSelector.jsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useState, useRef } from 'react';
22
import getProfileImages from '../../api/getProfileImages.js';
3+
import uploadImage from '../../api/postUpLoadImage.js';
34
import DEFAULT_PROFILE_IMAGE from '../../constants/image.js';
45
import styles from './UserProfileSelector.module.scss';
56

@@ -8,6 +9,8 @@ export default function UserProfileSelector({
89
onSelect,
910
}) {
1011
const [profileImages, setProfileImages] = useState([]);
12+
const [loadedImages, setLoadedImages] = useState({});
13+
const fileInput = useRef(null);
1114

1215
useEffect(() => {
1316
const loadImages = async () => {
@@ -17,18 +20,46 @@ export default function UserProfileSelector({
1720
loadImages();
1821
}, []);
1922

23+
const handleImageLoad = (url) => {
24+
setLoadedImages((prev) => ({ ...prev, [url]: true }));
25+
};
26+
27+
const handleUpload = async (e) => {
28+
const file = e.target.files[0];
29+
if (!file) return;
30+
31+
try {
32+
const uploadedUrl = await uploadImage(file);
33+
onSelect?.(uploadedUrl);
34+
} catch (error) {
35+
alert('이미지 업로드 실패했습니다.');
36+
}
37+
};
38+
39+
const triggerFileSeletor = () => {
40+
fileInput.current?.click();
41+
};
42+
2043
return (
2144
<div className={styles['profile-select']}>
2245
<h2 className={styles['profile-select__title']}>프로필 이미지</h2>
2346
<div className={styles['profile-select__content']}>
2447
<img
2548
src={value}
26-
alt="선택된 프로필"
49+
alt="선택된 프로필 및 업로드 이미지"
2750
className={styles['profile-select__selected-image']}
51+
onClick={triggerFileSeletor}
52+
/>
53+
<input
54+
type="file"
55+
accept="image/"
56+
onChange={handleUpload}
57+
ref={fileInput}
58+
style={{ display: 'none' }}
2859
/>
2960
<div className={styles['profile-select__right']}>
3061
<p className={styles['profile-select__description']}>
31-
프로필 이미지를 선택해주세요!
62+
프로필을 클릭해 업로드해 주세요!
3263
</p>
3364
<div className={styles['profile-select__image-list']}>
3465
{profileImages.map((url, idx) => (
@@ -38,8 +69,9 @@ export default function UserProfileSelector({
3869
alt={`profile-${idx}`}
3970
className={`${styles['profile-select__image']} ${
4071
value === url ? styles['profile-select__image--selected'] : ''
41-
}`}
72+
} ${!loadedImages[url] ? styles['profile-select__image--loading'] : ''}`}
4273
onClick={() => onSelect?.(url)}
74+
onLoad={() => handleImageLoad(url)}
4375
/>
4476
))}
4577
</div>

src/components/UserProfileSelector/UserProfileSelector.module.scss

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
&__selected-image {
2020
width: 80px;
2121
border-radius: 100px;
22+
cursor: pointer;
2223
}
2324

2425
&__content {
@@ -52,6 +53,7 @@
5253
border: 1px solid $gray-200;
5354
border-radius: 100px;
5455
transition: transform 0.1s ease-in-out;
56+
cursor: pointer;
5557

5658
&:hover {
5759
transform: scale(1.15) translateY(-6px) translateZ(0);
@@ -64,7 +66,22 @@
6466

6567
&__image--selected {
6668
transform: scale(1.2) translateY(-6px) translateZ(0);
67-
box-shadow: 0 0 0 2px $gray-300;
69+
box-shadow: 0 0 0 2px $gray-400;
70+
}
71+
72+
&__image--loading {
73+
background: linear-gradient(270deg, $gray-200, $gray-300, $gray-400);
74+
background-size: 400% 400%;
75+
animation: loadingSpin 1.2s infinite linear;
76+
}
77+
78+
@keyframes loadingSpin {
79+
0% {
80+
background-position: 0% 50%;
81+
}
82+
100% {
83+
background-position: 100% 50%;
84+
}
6885
}
6986

7087
@media (max-width: 767px) {
@@ -83,8 +100,11 @@
83100
row-gap: 4px;
84101
}
85102

86-
&__image {
103+
&__image,
104+
&__image--loading {
87105
width: 40px;
106+
height: 40px;
107+
background-size: 400% 400%;
88108
}
89109
}
90110
}

src/components/common/Button.module.scss

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
&--delete {
7171
min-width: 720px;
7272
}
73+
74+
&--primary {
75+
margin-bottom: 24px;
76+
}
7377
}
7478

7579
@media (max-width: 767px) {
@@ -78,14 +82,18 @@
7882
&--delete {
7983
min-width: 320px;
8084
}
85+
86+
&--primary {
87+
margin-bottom: 24px;
88+
}
8189
}
82-
}
8390

84-
@media (max-width: 1024px) {
85-
.button {
86-
&--delete {
87-
width: 100%;
88-
height: 55px;
91+
@media (max-width: 1024px) {
92+
.button {
93+
&--delete {
94+
width: 100%;
95+
height: 55px;
96+
}
8997
}
9098
}
9199
}

src/pages/Home/Home.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styles from './Home.module.scss';
2-
import img01 from '../../assets/images/img_01.svg';
3-
import img02 from '../../assets/images/img_02.png';
2+
import img01 from '../../assets/images/img-01.svg';
3+
import img02 from '../../assets/images/img-02.png';
44
import Button from '../../components/common/Button';
55
import { useNavigate } from 'react-router-dom';
66

@@ -21,7 +21,7 @@ export default function Home() {
2121
<img
2222
src={img01}
2323
alt="롤링페이퍼 이미지"
24-
className={styles.sectionImage}
24+
className={styles.sectionImagePaper}
2525
/>
2626
</article>
2727
</section>
@@ -40,7 +40,7 @@ export default function Home() {
4040
<img
4141
src={img02}
4242
alt="이모지 이미지"
43-
className={styles.sectionImage}
43+
className={styles.sectionImageEmoji}
4444
/>
4545
</article>
4646
</section>

src/pages/Home/Home.module.scss

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,39 @@ body {
3333

3434
border-radius: 16px;
3535
background-color: $surface;
36+
37+
opacity: 0;
38+
transition:
39+
transform 1.5s ease,
40+
opacity 1.5s ease;
3641
}
3742

3843
.rightImage {
3944
justify-content: space-between;
4045
margin-bottom: 30px;
46+
47+
transform: translateY(-30px);
48+
animation: slideDownFadeIn 1.2s ease forwards;
49+
animation-delay: 0.3s;
4150
}
4251

4352
.leftImage {
4453
flex-direction: row-reverse;
4554
justify-content: flex-end;
4655
margin-bottom: 48px;
56+
57+
transform: translateY(30px);
58+
animation: slideUpFadeIn 1.5s ease forwards;
59+
animation-delay: 0.6s;
4760
}
4861

4962
.sectionBox.leftImage {
5063
padding-left: 30px;
5164
}
5265

53-
.sectionImage {
66+
.sectionImagePaper,
67+
.sectionImageEmoji {
5468
max-width: 720px;
55-
height: 204px;
5669
}
5770

5871
.pointBadge {
@@ -88,7 +101,7 @@ body {
88101
color: $gray-500;
89102
}
90103

91-
@media (min-width: 768px) and (max-width: 1247px) {
104+
@media (min-width: 768px) and (max-width: 1023px) {
92105
.sectionBoxes {
93106
width: 100%;
94107
margin: 0 auto;
@@ -128,8 +141,6 @@ body {
128141
}
129142

130143
.title {
131-
font-size: 24px;
132-
line-height: 36px;
133144
white-space: nowrap;
134145
overflow: hidden;
135146
text-overflow: ellipsis;
@@ -140,7 +151,6 @@ body {
140151
}
141152

142153
.subtext {
143-
font-size: 18px;
144154
margin-top: 8px;
145155
}
146156
}
@@ -165,15 +175,35 @@ body {
165175
gap: 50px;
166176
}
167177

178+
.title {
179+
@include font-18-bold;
180+
}
181+
182+
.subtext {
183+
@include font-15-regular;
184+
}
185+
186+
.rightImage {
187+
gap: 45px;
188+
transform: translateY(-20px);
189+
}
190+
191+
.leftImage {
192+
gap: 48px;
193+
transform: translateY(20px);
194+
}
195+
168196
.rightImage,
169197
.leftImage {
170198
flex-direction: column;
171199
justify-content: center;
200+
min-width: 320px;
201+
overflow: hidden;
172202
}
173203

174-
.sectionImage {
175-
width: 100%;
176-
height: auto;
204+
.sectionImageEmoji,
205+
.sectionImagePaper {
206+
width: 150%;
177207
margin-bottom: 24px;
178208
}
179209

@@ -194,3 +224,17 @@ body {
194224
}
195225
}
196226
}
227+
228+
@keyframes slideDownFadeIn {
229+
to {
230+
transform: translateY(0);
231+
opacity: 1;
232+
}
233+
}
234+
235+
@keyframes slideUpFadeIn {
236+
to {
237+
transform: translateY(0);
238+
opacity: 1;
239+
}
240+
}

0 commit comments

Comments
 (0)