From 0212ef1215493bf11b11ad1bc07bc6bc3037962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B4=91=EB=AF=BC?= Date: Fri, 2 May 2025 13:01:37 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=97=90=20=EC=A7=84=ED=96=89=EB=A5=A0=20=EC=BD=9C=EB=B0=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/postUpload.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/postUpload.js b/src/api/postUpload.js index 4cfef03..428d4b4 100644 --- a/src/api/postUpload.js +++ b/src/api/postUpload.js @@ -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'); @@ -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; From ef9b88f0657f68e62b6855b237c8a681821cfb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B4=91=EB=AF=BC?= Date: Fri, 2 May 2025 14:40:06 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A7=84=ED=96=89=EB=A5=A0=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/UploadProgressBar.jsx | 11 ++++++++ .../common/UploadProgressBar.module.scss | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/components/common/UploadProgressBar.jsx create mode 100644 src/components/common/UploadProgressBar.module.scss diff --git a/src/components/common/UploadProgressBar.jsx b/src/components/common/UploadProgressBar.jsx new file mode 100644 index 0000000..c9b2c60 --- /dev/null +++ b/src/components/common/UploadProgressBar.jsx @@ -0,0 +1,11 @@ +import styles from './UploadProgressBar.module.scss'; + +export default function UploadProgressBar({ progress = 0 }) { + return ( +
+
+
+
+
+ ); +} diff --git a/src/components/common/UploadProgressBar.module.scss b/src/components/common/UploadProgressBar.module.scss new file mode 100644 index 0000000..5e9d0bc --- /dev/null +++ b/src/components/common/UploadProgressBar.module.scss @@ -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; +} From 7add8fcb1e512d6b8bea5da4e073d2e7d1991268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B4=91=EB=AF=BC?= Date: Fri, 2 May 2025 14:40:14 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A7=84=ED=96=89=EB=A5=A0=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=83=81=ED=83=9C=20=EB=B0=8F?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserProfileSelector.jsx | 40 ++++++++++++++----- .../UserProfileSelector.module.scss | 10 +++++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/components/UserProfileSelector/UserProfileSelector.jsx b/src/components/UserProfileSelector/UserProfileSelector.jsx index 4f919a5..c30dc28 100644 --- a/src/components/UserProfileSelector/UserProfileSelector.jsx +++ b/src/components/UserProfileSelector/UserProfileSelector.jsx @@ -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(() => { @@ -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); } }; @@ -44,12 +58,17 @@ export default function UserProfileSelector({

프로필 이미지

- 선택된 프로필 및 업로드 이미지 +
+ 선택된 프로필 및 업로드 이미지 + + {isUploading && } +
+ onSelect?.(url)} + } ${!loadedImages[url] ? styles['profile-select__image--loading'] : ''} + ${isUploading ? styles['profile-select__image--disabled'] : ''}`} + onClick={() => { + if (!isUploading) onSelect?.(url); + }} onLoad={() => handleImageLoad(url)} /> ))} diff --git a/src/components/UserProfileSelector/UserProfileSelector.module.scss b/src/components/UserProfileSelector/UserProfileSelector.module.scss index 6219311..da8bd1b 100644 --- a/src/components/UserProfileSelector/UserProfileSelector.module.scss +++ b/src/components/UserProfileSelector/UserProfileSelector.module.scss @@ -23,6 +23,10 @@ cursor: pointer; } + &__selected-wrapper { + position: relative; + } + &__content { display: flex; align-items: center; @@ -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; From b473ff59411e13af2bd7a905de8a758e38cbc5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B4=91=EB=AF=BC?= Date: Fri, 2 May 2025 14:40:20 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=ED=8F=BC=EC=97=90=EC=84=9C=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MessageForm/MessageForm.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/MessageForm/MessageForm.jsx b/src/pages/MessageForm/MessageForm.jsx index 9f1c757..8ef0e91 100644 --- a/src/pages/MessageForm/MessageForm.jsx +++ b/src/pages/MessageForm/MessageForm.jsx @@ -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) !== ''; @@ -144,6 +145,8 @@ export default function MessageForm() {
@@ -165,7 +168,7 @@ export default function MessageForm() { From dccc601ecab6eb208ada753e08eb399d730809e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B4=91=EB=AF=BC?= Date: Fri, 2 May 2025 14:58:54 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=EB=A5=A0=20=ED=91=9C=EC=8B=9C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BackgroundCard/BackgroundCard.jsx | 16 +++++++++++++++- .../BackgroundCard/BackgroundCard.module.scss | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/BackgroundCard/BackgroundCard.jsx b/src/components/BackgroundCard/BackgroundCard.jsx index f486a54..c83dc6d 100644 --- a/src/components/BackgroundCard/BackgroundCard.jsx +++ b/src/components/BackgroundCard/BackgroundCard.jsx @@ -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' @@ -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 = () => { @@ -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); } }; @@ -115,6 +128,7 @@ export default function BackgroundCard({ className={styles['background-card__check-icon']} /> )} + {isUploading && } ); } diff --git a/src/components/BackgroundCard/BackgroundCard.module.scss b/src/components/BackgroundCard/BackgroundCard.module.scss index 4fb1765..2e80b27 100644 --- a/src/components/BackgroundCard/BackgroundCard.module.scss +++ b/src/components/BackgroundCard/BackgroundCard.module.scss @@ -81,6 +81,7 @@ display: flex; justify-content: center; align-items: center; + position: relative; } .background-card__upload-btn { display: flex; From 589c32349802d95a0468187f913b8a1a6d112b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B4=91=EB=AF=BC?= Date: Fri, 2 May 2025 14:58:59 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CreateRecipient/CreateRecipient.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/CreateRecipient/CreateRecipient.jsx b/src/pages/CreateRecipient/CreateRecipient.jsx index a7ac884..2b930fb 100644 --- a/src/pages/CreateRecipient/CreateRecipient.jsx +++ b/src/pages/CreateRecipient/CreateRecipient.jsx @@ -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(() => { @@ -175,6 +177,8 @@ export default function CreateRecipient() { onClick={() => handleImageClick(data.length + 1)} isSelected={selectedImage === data.length + 1} onSelect={handleUploadImage} + isUploading={isUploading} + setIsUploading={setIsUploading} /> )} @@ -184,7 +188,7 @@ export default function CreateRecipient() {