Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d7872f7
[FEAT] 메인 레이아웃 구현
DDINGJOO Jan 6, 2026
5f8211e
Merge pull request #19 from Language-Study-Prooject/feature/1/2/6/log…
DDINGJOO Jan 6, 2026
c0f1a72
[FEAT] Sidebar 메뉴 계층 구조 개선
DDINGJOO Jan 6, 2026
3de5b6b
[FEAT] 면접 시뮬레이션 메뉴 제거
DDINGJOO Jan 6, 2026
6bdb2b5
[FEAT] 프리토킹(사람들과) 채팅방 리스트 페이지 구현
DDINGJOO Jan 6, 2026
7b29d50
Merge pull request #27 from Language-Study-Prooject/feature/24/25/26/…
DDINGJOO Jan 6, 2026
6f3ed4e
[FEAT] Sidebar 메뉴 카테고리 구조 변경
DDINGJOO Jan 6, 2026
3c7d786
[FEAT] Dashboard 카드 카테고리 구조 변경
DDINGJOO Jan 6, 2026
aeb3e5e
[FEAT] Dashboard 카드 호버 시 하위 카테고리 표시
DDINGJOO Jan 6, 2026
c3e1729
[FIX] Dashboard 카드 레이아웃 Grid로 변경
DDINGJOO Jan 6, 2026
0c9a8cc
Merge pull request #30 from Language-Study-Prooject/feature/28/menu-c…
DDINGJOO Jan 6, 2026
b5ee18f
[FEAT] ChatRoomCard 높이 일정하게 변경
DDINGJOO Jan 6, 2026
26ce9b0
[FIX] ChatRoomCard 가로/세로 크기 통일
DDINGJOO Jan 6, 2026
37ea06a
[FIX] ChatRoomCard 고정 높이 적용
DDINGJOO Jan 6, 2026
4402051
[FIX] ChatRoomCard 참여자 프로필 제거 및 너비 고정
DDINGJOO Jan 6, 2026
db07f45
[FIX] ChatRoomCard 레이아웃 개선
DDINGJOO Jan 6, 2026
f06bfae
[FIX] 생성일 라벨 추가
DDINGJOO Jan 6, 2026
34dc7dd
[FEAT] ChatRoomCard UI 개선
DDINGJOO Jan 6, 2026
c758b49
[FEAT] 채팅방 입장 모달 및 필터 추가
DDINGJOO Jan 6, 2026
0a47451
Merge pull request #32 from Language-Study-Prooject/fix/31/chatroom-c…
DDINGJOO Jan 6, 2026
d59ea7f
[FEAT] Chat API 연동 기반 구축
DDINGJOO Jan 6, 2026
379820f
[FEAT] 채팅방 목록/생성/입장 API 연동 및 채팅 UI 구현
DDINGJOO Jan 6, 2026
4d1cc2a
[FEAT] 전역 채팅 모달 및 TTS 기능 구현
DDINGJOO Jan 6, 2026
1a0bf22
Merge pull request #46 from Language-Study-Prooject/feature/40/41/42/…
DDINGJOO Jan 6, 2026
9792cd3
[FIX] 채팅 모달 최소화 시 우측 하단 이동
DDINGJOO Jan 6, 2026
bc76986
Merge pull request #48 from Language-Study-Prooject/fix/47/chat-modal…
DDINGJOO Jan 6, 2026
c9b4e35
[FEAT] TTS 음성 선택 기능 구현
DDINGJOO Jan 6, 2026
7fa39ad
Merge pull request #50 from Language-Study-Prooject/feature/49/tts-vo…
DDINGJOO Jan 6, 2026
6185619
[CR] TTS API 변경 대응 (#53)
DDINGJOO Jan 7, 2026
442a8e5
Merge pull request #54 from Language-Study-Prooject/feature/53/tts-ap…
DDINGJOO Jan 7, 2026
03ac906
[FIX] 채팅 모달 최소화 후 펼칠 때 스크롤 맨 아래로 유지 (#55)
DDINGJOO Jan 7, 2026
74cb7aa
Merge pull request #56 from Language-Study-Prooject/fix/55/chat-modal…
DDINGJOO Jan 7, 2026
002b09d
[FEAT] Vocab API 서비스 레이어 구축 (#58) (#99)
DDINGJOO Jan 7, 2026
6407921
[FEAT] 단어 학습 대시보드 구현 (#59) (#100)
DDINGJOO Jan 7, 2026
d47a214
[FEAT] 일일 학습 (플래시카드) 구현 (#60) (#101)
DDINGJOO Jan 7, 2026
839a8f8
[FEAT] 단어 시험 기능 구현 (#61) (#102)
DDINGJOO Jan 7, 2026
7efc5e4
[FEAT] 단어장 (Word List) 기능 구현 (#62)
DDINGJOO Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
407 changes: 373 additions & 34 deletions src/App.jsx

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions src/api/chatApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from 'axios'

const chatApi = axios.create({
baseURL: import.meta.env.VITE_CHAT_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor
chatApi.interceptors.request.use(
(config) => {
return config
},
(error) => {
return Promise.reject(error)
}
)

// Response interceptor
chatApi.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('Chat API Error:', error.response?.data || error.message)
return Promise.reject(error)
}
)

export default chatApi
30 changes: 30 additions & 0 deletions src/api/vocabApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from 'axios'

const vocabApi = axios.create({
baseURL: import.meta.env.VITE_VOCAB_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})

// Request interceptor
vocabApi.interceptors.request.use(
(config) => {
return config
},
(error) => {
return Promise.reject(error)
}
)

// Response interceptor
vocabApi.interceptors.response.use(
(response) => response.data,
(error) => {
console.error('Vocab API Error:', error.response?.data || error.message)
return Promise.reject(error)
}
)

export default vocabApi
29 changes: 29 additions & 0 deletions src/contexts/ChatContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createContext, useContext, useState, useCallback } from 'react'

const ChatContext = createContext(null)

export const ChatProvider = ({ children }) => {
const [activeRoom, setActiveRoom] = useState(null)

const openChatRoom = useCallback((room) => {
setActiveRoom(room)
}, [])

const closeChatRoom = useCallback(() => {
setActiveRoom(null)
}, [])

return (
<ChatContext.Provider value={{ activeRoom, openChatRoom, closeChatRoom }}>
{children}
</ChatContext.Provider>
)
}

export const useChat = () => {
const context = useContext(ChatContext)
if (!context) {
throw new Error('useChat must be used within a ChatProvider')
}
return context
}
42 changes: 42 additions & 0 deletions src/contexts/SettingsContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createContext, useContext, useState, useCallback, useEffect } from 'react'

const SettingsContext = createContext(null)

const STORAGE_KEY = 'app_settings'

const defaultSettings = {
ttsVoice: 'FEMALE', // MALE | FEMALE
}

export const SettingsProvider = ({ children }) => {
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem(STORAGE_KEY)
return stored ? { ...defaultSettings, ...JSON.parse(stored) } : defaultSettings
})

useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
}, [settings])

const updateSettings = useCallback((updates) => {
setSettings((prev) => ({ ...prev, ...updates }))
}, [])

const setTtsVoice = useCallback((voice) => {
updateSettings({ ttsVoice: voice })
}, [updateSettings])

return (
<SettingsContext.Provider value={{ settings, updateSettings, setTtsVoice }}>
{children}
</SettingsContext.Provider>
)
}

export const useSettings = () => {
const context = useContext(SettingsContext)
if (!context) {
throw new Error('useSettings must be used within a SettingsProvider')
}
return context
}
79 changes: 79 additions & 0 deletions src/contexts/ThemeContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createContext, useContext, useState, useEffect, useMemo } from 'react'
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
import { CssBaseline } from '@mui/material'
import { lightTheme, darkTheme } from '../theme/theme'

const ThemeContext = createContext({
mode: 'light',
toggleTheme: () => {},
setMode: () => {},
})

export const useThemeMode = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useThemeMode must be used within ThemeProvider')
}
return context
}

export const ThemeProvider = ({ children }) => {
// localStorage에서 테마 모드 불러오기 (시스템 설정 기본값)
const [mode, setMode] = useState(() => {
const savedMode = localStorage.getItem('themeMode')
if (savedMode) return savedMode

// 시스템 다크모드 감지
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
}
return 'light'
})

// 시스템 테마 변경 감지
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e) => {
const savedMode = localStorage.getItem('themeMode')
if (!savedMode) {
setMode(e.matches ? 'dark' : 'light')
}
}

mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])

// 테마 변경 시 localStorage 저장
useEffect(() => {
localStorage.setItem('themeMode', mode)
// body 클래스 추가 (CSS 변수 등에서 활용)
document.body.classList.remove('light-mode', 'dark-mode')
document.body.classList.add(`${mode}-mode`)
}, [mode])

const toggleTheme = () => {
setMode((prev) => (prev === 'light' ? 'dark' : 'light'))
}

const theme = useMemo(() => {
return mode === 'dark' ? darkTheme : lightTheme
}, [mode])

const contextValue = useMemo(() => ({
mode,
toggleTheme,
setMode,
}), [mode])

return (
<ThemeContext.Provider value={contextValue}>
<MuiThemeProvider theme={theme}>
<CssBaseline />
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}

export default ThemeContext
83 changes: 83 additions & 0 deletions src/domains/chat/services/chatService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import chatApi from '../../../api/chatApi'

const TEMP_USER_ID = import.meta.env.VITE_TEMP_USER_ID || 'user1'

// 채팅방 API
export const chatRoomService = {
// 채팅방 생성
create: async (data) => {
return chatApi.post('/chat/rooms', {
...data,
createdBy: TEMP_USER_ID,
})
},

// 채팅방 목록 조회
getList: async (params = {}) => {
const { limit = 10, level, joined, cursor } = params
const queryParams = new URLSearchParams()

queryParams.append('limit', limit)
if (level) queryParams.append('level', level)
if (joined) {
queryParams.append('joined', 'true')
queryParams.append('userId', TEMP_USER_ID)
}
if (cursor) queryParams.append('cursor', cursor)

return chatApi.get(`/chat/rooms?${queryParams.toString()}`)
},

// 채팅방 상세 조회
getDetail: async (roomId) => {
return chatApi.get(`/chat/rooms/${roomId}`)
},

// 채팅방 입장
join: async (roomId, password) => {
return chatApi.post(`/chat/rooms/${roomId}/join`, {
userId: TEMP_USER_ID,
...(password && { password }),
})
},

// 채팅방 퇴장
leave: async (roomId) => {
return chatApi.post(`/chat/rooms/${roomId}/leave`, {
userId: TEMP_USER_ID,
})
},
}

// 메시지 API
export const messageService = {
// 메시지 전송
send: async (roomId, content, messageType = 'TEXT') => {
return chatApi.post(`/chat/rooms/${roomId}/messages`, {
userId: TEMP_USER_ID,
content,
messageType,
})
},

// 메시지 목록 조회
getList: async (roomId, params = {}) => {
const { limit = 20, cursor } = params
const queryParams = new URLSearchParams()

queryParams.append('limit', limit)
if (cursor) queryParams.append('cursor', cursor)

return chatApi.get(`/chat/rooms/${roomId}/messages?${queryParams.toString()}`)
},
}

// 음성 API
export const voiceService = {
// TTS 변환
synthesize: async (messageId, roomId, voice = 'FEMALE') => {
return chatApi.post('/chat/voice/synthesize', { messageId, roomId, voice })
},
}

export { TEMP_USER_ID }
Loading