diff --git a/src/App.jsx b/src/App.jsx index 1f9a6fe..861df19 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -45,6 +45,7 @@ import { useAuth } from './contexts/AuthContext' import LoginPage from './pages/Login' import SignUpPage from './pages/SignUp' import { fetchMyProfile } from "./domains/profile/store/profileSlice"; +import ProfilePage from './domains/profile/pages/ProfilePage' function ProtectedRoute({ children }) { @@ -1183,6 +1184,7 @@ function App() { }> } /> } /> + } /> } /> } /> } /> diff --git a/src/domains/freetalk/components/ChatRoomModal.jsx b/src/domains/freetalk/components/ChatRoomModal.jsx index 1856abc..653be25 100644 --- a/src/domains/freetalk/components/ChatRoomModal.jsx +++ b/src/domains/freetalk/components/ChatRoomModal.jsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useRef, useState} from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Alert, Avatar, @@ -30,20 +30,20 @@ import { messageService, voiceService } from '../../chat/services/chatService' -import {useSettings} from '../../../contexts/SettingsContext' -import {useAuth} from '../../../contexts/AuthContext' -import {useThemeMode} from '../../../contexts/ThemeContext' -import {DESIGN_TOKENS, getChatStyles} from '../../../theme/theme' -import {useChatWebSocket} from '../hooks/useChatWebSocket' +import { useSettings } from '../../../contexts/SettingsContext' +import { useAuth } from '../../../contexts/AuthContext' +import { useThemeMode } from '../../../contexts/ThemeContext' +import { DESIGN_TOKENS, getChatStyles } from '../../../theme/theme' +import { useChatWebSocket } from '../hooks/useChatWebSocket' import SystemCommandMessage from './SystemCommandMessage' -import {MessageType} from '../types/chatCommandTypes' +import { MessageType } from '../types/chatCommandTypes' -const ChatRoomModal = ({open, onClose, room, onLeave}) => { +const ChatRoomModal = ({ open, onClose, room, onLeave }) => { const theme = useTheme() - const {mode} = useThemeMode() + const { mode } = useThemeMode() const isDark = mode === 'dark' - const {settings} = useSettings() - const {user} = useAuth() + const { settings } = useSettings() + const { user } = useAuth() const currentUserId = user?.userId || user?.username || user?.sub const messagesEndRef = useRef(null) const dragRef = useRef(null) @@ -65,10 +65,10 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { const [error, setError] = useState(null) const [playingTTS, setPlayingTTS] = useState(null) const [minimized, setMinimized] = useState(false) - const [position, setPosition] = useState({x: 0, y: 0}) - const [savedPosition, setSavedPosition] = useState({x: 0, y: 0}) + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [savedPosition, setSavedPosition] = useState({ x: 0, y: 0 }) const [isDragging, setIsDragging] = useState(false) - const [dragOffset, setDragOffset] = useState({x: 0, y: 0}) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) const [opacity, setOpacity] = useState(100) const [opacityAnchorEl, setOpacityAnchorEl] = useState(null) // 메시지 목록 조회 @@ -76,7 +76,7 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { if (!room?.id) return try { - const response = await messageService.getList(room.id, {limit: 50}) + const response = await messageService.getList(room.id, { limit: 50 }) const responseData = response.data || response const transformedMessages = (responseData.messages || []).map((msg) => ({ id: msg.messageId || msg.pk?.replace('MESSAGE#', ''), @@ -102,9 +102,9 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { // 초기 로드 useEffect(() => { - console.log('[ChatRoomModal] useEffect triggered:', {open, roomId: room?.id, currentUserId}) + console.log('[ChatRoomModal] useEffect triggered:', { open, roomId: room?.id, currentUserId }) if (open && room?.id && currentUserId) { - console.log('[ChatRoomModal] Initializing...', {roomId: room.id, userId: currentUserId}) + console.log('[ChatRoomModal] Initializing...', { roomId: room.id, userId: currentUserId }) setLoading(true) setMinimized(false) @@ -125,7 +125,7 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { // 스크롤 맨 아래로 const scrollToBottom = (instant = false) => { - messagesEndRef.current?.scrollIntoView({behavior: instant ? 'instant' : 'smooth'}) + messagesEndRef.current?.scrollIntoView({ behavior: instant ? 'instant' : 'smooth' }) } // 메시지 로드 완료 후 스크롤 @@ -259,7 +259,7 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { if (!minimized) { // 최소화: 현재 위치 저장 후 우측 하단으로 이동 setSavedPosition(position) - setPosition({x: 0, y: 0}) + setPosition({ x: 0, y: 0 }) } else { // 최대화: 저장된 위치로 복원 후 스크롤 맨 아래로 setPosition(savedPosition) @@ -299,7 +299,7 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { { pointerEvents: 'auto', }} > - + {room?.name || '채팅방'} @@ -358,28 +358,28 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { /> )} - + setOpacityAnchorEl(e.currentTarget)} - sx={{color: 'white'}} + sx={{ color: 'white' }} title="투명도" > - + setOpacityAnchorEl(null)} - anchorOrigin={{vertical: 'top', horizontal: 'center'}} - transformOrigin={{vertical: 'bottom', horizontal: 'center'}} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + transformOrigin={{ vertical: 'bottom', horizontal: 'center' }} slotProps={{ paper: { - sx: {pointerEvents: 'auto'} + sx: { pointerEvents: 'auto' } } }} > - + 투명도: {opacity}% @@ -392,18 +392,18 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { /> - - + + - - {minimized ? : } + + {minimized ? : } - - + + - - + + @@ -412,61 +412,61 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { <> {/* 에러 메시지 */} {error && ( - setError(null)} sx={{borderRadius: 0}}> + setError(null)} sx={{ borderRadius: 0 }}> {error} )} {/* 메시지 영역 */} {loading ? ( - + + + ) : ( + - - - ) : ( - - {messages.length === 0 ? ( - - - 첫 메시지를 보내보세요! - - - ) : ( - messages.map((message) => { - // 시스템 명령어 메시지 (SYSTEM_COMMAND) - if (message.messageType === MessageType.SYSTEM_COMMAND || message.messageType === 'SYSTEM_COMMAND') { - return ( - - ) - } - + // 스크롤바 숨김 (hover 시만 표시) + '&::-webkit-scrollbar': { + width: 6, + }, + '&::-webkit-scrollbar-thumb': { + bgcolor: 'transparent', + borderRadius: 3, + }, + '&:hover::-webkit-scrollbar-thumb': { + bgcolor: isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)', + }, + }} + > + {messages.length === 0 ? ( + + + 첫 메시지를 보내보세요! + + + ) : ( + messages.map((message) => { + // 시스템 명령어 메시지 (SYSTEM_COMMAND) + if (message.messageType === MessageType.SYSTEM_COMMAND || message.messageType === 'SYSTEM_COMMAND') { return ( + + ) + } + + return ( { whiteSpace: 'pre-wrap', height: 'auto', py: 0.5, - '& .MuiChip-label': {whiteSpace: 'pre-wrap'}, + '& .MuiChip-label': { whiteSpace: 'pre-wrap' }, }} /> ) : ( @@ -518,7 +518,7 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { mr: message.isOwn ? 0.5 : 0, fontSize: '0.6rem' }}> - {message.userId} + {message.nickname || message.userId} { size="small" onClick={() => handlePlayTTS(message.id)} disabled={playingTTS === message.id} - sx={{p: 0.25}} + sx={{ p: 0.25 }} > {playingTTS === message.id ? ( - + ) : ( + }} /> )} + color="text.secondary" + sx={{ fontSize: '0.55rem' }}> {formatTime(message.createdAt)} @@ -584,17 +584,17 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { size="small" onClick={() => handlePlayTTS(message.id)} disabled={playingTTS === message.id} - sx={{p: 0.25}} + sx={{ p: 0.25 }} > {playingTTS === message.id ? ( - + ) : ( - + )} + color="text.secondary" + sx={{ fontSize: '0.55rem' }}> {formatTime(message.createdAt)} @@ -604,11 +604,12 @@ const ChatRoomModal = ({open, onClose, room, onLeave}) => { )} - )}) - )} -
- - )} + ) + }) + )} +
+ + )} {/* 입력 영역 */} { color: 'white', width: 32, height: 32, - '&:hover': {bgcolor: 'primary.dark'}, - '&:disabled': {bgcolor: 'grey.300'}, + '&:hover': { bgcolor: 'primary.dark' }, + '&:disabled': { bgcolor: 'grey.300' }, }} > - {sendingMessage ? : - } + {sendingMessage ? : + } diff --git a/src/domains/profile/pages/ProfilePage.jsx b/src/domains/profile/pages/ProfilePage.jsx new file mode 100644 index 0000000..2f29b8b --- /dev/null +++ b/src/domains/profile/pages/ProfilePage.jsx @@ -0,0 +1,106 @@ +import { useState, useEffect } from 'react' +import { Box, Button, Card, CardContent, Container, TextField, Typography, Avatar, Alert } from '@mui/material' +import { useDispatch, useSelector } from 'react-redux' + +// ▼▼▼ [중요] 파일 위치가 바뀌었으니 import 경로도 수정했습니다! ▼▼▼ +// (pages 폴더와 store 폴더가 같은 profile 폴더 안에 형제(sibling) 관계이므로) +import { updateProfile } from '../store/profileSlice' + +const ProfilePage = () => { + const dispatch = useDispatch() + const { profile, updateLoading } = useSelector((state) => state.profile) + + const [nickname, setNickname] = useState('') + const [successMsg, setSuccessMsg] = useState('') + + // 리덕스에 있는 내 정보(profile)가 로드되면 입력창에 채워넣기 + useEffect(() => { + if (profile?.nickname) { + setNickname(profile.nickname) + } + }, [profile]) + + const handleSave = async () => { + try { + // 1. 닉네임 변경 요청 보내기 + await dispatch(updateProfile({ nickname })).unwrap() + + // 2. 성공 메시지 띄우기 + setSuccessMsg('닉네임이 변경되었습니다! 로그아웃 후 다시 로그인해주세요.') + } catch (error) { + alert('변경 실패: ' + error) + } + } + + return ( + + + 내 프로필 수정 + + + + + + {/* 프로필 이미지 */} + + + {nickname ? nickname.substring(0, 1).toUpperCase() : 'U'} + + + + {/* 이메일 (수정 불가) */} + + + {/* 닉네임 (수정 가능) */} + setNickname(e.target.value)} + fullWidth + variant="outlined" + helperText="채팅방에서 사용할 멋진 닉네임을 지어주세요!" + /> + + {/* 성공 메시지 */} + {successMsg && ( + {successMsg} + )} + + {/* 저장 버튼 */} + + + + + + ) +} + +export default ProfilePage \ No newline at end of file