Skip to content
8 changes: 7 additions & 1 deletion src/api/postUpload.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { api } from './api';

export default async function uploadImage(file) {
export default async function uploadImage(file, onProgress) {
const formData = new FormData();
formData.append('file', file);
formData.append('upload_preset', 'profile_upload');
Expand All @@ -14,6 +14,12 @@ export default async function uploadImage(file) {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (e) => {
if (onProgress) {
const percent = Math.round((e.loaded * 100) / e.total);
onProgress(percent);
}
},
},
);
return res.data.secure_url;
Expand Down
16 changes: 15 additions & 1 deletion src/components/BackgroundCard/BackgroundCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styles from './BackgroundCard.module.scss';
import uploadImage from '../../api/postUpload';
import checked from '../../assets/images/checked.svg';
import upload from '../../assets/images/upload.svg';
import UploadProgressBar from '../common/UploadProgressBar';

export default function BackgroundCard({
type, // 'color' 또는 'image' 또는 'upload'
Expand All @@ -13,7 +14,10 @@ export default function BackgroundCard({
isLoading,
onLoad,
onSelect,
isUploading,
setIsUploading,
}) {
const [uploadProgress, setUploadProgress] = useState(0);
const [imageUrl, setImageUrl] = useState(url);

const getClassName = () => {
Expand All @@ -33,12 +37,21 @@ export default function BackgroundCard({
const file = e.target.files[0];
if (!file) return;

e.target.value = null;

setIsUploading(true);

try {
const uploadedUrl = await uploadImage(file);
const uploadedUrl = await uploadImage(file, (percent) => {
setUploadProgress(percent);
});
setImageUrl(uploadedUrl);
onSelect?.(uploadedUrl);
} catch {
alert('이미지 업로드 실패했습니다.');
} finally {
setIsUploading(false);
setUploadProgress(0);
}
};

Expand Down Expand Up @@ -115,6 +128,7 @@ export default function BackgroundCard({
className={styles['background-card__check-icon']}
/>
)}
{isUploading && <UploadProgressBar progress={uploadProgress} />}
</li>
);
}
1 change: 1 addition & 0 deletions src/components/BackgroundCard/BackgroundCard.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.background-card__upload-btn {
display: flex;
Expand Down
40 changes: 31 additions & 9 deletions src/components/UserProfileSelector/UserProfileSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { useEffect, useState, useRef } from 'react';
import getProfileImages from '../../api/getProfileImages.js';
import uploadImage from '../../api/postUpload.js';
import DEFAULT_PROFILE_IMAGE from '../../constants/image.js';
import UploadProgressBar from '../common/UploadProgressBar.jsx';
import styles from './UserProfileSelector.module.scss';

export default function UserProfileSelector({
value = DEFAULT_PROFILE_IMAGE,
onSelect,
isUploading,
setIsUploading,
}) {
const [profileImages, setProfileImages] = useState([]);
const [loadedImages, setLoadedImages] = useState({});
const [uploadProgress, setUploadProgress] = useState(0);
const fileInput = useRef(null);

useEffect(() => {
Expand All @@ -28,11 +32,21 @@ export default function UserProfileSelector({
const file = e.target.files[0];
if (!file) return;

e.target.value = null;

setIsUploading(true);

try {
const uploadedUrl = await uploadImage(file);
const uploadedUrl = await uploadImage(file, (percent) => {
setUploadProgress(percent);
});

onSelect?.(uploadedUrl);
} catch (error) {
alert('이미지 업로드 실패했습니다.');
} finally {
setIsUploading(false);
setUploadProgress(0);
}
};

Expand All @@ -44,12 +58,17 @@ export default function UserProfileSelector({
<div className={styles['profile-select']}>
<h2 className={styles['profile-select__title']}>프로필 이미지</h2>
<div className={styles['profile-select__content']}>
<img
src={value}
alt="선택된 프로필 및 업로드 이미지"
className={styles['profile-select__selected-image']}
onClick={triggerFileSeletor}
/>
<div className={styles['profile-select__selected-wrapper']}>
<img
src={value}
alt="선택된 프로필 및 업로드 이미지"
className={styles['profile-select__selected-image']}
onClick={triggerFileSeletor}
/>

{isUploading && <UploadProgressBar progress={uploadProgress} />}
</div>

<input
type="file"
accept="image/*"
Expand All @@ -69,8 +88,11 @@ export default function UserProfileSelector({
alt={`profile-${idx}`}
className={`${styles['profile-select__image']} ${
value === url ? styles['profile-select__image--selected'] : ''
} ${!loadedImages[url] ? styles['profile-select__image--loading'] : ''}`}
onClick={() => onSelect?.(url)}
} ${!loadedImages[url] ? styles['profile-select__image--loading'] : ''}
${isUploading ? styles['profile-select__image--disabled'] : ''}`}
onClick={() => {
if (!isUploading) onSelect?.(url);
}}
onLoad={() => handleImageLoad(url)}
/>
))}
Expand Down
10 changes: 10 additions & 0 deletions src/components/UserProfileSelector/UserProfileSelector.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
cursor: pointer;
}

&__selected-wrapper {
position: relative;
}

&__content {
display: flex;
align-items: center;
Expand All @@ -49,6 +53,12 @@
gap: 4px;
}

&__image--disabled {
pointer-events: none;
opacity: 0.4;
filter: grayscale(60%);
}

&__image {
width: 56px;
border: 1px solid $gray-200;
Expand Down
11 changes: 11 additions & 0 deletions src/components/common/UploadProgressBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styles from './UploadProgressBar.module.scss';

export default function UploadProgressBar({ progress = 0 }) {
return (
<div className={styles.overlay}>
<div className={styles.bar}>
<div className={styles.fill} style={{ width: `${progress}%` }} />
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/common/UploadProgressBar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@use '../../assets/styles/variables.scss' as *;

.overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
inset: 0;
border-radius: 50%;
z-index: 10;
background-color: rgba(255, 255, 255, 0.6);
}

.bar {
overflow: hidden;
width: 60%;
height: 8px;
border-radius: 5px;
background-color: $gray-200;
}

.fill {
width: 0%;
height: 100%;
background-color: $purple-700;
transition: width 0.3s ease;
}
6 changes: 5 additions & 1 deletion src/pages/CreateRecipient/CreateRecipient.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default function CreateRecipient() {
const [imageLoading, setImageLoading] = useState([]);
const [uploadedImageUrl, setUploadedImageUrl] = useState(null);
const [isCreating, setIsCreating] = useState(false);
const [isUploading, setIsUploading] = useState(false);

const navigate = useNavigate();

useEffect(() => {
Expand Down Expand Up @@ -175,6 +177,8 @@ export default function CreateRecipient() {
onClick={() => handleImageClick(data.length + 1)}
isSelected={selectedImage === data.length + 1}
onSelect={handleUploadImage}
isUploading={isUploading}
setIsUploading={setIsUploading}
/>
</ul>
)}
Expand All @@ -184,7 +188,7 @@ export default function CreateRecipient() {
<Button
type="button"
onClick={handleButtonClick}
disabled={!value.trim() || isCreating}
disabled={!value.trim() || isCreating || isUploading}
>
생성하기
</Button>
Expand Down
5 changes: 4 additions & 1 deletion src/pages/MessageForm/MessageForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function MessageForm() {
const [font, setFont] = useState('Noto Sans');
const [isRestored, setIsRestored] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [isUploading, setIsUploading] = useState(false);

const stripHtml = (html) => html.replace(/<[^>]+>/g, '').trim();
const isValid = sender.trim() !== '' && stripHtml(message) !== '';
Expand Down Expand Up @@ -144,6 +145,8 @@ export default function MessageForm() {
<UserProfileSelector
value={profileImage}
onSelect={setProfileImage}
isUploading={isUploading}
setIsUploading={setIsUploading}
/>
</div>
<div className={styles['message-form__relationship-select']}>
Expand All @@ -165,7 +168,7 @@ export default function MessageForm() {
<Button
type="button"
onClick={handleSubmit}
disabled={!isValid || isCreating}
disabled={!isValid || isCreating || isUploading}
>
생성하기
</Button>
Expand Down