diff --git a/src/domains/freetalk/components/SystemCommandMessage.jsx b/src/domains/freetalk/components/SystemCommandMessage.jsx
index 12a1423..253382f 100644
--- a/src/domains/freetalk/components/SystemCommandMessage.jsx
+++ b/src/domains/freetalk/components/SystemCommandMessage.jsx
@@ -18,9 +18,18 @@ const SystemCommandMessage = ({ data }) => {
const { mode } = useThemeMode()
const isDark = mode === 'dark'
- const config = SystemCommandConfig[data.commandType] || SystemCommandConfig.help
+ // 명령어 타입 추출
+ const commandType = data?.commandType || data?.type || data?.raw?.type || 'help'
+ const config = SystemCommandConfig[commandType] || SystemCommandConfig.help
const { icon, color, bgColor } = config
+ // 표시 텍스트 추출
+ const displayText = data?.displayText || data?.message || data?.content || ''
+ const userId = data?.userId || data?.nickname || ''
+
+ // 결과 값 추출
+ const result = data?.result || data?.raw || {}
+
return (
{
{/* 내용 */}
-
-
- {data.userId}
-
+ {/* displayText가 있으면 그대로 표시 (백엔드에서 이미 포맷팅됨) */}
+ {displayText ? (
- {data.displayText}
+ {displayText}
-
-
- {/* 추가 결과 정보 */}
- {renderCommandResult(data.commandType, data.result, isDark)}
+ ) : (
+ <>
+
+
+ {userId}
+
+
+ {/* 추가 결과 정보 */}
+ {renderCommandResult(commandType, result, isDark)}
+ >
+ )}
diff --git a/src/domains/freetalk/hooks/useChatWebSocket.js b/src/domains/freetalk/hooks/useChatWebSocket.js
index 8aad516..fd6eaaa 100644
--- a/src/domains/freetalk/hooks/useChatWebSocket.js
+++ b/src/domains/freetalk/hooks/useChatWebSocket.js
@@ -165,14 +165,21 @@ export function useChatWebSocket(roomId, userId) {
},
onSystemCommand: (data) => {
- console.log('[useChatWebSocket] System command:', data)
- const commandData = data.data || data
+ const commandData = data.data || {}
const commandMessage = {
id: `syscmd-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
messageType: 'SYSTEM_COMMAND',
- userId: commandData.userId,
- createdAt: new Date().toISOString(),
- data: commandData,
+ userId: commandData.userId || commandData.nickname || data.userId,
+ createdAt: data.createdAt || new Date().toISOString(),
+ data: {
+ commandType: commandData.type || 'help',
+ userId: commandData.userId || commandData.nickname,
+ displayText: data.content || data.message || '',
+ result: typeof commandData.result === 'object'
+ ? commandData.result
+ : { value: commandData.result },
+ raw: commandData,
+ },
}
setMessages((prev) => [...prev, commandMessage])
},
From 44fa7d9ab155c75a52ed00bf73651f3fa774c108 Mon Sep 17 00:00:00 2001
From: hyein Heo <128613248+hye-inA@users.noreply.github.com>
Date: Sun, 25 Jan 2026 18:58:06 +0900
Subject: [PATCH 08/14] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?=
=?UTF-8?q?=EC=9D=B8/=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=A0=95?=
=?UTF-8?q?=EC=83=81=ED=99=94=20=EB=B0=8F=20=EC=B1=84=ED=8C=85=20=EC=84=9C?=
=?UTF-8?q?=EB=B2=84=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20=EB=A9=94=EC=9D=B8?=
=?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=ED=97=A4=EB=8D=94=20=ED=94=84=EB=A1=9C?=
=?UTF-8?q?=ED=95=84=20=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C=20(#208)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor : 프리토킹 사용자 닉네임 수정
* refactor : 메인 헤더 프로필 드롭다운 메뉴 사용자 정보 동기화 리팩토링
---
src/App.jsx | 11 ++
src/aws-config.js | 4 +-
.../freetalk/hooks/useChatWebSocket.js | 11 +-
src/domains/freetalk/pages/ChatRoomPage.jsx | 101 ++++++++--------
src/domains/profile/hooks/useProfile.js | 35 ++++++
.../profile/services/profileService.js | 39 ++++++
src/domains/profile/store/profileSlice.js | 114 ++++++++++++++++++
src/layouts/MainLayout/Header/index.jsx | 82 +++++++------
src/store/index.js | 5 +-
9 files changed, 304 insertions(+), 98 deletions(-)
create mode 100644 src/domains/profile/hooks/useProfile.js
create mode 100644 src/domains/profile/services/profileService.js
create mode 100644 src/domains/profile/store/profileSlice.js
diff --git a/src/App.jsx b/src/App.jsx
index ab94f58..6896eeb 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'
+import { useDispatch } from 'react-redux'
import { Box, Button, Card, CardContent, CircularProgress, Collapse, Container, Grid, Typography } from '@mui/material'
import {
ChevronRight as ChevronRightIcon,
@@ -43,6 +44,7 @@ import { useSettings } from './contexts/SettingsContext'
import { useAuth } from './contexts/AuthContext'
import LoginPage from './pages/Login'
import SignUpPage from './pages/SignUp'
+import { fetchMyProfile } from "./domains/profile/store/profileSlice";
function ProtectedRoute({ children }) {
@@ -1141,8 +1143,17 @@ function NotFound() {
}
function App() {
+ const dispatch = useDispatch()
+ const { isAuthenticated } = useAuth()
const { activeRoom, closeChatRoom } = useChat()
+ useEffect(() => {
+ if (isAuthenticated) {
+ // redux로 프로필 정보 API(/users/profile/me) 호출
+ dispatch(fetchMyProfile())
+ }
+ }, [isAuthenticated, dispatch])
+
const handleRefreshRooms = () => {
// Refresh rooms list after leaving a room
}
diff --git a/src/aws-config.js b/src/aws-config.js
index d4d0a9d..6d2b672 100644
--- a/src/aws-config.js
+++ b/src/aws-config.js
@@ -1,8 +1,8 @@
const awsConfig = {
Auth: {
Cognito: {
- userPoolId: 'ap-northeast-2_ezDwzFCzR',
- userPoolClientId: '4ns077jcr1pkue2vvisr6qdpu5',
+ userPoolId: import.meta.env.VITE_COGNITO_POOL_ID,
+ userPoolClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,
loginWith: {
email: true,
diff --git a/src/domains/freetalk/hooks/useChatWebSocket.js b/src/domains/freetalk/hooks/useChatWebSocket.js
index fd6eaaa..633d755 100644
--- a/src/domains/freetalk/hooks/useChatWebSocket.js
+++ b/src/domains/freetalk/hooks/useChatWebSocket.js
@@ -1,6 +1,6 @@
-import {useCallback, useEffect, useRef, useState} from 'react'
-import {chatWebSocketService} from '../services/chatWebSocketService'
-import {chatRoomService} from '../../chat/services/chatService'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { chatWebSocketService } from '../services/chatWebSocketService'
+import { chatRoomService } from '../../chat/services/chatService'
/**
* Chat WebSocket 훅
@@ -26,10 +26,10 @@ export function useChatWebSocket(roomId, userId) {
* WebSocket 연결
*/
const connect = useCallback(async (forceNewToken = false) => {
- console.log('[useChatWebSocket] Attempting to connect...', {roomId, userId, forceNewToken})
+ console.log('[useChatWebSocket] Attempting to connect...', { roomId, userId, forceNewToken })
if (!roomId || !userId) {
- console.error('[useChatWebSocket] roomId and userId are required', {roomId, userId})
+ console.error('[useChatWebSocket] roomId and userId are required', { roomId, userId })
return
}
@@ -74,6 +74,7 @@ export function useChatWebSocket(roomId, userId) {
id: messageId,
content: data.content,
userId: data.userId,
+ nickname: data.nickname,
messageType: data.messageType || 'TEXT',
createdAt: data.createdAt || new Date().toISOString(),
isOwn: data.userId === userId,
diff --git a/src/domains/freetalk/pages/ChatRoomPage.jsx b/src/domains/freetalk/pages/ChatRoomPage.jsx
index 2a220d6..781a891 100644
--- a/src/domains/freetalk/pages/ChatRoomPage.jsx
+++ b/src/domains/freetalk/pages/ChatRoomPage.jsx
@@ -1,5 +1,5 @@
-import {useCallback, useEffect, useRef, useState} from 'react'
-import {useNavigate, useParams} from 'react-router-dom'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
import {
Alert,
AppBar,
@@ -21,31 +21,31 @@ import {
Send as SendIcon,
VolumeUp as VolumeUpIcon,
} from '@mui/icons-material'
-import {chatRoomService, messageService, voiceService} from '../../chat/services/chatService'
-import {useAuth} from '../../../contexts/AuthContext'
-import {useChatWebSocket} from '../hooks/useChatWebSocket'
-import {useThemeMode} from '../../../contexts/ThemeContext'
+import { chatRoomService, messageService, voiceService } from '../../chat/services/chatService'
+import { useAuth } from '../../../contexts/AuthContext'
+import { useChatWebSocket } from '../hooks/useChatWebSocket'
+import { useThemeMode } from '../../../contexts/ThemeContext'
import CommandAutocomplete from '../components/CommandAutocomplete'
import PollCard from '../components/PollCard'
import SystemCommandMessage from '../components/SystemCommandMessage'
-import {parseCommand, MessageType} from '../types/chatCommandTypes'
+import { parseCommand, MessageType } from '../types/chatCommandTypes'
const levelColors = {
- beginner: {bg: '#e8f5e9', color: '#2e7d32', label: '초급'},
- intermediate: {bg: '#fff3e0', color: '#ef6c00', label: '중급'},
- advanced: {bg: '#fce4ec', color: '#c2185b', label: '고급'},
+ beginner: { bg: '#e8f5e9', color: '#2e7d32', label: '초급' },
+ intermediate: { bg: '#fff3e0', color: '#ef6c00', label: '중급' },
+ advanced: { bg: '#fce4ec', color: '#c2185b', label: '고급' },
}
const ChatRoomPage = () => {
- const {roomId} = useParams()
+ const { roomId } = useParams()
const navigate = useNavigate()
- const {user} = useAuth()
- const {mode} = useThemeMode()
+ const { user } = useAuth()
+ const { mode } = useThemeMode()
const isDark = mode === 'dark'
const currentUserId = user?.userId || user?.username || user?.sub
// 디버깅: 사용자 정보 확인
- console.log('[ChatRoomPage] User info:', {user, currentUserId, roomId})
+ console.log('[ChatRoomPage] User info:', { user, currentUserId, roomId })
const messagesEndRef = useRef(null)
const messagesContainerRef = useRef(null)
@@ -90,11 +90,12 @@ const ChatRoomPage = () => {
// 기존 메시지 목록 조회 (초기 로드용)
const fetchMessages = useCallback(async () => {
try {
- const response = await messageService.getList(roomId, {limit: 50})
+ const response = await messageService.getList(roomId, { limit: 50 })
const transformedMessages = (response.messages || []).map((msg, index) => ({
id: msg.messageId || `msg-${index}-${Date.now()}`,
content: msg.content,
userId: msg.userId,
+ nickname: msg.nickname,
messageType: msg.messageType,
createdAt: new Date(msg.createdAt),
isOwn: msg.userId === currentUserId,
@@ -122,12 +123,12 @@ const ChatRoomPage = () => {
// WebSocket 연결 (별도 effect)
useEffect(() => {
- console.log('[ChatRoomPage] WebSocket effect triggered:', {roomId, currentUserId, isConnected})
+ console.log('[ChatRoomPage] WebSocket effect triggered:', { roomId, currentUserId, isConnected })
if (currentUserId && roomId) {
- console.log('[ChatRoomPage] Connecting WebSocket...', {roomId, currentUserId})
+ console.log('[ChatRoomPage] Connecting WebSocket...', { roomId, currentUserId })
wsConnect()
} else {
- console.log('[ChatRoomPage] Missing required values:', {roomId, currentUserId})
+ console.log('[ChatRoomPage] Missing required values:', { roomId, currentUserId })
}
return () => {
@@ -138,7 +139,7 @@ const ChatRoomPage = () => {
// 스크롤 맨 아래로
const scrollToBottom = () => {
- messagesEndRef.current?.scrollIntoView({behavior: 'smooth'})
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
@@ -161,7 +162,7 @@ const ChatRoomPage = () => {
setShowCommandAutocomplete(false)
// 명령어 파싱
- const {isCommand, command, args} = parseCommand(messageContent)
+ const { isCommand, command, args } = parseCommand(messageContent)
// /leave 명령어는 클라이언트에서 직접 처리
if (isCommand && command === '/leave') {
@@ -293,23 +294,23 @@ const ChatRoomPage = () => {
if (loading) {
return (
-
-
+
+
)
}
return (
-
+
{/* 헤더 */}
- navigate('/freetalk/people')} sx={{mr: 1}}>
-
+ navigate('/freetalk/people')} sx={{ mr: 1 }}>
+
-
-
-
+
+
+
{room?.name || '채팅방'}
{/* 연결 상태 표시 */}
@@ -320,7 +321,7 @@ const ChatRoomPage = () => {
}}
/>
-
+
{room?.level && (
{
-
+
-
+
{/* 에러 메시지 */}
{error && (
-
+
{error}
)}
@@ -370,7 +371,7 @@ const ChatRoomPage = () => {
}}
>
{messages.length === 0 ? (
-
+
아직 메시지가 없습니다. 첫 메시지를 보내보세요!
@@ -380,7 +381,7 @@ const ChatRoomPage = () => {
// 투표 메시지
if (message.messageType === MessageType.POLL_CREATE) {
return (
-
+
{
// 시스템 명령어 메시지
if (message.messageType === MessageType.SYSTEM_COMMAND) {
return (
-
+
)
}
@@ -411,7 +412,7 @@ const ChatRoomPage = () => {
>
{/* 시스템 메시지 */}
{message.isSystem ? (
-
+
{message.content}
@@ -420,8 +421,8 @@ const ChatRoomPage = () => {
<>
{/* 아바타 (상대방만) */}
{!message.isOwn && (
-
- {message.userId?.charAt(0)?.toUpperCase() || 'U'}
+
+ {(message.nickname || message.userId)?.charAt(0)?.toUpperCase() || 'U'}
)}
@@ -435,13 +436,13 @@ const ChatRoomPage = () => {
>
{/* 사용자 이름 (상대방만) */}
{!message.isOwn && (
-
- {message.userId}
+
+ {message.nickname || message.userId}
)}
{/* 메시지 버블 */}
-
+
{message.isOwn && (
{formatTime(message.createdAt)}
@@ -463,23 +464,23 @@ const ChatRoomPage = () => {
opacity: message.isPending ? 0.7 : 1,
}}
>
-
+
{message.content}
{!message.isOwn && (
-
+
handlePlayTTS(message.id)}
disabled={playingTTS === message.id}
- sx={{p: 0.5}}
+ sx={{ p: 0.5 }}
>
{playingTTS === message.id ? (
-
+
) : (
-
+
)}
@@ -495,7 +496,7 @@ const ChatRoomPage = () => {
)
})
)}
-
+
{/* 입력 영역 */}
@@ -539,11 +540,11 @@ const ChatRoomPage = () => {
sx={{
bgcolor: 'primary.main',
color: 'white',
- '&:hover': {bgcolor: 'primary.dark'},
- '&:disabled': {bgcolor: 'grey.300'},
+ '&:hover': { bgcolor: 'primary.dark' },
+ '&:disabled': { bgcolor: 'grey.300' },
}}
>
-
+
diff --git a/src/domains/profile/hooks/useProfile.js b/src/domains/profile/hooks/useProfile.js
new file mode 100644
index 0000000..3e965c4
--- /dev/null
+++ b/src/domains/profile/hooks/useProfile.js
@@ -0,0 +1,35 @@
+import { useEffect } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import {
+ fetchMyProfile,
+ updateProfile,
+ uploadProfileImage,
+ clearError
+} from '../store/profileSlice'
+
+export const useProfile = () => {
+ const dispatch = useDispatch()
+ const { profile, loading, error, updateLoading, imageUploading } = useSelector(
+ (state) => state.profile
+ )
+
+ useEffect(() => {
+ if (!profile && !loading && !error) {
+ dispatch(fetchMyProfile())
+ }
+ }, [dispatch, profile, loading, error])
+
+ return {
+ profile,
+ loading,
+ error,
+ updateLoading,
+ imageUploading,
+ updateProfile: (data) => dispatch(updateProfile(data)).unwrap(),
+ uploadImage: (file) => dispatch(uploadProfileImage(file)).unwrap(),
+ clearError: () => dispatch(clearError()),
+ refetch: () => dispatch(fetchMyProfile())
+ }
+}
+
+export default useProfile
\ No newline at end of file
diff --git a/src/domains/profile/services/profileService.js b/src/domains/profile/services/profileService.js
new file mode 100644
index 0000000..c41fcdc
--- /dev/null
+++ b/src/domains/profile/services/profileService.js
@@ -0,0 +1,39 @@
+import api from '../../../api/axios'
+
+const profileService = {
+ // 내 프로필 조회
+ getMyProfile: async () => {
+ const response = await api.get('/users/profile/me')
+ return response.data
+ },
+
+ // 프로필 수정 (닉네임, 레벨)
+ updateProfile: async ({ nickname, level, profileUrl }) => {
+ const response = await api.put('/users/profile/me', {
+ nickname,
+ level,
+ profileUrl
+ })
+ return response.data
+ },
+
+ // 이미지 업로드 URL 발급
+ getImageUploadUrl: async (fileName, contentType) => {
+ const response = await api.post('/users/profile/me/image', {
+ fileName,
+ contentType
+ })
+ return response.data
+ },
+
+ // S3에 이미지 직접 업로드
+ uploadImageToS3: async (uploadUrl, file) => {
+ await fetch(uploadUrl, {
+ method: 'PUT',
+ headers: { 'Content-Type': file.type },
+ body: file
+ })
+ }
+}
+
+export default profileService
\ No newline at end of file
diff --git a/src/domains/profile/store/profileSlice.js b/src/domains/profile/store/profileSlice.js
new file mode 100644
index 0000000..76995ec
--- /dev/null
+++ b/src/domains/profile/store/profileSlice.js
@@ -0,0 +1,114 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+import profileService from '../services/profileService'
+
+// 프로필 조회
+export const fetchMyProfile = createAsyncThunk(
+ 'profile/fetchMyProfile',
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await profileService.getMyProfile()
+ return response.data || response
+ } catch (error) {
+ const status = error.response?.status
+ const message = error.response?.data?.error || error.message || '프로필 조회 실패'
+ return rejectWithValue({ message, status })
+ }
+ }
+)
+
+// 프로필 수정
+export const updateProfile = createAsyncThunk(
+ 'profile/updateProfile',
+ async (data, { rejectWithValue }) => {
+ try {
+ const response = await profileService.updateProfile(data)
+ return response.data
+ } catch (error) {
+ return rejectWithValue(error.response?.data?.error || '프로필 수정 실패')
+ }
+ }
+)
+
+// 이미지 업로드
+export const uploadProfileImage = createAsyncThunk(
+ 'profile/uploadImage',
+ async (file, { dispatch, rejectWithValue }) => {
+ try {
+ const urlResponse = await profileService.getImageUploadUrl(file.name, file.type)
+ const { uploadUrl, imageUrl } = urlResponse.data
+
+ await profileService.uploadImageToS3(uploadUrl, file)
+ await dispatch(updateProfile({ profileUrl: imageUrl }))
+
+ return imageUrl
+ } catch (error) {
+ return rejectWithValue('이미지 업로드 실패')
+ }
+ }
+)
+
+const profileSlice = createSlice({
+ name: 'profile',
+ initialState: {
+ profile: null,
+ loading: false,
+ error: null,
+ updateLoading: false,
+ imageUploading: false,
+ authError: false
+ },
+ reducers: {
+ clearError: (state) => { state.error = null },
+ clearProfile: (state) => { state.profile = null }
+ },
+ extraReducers: (builder) => {
+ builder
+ // fetchMyProfile
+ .addCase(fetchMyProfile.pending, (state) => {
+ state.loading = true
+ state.error = null
+ })
+ .addCase(fetchMyProfile.fulfilled, (state, action) => {
+ state.loading = false
+ state.profile = action.payload
+ })
+ .addCase(fetchMyProfile.rejected, (state, action) => {
+ state.loading = false
+ state.error = action.payload?.message || action.payload
+
+ const status = action.payload?.status
+ const message = String(action.payload?.message || action.payload || '')
+
+ if (status === 401 || message.includes('401') || message.includes('인증')) {
+ state.profile = null
+ state.authError = true
+ }
+ })
+ // updateProfile
+ .addCase(updateProfile.pending, (state) => {
+ state.updateLoading = true
+ })
+ .addCase(updateProfile.fulfilled, (state, action) => {
+ state.updateLoading = false
+ state.profile = action.payload
+ })
+ .addCase(updateProfile.rejected, (state, action) => {
+ state.updateLoading = false
+ state.error = action.payload
+ })
+ // uploadProfileImage
+ .addCase(uploadProfileImage.pending, (state) => {
+ state.imageUploading = true
+ })
+ .addCase(uploadProfileImage.fulfilled, (state) => {
+ state.imageUploading = false
+ })
+ .addCase(uploadProfileImage.rejected, (state, action) => {
+ state.imageUploading = false
+ state.error = action.payload
+ })
+ }
+})
+
+export const { clearError, clearProfile } = profileSlice.actions
+export default profileSlice.reducer
\ No newline at end of file
diff --git a/src/layouts/MainLayout/Header/index.jsx b/src/layouts/MainLayout/Header/index.jsx
index f52deba..d4c6583 100644
--- a/src/layouts/MainLayout/Header/index.jsx
+++ b/src/layouts/MainLayout/Header/index.jsx
@@ -1,5 +1,6 @@
-import {useState} from 'react'
-import {useNavigate} from 'react-router-dom'
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useSelector } from 'react-redux'
import {
AppBar,
Avatar,
@@ -24,25 +25,26 @@ import {
Settings as SettingsIcon,
Translate as TranslateIcon,
} from '@mui/icons-material'
-import {useThemeMode} from '../../../contexts/ThemeContext'
-import {useSettings, useTranslation} from '../../../contexts/SettingsContext'
-import {LANGUAGE_LABELS, LANGUAGES} from '../../../i18n/translations'
-import {useAuth} from '../../../contexts/AuthContext'
-import {useNotificationContext, NotificationMenu} from '../../../domains/notification'
+import { useThemeMode } from '../../../contexts/ThemeContext'
+import { useSettings, useTranslation } from '../../../contexts/SettingsContext'
+import { LANGUAGE_LABELS, LANGUAGES } from '../../../i18n/translations'
+import { useAuth } from '../../../contexts/AuthContext'
+import { useNotificationContext, NotificationMenu } from '../../../domains/notification'
-const Header = ({onMenuClick, sidebarOpen}) => {
+const Header = ({ onMenuClick, sidebarOpen }) => {
const theme = useTheme()
const navigate = useNavigate()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
- const {mode, toggleTheme} = useThemeMode()
- const {setLanguage, language} = useSettings()
- const {t} = useTranslation()
+ const { mode, toggleTheme } = useThemeMode()
+ const { setLanguage, language } = useSettings()
+ const { t } = useTranslation()
+ const { profile } = useSelector((state) => state.profile)
const [anchorEl, setAnchorEl] = useState(null)
const [notificationAnchor, setNotificationAnchor] = useState(null)
const [langAnchor, setLangAnchor] = useState(null)
- const {logout} = useAuth()
- const {unreadCount} = useNotificationContext()
+ const { logout } = useAuth()
+ const { unreadCount } = useNotificationContext()
const handleProfileMenuOpen = (event) => {
setAnchorEl(event.currentTarget)
@@ -93,7 +95,7 @@ const Header = ({onMenuClick, sidebarOpen}) => {
borderColor: mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)',
}}
>
-
+
{/* Hamburger menu (mobile) */}
{isMobile && (
{
},
}}
>
-
+
)}
@@ -145,7 +147,7 @@ const Header = ({onMenuClick, sidebarOpen}) => {
L
-
+
{
-
+
{/* Right side icons */}
-
+
{/* Language selector */}
{
},
}}
>
-
+
{/* Dark mode toggle */}
@@ -192,7 +194,7 @@ const Header = ({onMenuClick, sidebarOpen}) => {
},
}}
>
- {mode === 'dark' ? : }
+ {mode === 'dark' ? : }
{/* Notifications */}
@@ -219,14 +221,14 @@ const Header = ({onMenuClick, sidebarOpen}) => {
},
}}
>
-
+
{/* Profile */}
{
fontSize: 14,
}}
>
- U
+ {profile?.nickname ? profile.nickname.substring(0, 1).toUpperCase() : 'U'}
@@ -255,15 +257,15 @@ const Header = ({onMenuClick, sidebarOpen}) => {
minWidth: 160,
},
}}
- transformOrigin={{horizontal: 'right', vertical: 'top'}}
- anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
+ transformOrigin={{ horizontal: 'right', vertical: 'top' }}
+ anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
-
+
{t('settings.language')}
-
+
{Object.entries(LANGUAGES).map(([key, value]) => (