diff --git a/src/pages/MessagePage/MessagePage.jsx b/src/pages/MessagePage/MessagePage.jsx index 6d20bf7..3c9e0cd 100644 --- a/src/pages/MessagePage/MessagePage.jsx +++ b/src/pages/MessagePage/MessagePage.jsx @@ -13,6 +13,8 @@ import { FONT_OPTIONS, FONT_DROPDOWN_ITEMS } from '@/constants/fontMap'; import { useEffect } from 'react'; import Button from '@/components/Button/Button'; import EditorWrapper from '@/pages/MessagePage/components/EditorWrapper'; +import logoIcon from '@/assets/icons/icon_logo_white.svg'; +import backIcon from '@/assets/icons/icon_back.svg'; // 상대와의 관계 옵션 const RELATIONSHIP_OPTIONS = ['친구', '지인', '동료', '가족']; @@ -87,6 +89,11 @@ function MessagePage() { navigate(`/post/${recipientId}`); // 실제 라우트에 맞게 수정 }; + const handleGoBackClick = (e) => { + e.preventDefault(); + navigate(-1); + }; + return (
@@ -160,16 +167,19 @@ function MessagePage() {
{/* 6) 전송 버튼 */} -
- + +
diff --git a/src/pages/MessagePage/MessagePage.module.scss b/src/pages/MessagePage/MessagePage.module.scss index 075e190..dca2031 100644 --- a/src/pages/MessagePage/MessagePage.module.scss +++ b/src/pages/MessagePage/MessagePage.module.scss @@ -34,38 +34,73 @@ } /* 전송 버튼 영역 */ - &__actions { + &__button-area { display: flex; flex-direction: column; - align-items: center; - justify-content: center; - } - &__spacer { - height: 200px; + gap: 30px; + + @include tablet { + flex-direction: row; + gap: 20px; + } + + @include mobile { + flex-direction: row; + gap: 20px; + } } &__submit { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + padding: 14px 0; background-color: var(--color-purple-600); - color: var(--color-white); - padding: 0.75rem 2rem; border: none; border-radius: 12px; - font-size: 1rem; - font-weight: 600; + font-weight: 700; + font-size: 18px; + line-height: 28px; + color: var(--color-white); cursor: pointer; - transition: background-color 0.3s ease; &:hover { background-color: var(--color-purple-700); } - &:active { - background-color: var(--color-purple-800); - } - &:disabled { - background-color: var(--color-gray-400); + background-color: var(--color-gray-300); cursor: not-allowed; } } + + &__back { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + padding: 14px 0; + background-color: var(--color-white); + border: 1px solid var(--color-purple-600); + border-radius: 12px; + font-weight: 700; + font-size: 18px; + line-height: 28px; + color: var(--color-purple-600); + cursor: pointer; + + &:hover { + background-color: var(--color-purple-100); + } + } + + &__button-logo { + aspect-ratio: 1; + width: 18px; + } } diff --git a/src/pages/MessagePage/components/ProfileSelector.jsx b/src/pages/MessagePage/components/ProfileSelector.jsx index 569c582..190db3b 100644 --- a/src/pages/MessagePage/components/ProfileSelector.jsx +++ b/src/pages/MessagePage/components/ProfileSelector.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; import { useApi } from '@/hooks/useApi'; import { getProfileImages } from '@/apis/profileImagesApi'; import ImagePreloader from '@/components/ImagePreloader'; @@ -7,6 +7,9 @@ import AVATAR_PLACEHOLDER from '@/assets/images/image_profile_default.svg'; import HorizontalScrollContainer from '@/components/HorizontalScrollContainer/HorizontalScrollContainer'; import GradientImage from '@/components/GradientImage/GradientImage'; import LoadingLabel from '@/components/LoadingLabel/LoadingLabel'; +import Button from '@/components/Button/Button'; +import { uploadImageToCloudinary } from '@/apis/syncApi/uploadImageToCloudinary'; +import { useToast } from '@/hooks/useToast'; /** * 프로필 이미지 선택기 @@ -16,78 +19,143 @@ import LoadingLabel from '@/components/LoadingLabel/LoadingLabel'; */ function ProfileSelector({ onSelectImage }) { + const { showToast } = useToast(); // 프로필 이미지 목록 가져오기 (API 호출) const { data: imageData, loading } = useApi(getProfileImages); - // imageData가 배열이 아닐 경우 빈 배열로 초기화 - const imageUrls = useMemo(() => { + const apiImageUrls = useMemo(() => { return Array.isArray(imageData?.imageUrls) ? imageData.imageUrls : []; }, [imageData]); // 선택된 이미지 URL 상태 const [selectedUrl, setSelectedUrl] = useState(''); + // 로딩된 이미지 개수 상태 const [loadedCount, setLoadedCount] = useState(0); - const allLoaded = imageUrls.length > 0 && loadedCount >= imageUrls.length; + // 모든 이미지가 로드되었는지 여부 + const allLoaded = apiImageUrls.length > 0 && loadedCount >= apiImageUrls.length; + + /* ---------------- 사용자 업로드 이미지 ---------------- */ + const [uploading, setUploading] = useState(false); // 업로드 진행 여부 + const fileInputRef = useRef(null); // 로딩 후, 선택된 이미지 URL이 없으면 첫 번째 URL을 기본값으로 설정 useEffect(() => { - if (!loading && imageUrls.length > 0 && !selectedUrl) { + if (!loading && apiImageUrls.length > 0 && !selectedUrl) { // 아직 선택된 값이 없으면 첫 번째 URL을 기본값으로 - setSelectedUrl((prev) => prev || imageUrls[0]); - onSelectImage && onSelectImage(imageUrls[0]); + setSelectedUrl((prev) => prev || apiImageUrls[0]); + onSelectImage && onSelectImage(apiImageUrls[0]); } - }, [loading, imageUrls, selectedUrl, onSelectImage]); + }, [loading, apiImageUrls, selectedUrl, onSelectImage]); const handleImageSelect = (url) => { setSelectedUrl(url); onSelectImage && onSelectImage(url); }; + const handleFileChange = async (e) => { + const file = e.target.files?.[0]; + if (!file || !file.type.startsWith('image/')) { + alert('이미지 파일만 업로드할 수 있습니다.'); + return; + } + + const previewUrl = URL.createObjectURL(file); + + setSelectedUrl(previewUrl); + onSelectImage?.(previewUrl); + setUploading(true); + // Cloudinary 업로드 시작 + try { + const uploadedUrl = await uploadImageToCloudinary(file); + setSelectedUrl(uploadedUrl); + onSelectImage?.(uploadedUrl); + } catch (err) { + console.error(err); + showToast?.({ + type: 'fail', + message: '프로필 이미지 업로드에 실패했습니다.', + }); + setSelectedUrl(''); + onSelectImage?.(''); + } finally { + setUploading(false); + URL.revokeObjectURL(previewUrl); + e.target.value = ''; + } + }; + const handleThumbLoad = () => setLoadedCount((c) => c + 1); + const uploadBtnClick = () => { + if (uploading) return; // 업로드 중이면 무시 + if (fileInputRef.current) { + fileInputRef.current.value = ''; // *** 핵심: 값 초기화 *** + fileInputRef.current.click(); // 파일 다이얼로그 오픈 + } + }; return ( -
- {/* 백그라운드에서 모든 이미지 미리 로드 */} - - {/* 현재 선택된 이미지를 보여주는 영역 */} -
- -
+
+
+ {/* 백그라운드에서 모든 이미지 미리 로드 */} + + {/* 현재 선택된 이미지를 보여주는 영역 */} +
+ +
- {/* 이미지 리스트 */} -
- - - {imageUrls.map((url, idx) => { - const isSelected = url === selectedUrl; + {/* 이미지 리스트 */} +
+ + + {apiImageUrls.map((url, idx) => { + const isSelected = url === selectedUrl; - return ( - handleImageSelect(url)} - draggable='false' - onLoaded={handleThumbLoad} - /> - ); - })} - + return ( + handleImageSelect(url)} + draggable='false' + onLoaded={handleThumbLoad} + /> + ); + })} + +
+ + +
); } diff --git a/src/pages/MessagePage/components/ProfileSelector.module.scss b/src/pages/MessagePage/components/ProfileSelector.module.scss index 6e9dadb..83835b5 100644 --- a/src/pages/MessagePage/components/ProfileSelector.module.scss +++ b/src/pages/MessagePage/components/ProfileSelector.module.scss @@ -1,9 +1,18 @@ +.profile-selector__container { + display: flex; + flex-direction: column; + gap: 20px; +} + .profile-selector { width: 100%; display: flex; gap: 32px; align-items: center; justify-content: center; + @include mobile { + gap: 0; + } &__preview-container { display: flex; @@ -31,6 +40,11 @@ font-size: var(--font-size-16); font-weight: var(--font-weight-regular); color: var(--color-gray-500); + //텍스트 잘리지 않게 + white-space: nowrap; + @include mobile { + margin-left: -40px; + } } &__images { gap: 4px;