From f149948ea0da574e28d5fb99c506316ab8cc7fd1 Mon Sep 17 00:00:00 2001 From: seoyeonson Date: Thu, 6 Mar 2025 16:42:47 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20+=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=B2=A8=20=ED=91=9C=EC=8B=9C=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home.jsx | 94 +++++++-- src/services/NotiService.js | 22 +++ src/services/WebSocketNotificationService.js | 192 +++++++++++++++++++ 3 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 src/services/WebSocketNotificationService.js diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 355eb85..fbb20e8 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -7,7 +7,7 @@ import { FaBell } from 'react-icons/fa'; import './CommitStats.css'; import '../modals/NotificationModal.css'; import NotiService from '../services/NotiService'; -import webSocketService from '../services/WebSocketService'; +import webSocketNotificationService from '../services/WebSocketNotificationService'; const Home = () => { // 알림 모달 @@ -27,12 +27,29 @@ const Home = () => { const [connected, setConnected] = useState(false); // 웹소켓 연결 상태 const navigate = useNavigate(); - const toggleModal = () => { + const toggleModal = async () => { setIsModalOpen(!isModalOpen); - if (isModalOpen) { - setHasNewNotification(false); // 모달을 열면 새로운 알림 표시 제거 + if (!isModalOpen) { // 모달이 열릴 때 (false -> true) + try { + // 읽지 않은 알림만 필터링 + const unreadNotifications = notifications.filter(noti => !noti.read); + if (unreadNotifications.length > 0) { + const unreadIds = unreadNotifications.map(noti => noti.id); + await NotiService.markAsRead(unreadIds); + + // 상태 업데이트: 알림을 읽음으로 표시 + setNotifications(prev => prev.map(noti => + unreadIds.includes(noti.id) + ? { ...noti, read: true } + : noti + )); + } + setHasNewNotification(false); + } catch (error) { + console.error('Failed to mark notifications as read:', error); + } } - }; +}; // 메시지 목록을 로드하는 함수 const loadNotis = async () => { @@ -53,24 +70,39 @@ useEffect(() => { loadNotis(); // 웹소켓 연결 - webSocketService.connect(); + webSocketNotificationService.connect(); // 연결 상태 변경 이벤트 리스너 등록 - const unsubscribeFromConnection = webSocketService.onConnectionChange(setConnected); + const unsubscribeFromConnection = webSocketNotificationService.onConnectionChange(setConnected); + + // 새로운 알림 메시지 핸들러 등록 + const unsubscribeFromMessages = webSocketNotificationService.onMessage((newNotification) => { + console.log('New notification received:', newNotification); + // 새로운 알림을 기존 알림 목록 앞에 추가 + setNotifications(prev => [newNotification, ...prev]); + // 모달이 닫혀있을 때만 새 알림 표시 + if (!isModalOpen) { + setHasNewNotification(true); + } + }); // 채팅방 구독 시도 setTimeout(() => { - const success = webSocketService.subscribeToNotificationChannel(); + const success = webSocketNotificationService.subscribeToNotificationChannel(); console.log('Notis subscription success:', success); }, 1000); // 약간의 지연을 두어 연결이 설정될 시간을 줌 // 컴포넌트 언마운트 시 이벤트 리스너 제거 및 구독 해제 - return () => { + return () => { if (unsubscribeFromConnection) { - unsubscribeFromConnection(); + unsubscribeFromConnection(); } - }; -}, []); + if (unsubscribeFromMessages) { + unsubscribeFromMessages(); + } + webSocketNotificationService.disconnect(); + }; +}, [isModalOpen]); // isModalOpen 의존성 추가 useEffect(() => { const fetchCommitData = async () => { @@ -195,16 +227,43 @@ useEffect(() => { } }; + const notificationBtnStyle = { + position: 'relative', + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + alignItems: 'center', + }; + + const notificationIconStyle = { + fontSize: '20px', + }; + + const notificationBadgeStyle = { + position: 'absolute', + top: '4px', + right: '12px', + width: '8px', + height: '8px', + backgroundColor: '#ef4444', + borderRadius: '50%', + display: hasNewNotification ? 'block' : 'none', + }; + return (
CommitField
- + {/* 채팅 버튼 추가 */} + + {isRefreshing ? '새로고침 중...' : `마지막 업데이트: ${formatRefreshTime(lastRefreshTime)}`} +
-
성장 단계: {userInfo.petGrow}
); }; -export default Profile; +export default Profile; \ No newline at end of file diff --git a/src/pages/profile.css b/src/pages/profile.css index 6ef84a6..30251bb 100644 --- a/src/pages/profile.css +++ b/src/pages/profile.css @@ -1,74 +1,298 @@ -@keyframes breathe { - 0%, 100% { - transform: scaleY(1); /* 원래 크기 */ - } - 50% { - transform: scaleY(0.8); /* 0.8배로 작아짐 */ - } -} - -h2 { - color: #222222; +/* Enhanced Profile Styles */ +.profile-container { + width: 100%; + background-color: white; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + padding: 24px; + margin-bottom: 32px; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 24px; + transition: all 0.3s ease; } - -.animated-pet { - animation: breathe 1.5s infinite ease-in-out; +.profile-container:hover { + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); } -.flex-box{ - width:100%; - display: flex; - justify-content: center; +/* Pet section styling */ +.pet-section { + position: relative; + flex-shrink: 0; } -.profile-container { +.pet-frame { + width: 140px; + height: 140px; + background: linear-gradient(135deg, #f0f9ff 0%, #e1f5fe 100%); + border-radius: 70px; + padding: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); display: flex; + justify-content: center; align-items: center; - gap: 20px; - padding: 15px; - width: 750px; - height: 140px; - background-color: #fefefe; - color: #010101; - border: 1px solid #fff; - border-radius: 10px; - line-height: 1.4; + position: relative; + overflow: hidden; } -.pet-box { - width: 128px; - height: 128px; - border: 1px solid #ff8a9e; - background-image: url('/pets/BACK.png'); +.pet-frame::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 50%); + border-radius: 70px; + z-index: 1; } -.pet-box img { + +.pet-image { width: 128px; height: 128px; + object-fit: contain; + z-index: 2; +} + +.pet-stage { + position: absolute; + bottom: -5px; + right: -5px; + background-color: #ff6b6b; + color: white; + font-weight: bold; + font-size: 14px; + padding: 6px 10px; + border-radius: 20px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + z-index: 3; } -.info-box { +/* User info section styling */ +.user-info { flex: 1; + display: flex; + flex-direction: column; + gap: 12px; } -.info-box img { - width: 32px; - height: 32px; +.user-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; } -.avatar { - width: 100px; + +.avatar-container { + width: 50px; + height: 50px; border-radius: 50%; + overflow: hidden; + border: 3px solid #f0f0f0; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); +} + +.avatar-container img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.username { + font-size: 20px; + font-weight: 700; + color: #1f2937; +} + +.user-details { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.detail-item { + display: flex; + align-items: center; + gap: 8px; +} + +.detail-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: #6b7280; +} + +.detail-text { + font-size: 14px; + color: #4b5563; +} + +.detail-value { + font-weight: 600; + color: #111827; +} + +/* Pet info section */ +.pet-info { + margin-top: 8px; + padding-top: 12px; + border-top: 1px solid #f3f4f6; +} + +.pet-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 16px; + font-weight: 600; + color: #4b5563; + margin-bottom: 8px; +} + +.exp-bar-container { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; } .exp-bar { + flex: 1; + height: 8px; + background-color: #f3f4f6; + border-radius: 4px; + overflow: hidden; + position: relative; +} + +.exp-progress { + height: 100%; + background: linear-gradient(to right, #3fb27f, #4ade80); + border-radius: 4px; + transition: width 0.5s ease; +} + +.exp-text { + font-size: 14px; + font-weight: 600; + color: #6b7280; + min-width: 70px; + text-align: right; +} + +/* Tier badge */ +.tier-badge { + display: inline-flex; + align-items: center; + background-color: #f3f4f6; + padding: 4px 10px; + border-radius: 16px; + font-size: 14px; + font-weight: 600; +} + +/* Animation for pet */ +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 0.7; + } + 50% { + transform: scale(1.05); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.7; + } +} + +.animated-pet { + animation: float 3s ease-in-out infinite; +} + +.animated-pet.refreshing { + animation: pulse 1s ease-in-out infinite; +} + +/* 새로고침 관련 스타일 */ +.refresh-info { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + font-size: 12px; + color: #9ca3af; +} + +.refresh-button { + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 50%; display: flex; align-items: center; - gap: 10px; + justify-content: center; + transition: all 0.2s ease; +} + +.refresh-button:hover { + background-color: #f3f4f6; +} + +.refresh-button:disabled { + cursor: not-allowed; + opacity: 0.7; } -.bar { - width: 200px; - display: flex; - align-items: center; - height: 30px; +.refresh-icon { + font-size: 14px; + display: inline-block; } + +.refresh-icon.rotating { + animation: rotate 1s linear infinite; +} + +.last-refresh-time { + font-size: 11px; + color: #9ca3af; +} + +/* 새로고침 진행 중 표시 */ +.refreshing-indicator { + position: absolute; + top: -5px; + right: -5px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: #3b82f6; + box-shadow: 0 0 0 2px white; + z-index: 10; + animation: pulse 1s infinite ease-in-out; +} \ No newline at end of file From 5af9d14e39dad858df62a9d10ace417040788076 Mon Sep 17 00:00:00 2001 From: whale22 Date: Fri, 7 Mar 2025 12:06:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20stash=20=EC=A0=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Profile.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index ca113fa..f8eed47 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -27,7 +27,7 @@ const Profile = ({ userInfo }) => { EGG: "알", HATCH: "부화", BABY: "아기", - ADULT: "성체", + GROWN: "성체", }; // 경험치 바 계산 @@ -48,8 +48,8 @@ const Profile = ({ userInfo }) => { if (response.ok) { const data = await response.json(); // 새로운 데이터로 상태 업데이트 - setSeasonCommitCount(data.data.seasonCommitCount || 0); - setPetExp(data.data.petExp || 0); + setSeasonCommitCount(userInfo.seasonCommitCount || 0); + setPetExp(userInfo.petExp || 0); // 마지막 갱신 시간 업데이트 setLastRefreshTime(new Date()); }