Skip to content

Commit 7ffd1ce

Browse files
author
jyn
committed
Merge branch 'develop' into feature/jeon
2 parents c17cb98 + 57994be commit 7ffd1ce

File tree

21 files changed

+661
-209
lines changed

21 files changed

+661
-209
lines changed

src/api/postUpload.js

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

src/assets/images/img-01.svg

Lines changed: 89 additions & 0 deletions
Loading

src/assets/images/img-02.png

83.3 KB
Loading

src/assets/images/img_01.png

-112 KB
Binary file not shown.

src/assets/images/img_02.png

-80 KB
Binary file not shown.

src/components/Card/Card.module.scss

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
box-shadow: 0px 2px 12px 0px rgba(0, 0, 0, 0.08);
1111
background: $white;
1212
gap: 16px;
13-
transition: transform 0.2s ease;
13+
transition: transform 0.2s ease-in-out;
1414
cursor: pointer;
1515

1616
&:hover {
17-
transform: scale(0.95);
17+
transform: scale(0.95) translateZ(0);
1818
}
1919

2020
&.card--empty {
@@ -117,13 +117,13 @@
117117

118118
@keyframes click-pop {
119119
0% {
120-
transform: scale(1);
120+
transform: scale(1) translateZ(0);
121121
}
122122
50% {
123-
transform: scale(1.03); // 살짝 커졌다가
123+
transform: scale(1.03) translateZ(0); // 살짝 커졌다가
124124
}
125125
100% {
126-
transform: scale(1); // 다시 원래대로
126+
transform: scale(1) translateZ(0); // 다시 원래대로
127127
}
128128
}
129129

@@ -133,13 +133,13 @@
133133
}
134134
}
135135

136-
@media (max-width: 1024px) {
136+
@media (max-width: 1023px) {
137137
.card {
138138
height: 284px;
139139
}
140140
}
141141

142-
@media (max-width: 768px) {
142+
@media (max-width: 767px) {
143143
.card {
144144
height: 230px;
145145

src/components/Modal/Modal.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
}
111111
}
112112

113-
@media (max-width: 768px) {
113+
@media (max-width: 767px) {
114114
.modal {
115115
width: calc(100% - 48px);
116116
}

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/postUpload.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: 35 additions & 1 deletion
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 {
@@ -51,6 +52,36 @@
5152
width: 56px;
5253
border: 1px solid $gray-200;
5354
border-radius: 100px;
55+
transition: transform 0.1s ease-in-out;
56+
cursor: pointer;
57+
58+
&:hover {
59+
transform: scale(1.15) translateY(-6px) translateZ(0);
60+
}
61+
62+
&:active {
63+
transform: scale(1) translateY(0) translateZ(0);
64+
}
65+
}
66+
67+
&__image--selected {
68+
transform: scale(1.2) translateY(-6px) translateZ(0);
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+
}
5485
}
5586

5687
@media (max-width: 767px) {
@@ -69,8 +100,11 @@
69100
row-gap: 4px;
70101
}
71102

72-
&__image {
103+
&__image,
104+
&__image--loading {
73105
width: 40px;
106+
height: 40px;
107+
background-size: 400% 400%;
74108
}
75109
}
76110
}

src/components/common/Button.module.scss

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,31 @@
99
background-color: $purple-600;
1010
@include font-18-bold;
1111
color: $white;
12+
transition:
13+
transform 0.2s ease-in-out,
14+
background-color 0.2s ease-in-out;
1215

1316
&:disabled {
1417
background-color: $gray-300;
1518
cursor: not-allowed;
19+
filter: opacity(50%);
1620

1721
&:hover,
1822
&:active,
1923
&:focus {
2024
background-color: $gray-300;
2125
border: $gray-300;
26+
transform: none;
2227
}
2328
}
2429

2530
&:hover {
31+
transform: scale(1.05) translateZ(0);
2632
background-color: $purple-700;
2733
}
2834

2935
&:active {
36+
transform: scale(0.97) translateZ(0);
3037
background-color: $purple-800;
3138
}
3239

@@ -63,6 +70,10 @@
6370
&--delete {
6471
max-width: 720px;
6572
}
73+
74+
&--primary {
75+
margin-bottom: 24px;
76+
}
6677
}
6778

6879
@media (max-width: 767px) {
@@ -71,14 +82,18 @@
7182
&--delete {
7283
min-width: 320px;
7384
}
85+
86+
&--primary {
87+
margin-bottom: 24px;
88+
}
7489
}
75-
}
7690

77-
@media (max-width: 1024px) {
78-
.button {
79-
&--delete {
80-
width: 100%;
81-
height: 55px;
91+
@media (max-width: 1024px) {
92+
.button {
93+
&--delete {
94+
width: 100%;
95+
height: 55px;
96+
}
8297
}
8398
}
8499
}

0 commit comments

Comments
 (0)