Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bc58205
[Feat] : AI 프리토킹(Speaking) 기능 구현 (#195)
hye-inA Jan 23, 2026
52ceba7
[FEAT] SSE 기반 실시간 알림 시스템 연동 (#197) (#198)
DDINGJOO Jan 24, 2026
232f3c9
fix : 연속 선언된 변수 t 제거 (#199)
hye-inA Jan 24, 2026
39c2c5d
Merge branch 'prod' into develop
hye-inA Jan 24, 2026
05e7928
refactor : AI 말하기 routing 페이지 수정 (#202)
hye-inA Jan 24, 2026
f4114e0
Merge branch 'prod' into develop
hye-inA Jan 24, 2026
f8ea2ed
[FEAT] 채팅 슬래시 명령어 시스템 구현 (#200) (#204)
DDINGJOO Jan 24, 2026
ba1cc05
[FEAT] 영어 끝말잇기(Word Chain) 게임 구현 (#205) (#206)
DDINGJOO Jan 24, 2026
d979845
[FIX] 채팅 슬래시 명령어 버그 수정 (#207)
DDINGJOO Jan 24, 2026
44fa7d9
refactor : 로그인/인증 로직 정상화 및 채팅 서버 연결 및 메인 화면 헤더 프로필 상태 표시 (#208)
hye-inA Jan 25, 2026
100c283
[FIX] 끝말잇기 게임 버그 수정 및 UI 개선 (#210)
DDINGJOO Jan 25, 2026
ee93158
feat : 사용자 프로필 닉네임 표시 구현 (#212)
hye-inA Jan 25, 2026
83a9a18
feature : 채팅 화면 컴포넌트에 닉네임 필드 추가 (#213)
hye-inA Jan 26, 2026
43eb0f5
Feature : OPIc 모의고사 테스트 진행(녹음/제출) 및 세션 완료 기능 구현 (#216)
hye-inA Jan 27, 2026
956b6e0
featrue : OPIC 말하기연습 전체 세션에 대한 피드백 리포트 화면 구현 (#218)
hye-inA Jan 27, 2026
7cc571b
[STYLE] Adjust formatting for improved readability and consistency
DDINGJOO Jan 28, 2026
0769355
Merge origin/prod into develop - resolve conflicts
DDINGJOO Jan 28, 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
15 changes: 4 additions & 11 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ 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'
import OPIcPage from './domains/opic/pages/OPIcPage'


function ProtectedRoute({ children }) {
Expand Down Expand Up @@ -665,16 +667,6 @@ function Dashboard() {
)
}

// Placeholder Pages
function OpicPage() {
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<Typography variant="h4" fontWeight={700}>OPIC Practice</Typography>
<Typography color="text.secondary">Level-based training</Typography>
</Container>
)
}


function ReportsPage() {
const { isKorean } = useSettings()
Expand Down Expand Up @@ -1183,7 +1175,8 @@ function App() {
}>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/opic" element={<OpicPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/opic" element={<OPIcPage />} />
<Route path="/freetalk/people" element={<FreetalkPeoplePage />} />
<Route path="/freetalk/ai" element={<SpeakingPage />} />
<Route path="/writing" element={<WritingPage />} />
Expand Down
13 changes: 13 additions & 0 deletions src/api/opicApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import api from './axios'

const OPIC_TIMEOUT = 60000

const opicApi = {
get: (url, config) => api.get(url, { timeout: OPIC_TIMEOUT, ...config }).then(res => res.data),
post: (url, data, config) => api.post(url, data, { timeout: OPIC_TIMEOUT, ...config }).then(res => res.data),
put: (url, data, config) => api.put(url, data, { timeout: OPIC_TIMEOUT, ...config }).then(res => res.data),
patch: (url, data, config) => api.patch(url, data, { timeout: OPIC_TIMEOUT, ...config }).then(res => res.data),
delete: (url, config) => api.delete(url, { timeout: OPIC_TIMEOUT, ...config }).then(res => res.data),
}

export default opicApi
209 changes: 105 additions & 104 deletions src/domains/freetalk/components/ChatRoomModal.jsx

Large diffs are not rendered by default.

319 changes: 319 additions & 0 deletions src/domains/opic/constants/opicConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
/**
* OPIc domain constants
* Based on grammarConstants.js pattern
*/

// ============================================
// 주제 타입
// ============================================
export const OPIC_TOPICS = {
DESCRIPTION: 'DESCRIPTION',
HABIT: 'HABIT',
PAST_EXPERIENCE: 'PAST_EXPERIENCE',
COMPARISON: 'COMPARISON',
ROLE_PLAY: 'ROLE_PLAY',
ISSUE: 'ISSUE',
}


export const OPIC_TOPIC_LABELS = {
DESCRIPTION: { ko: '단순 묘사', en: 'Description' },
HABIT: { ko: '습관/경향', en: 'Habit' },
PAST_EXPERIENCE: { ko: '과거 경험', en: 'Past Experience' },
COMPARISON: { ko: '비교', en: 'Comparison' },
ROLE_PLAY: { ko: '롤플레이', en: 'Role Play' },
ISSUE: { ko: '사회 이슈/심화', en: 'Social Issues' },
}

// 주제별 아이콘
export const OPIC_TOPIC_ICONS = {
DESCRIPTION: '🏠',
HABIT: '🔄',
PAST_EXPERIENCE: '📅',
}

// 주제별 색상
export const OPIC_TOPIC_COLORS = {
DESCRIPTION: '#3b82f6',
HABIT: '#22c55e',
PAST_EXPERIENCE: '#f97316',
}

// ============================================
// 세부 주제
// ============================================
export const OPIC_SUBTOPICS = [
{ value: 'BANKS', labelKo: '은행', labelEn: 'Banks' },
{ value: 'BARS', labelKo: '술집/바', labelEn: 'Bars' },
{ value: 'CAFES', labelKo: '카페', labelEn: 'Cafes' },
{ value: 'CONCERTS', labelKo: '콘서트', labelEn: 'Concerts' },
{ value: 'FAMILY', labelKo: '가족', labelEn: 'Family' },
{ value: 'FURNITURE', labelKo: '가구', labelEn: 'Furniture' },
{ value: 'GAMES', labelKo: '게임', labelEn: 'Games' },
{ value: 'GYM', labelKo: '헬스장', labelEn: 'Gym' },
{ value: 'HEALTH', labelKo: '건강', labelEn: 'Health' },
{ value: 'HOLIDAYS', labelKo: '명절/휴일', labelEn: 'Holidays' },
{ value: 'HOMES', labelKo: '집', labelEn: 'Homes' },
{ value: 'HOTEL', labelKo: '호텔', labelEn: 'Hotel' },
{ value: 'INTERNET', labelKo: '인터넷', labelEn: 'Internet' },
{ value: 'MOVIES', labelKo: '영화', labelEn: 'Movies' },
{ value: 'MUSIC', labelKo: '음악', labelEn: 'Music' },
{ value: 'PARKS', labelKo: '공원', labelEn: 'Parks' },
{ value: 'PHONE', labelKo: '전화기', labelEn: 'Phone' },
{ value: 'RECYCLING', labelKo: '재활용', labelEn: 'Recycling' },
{ value: 'RESTAURANTS', labelKo: '식당', labelEn: 'Restaurants' },
{ value: 'SHOPPING', labelKo: '쇼핑', labelEn: 'Shopping' },
{ value: 'TECHNOLOGY', labelKo: '기술', labelEn: 'Technology' },
{ value: 'TRAVEL', labelKo: '여행', labelEn: 'Travel' },
{ value: 'VACATION', labelKo: '휴가', labelEn: 'Vacation' },
{ value: 'WEATHER', labelKo: '날씨', labelEn: 'Weather' },
{ value: 'FREESTYLE', labelKo: '돌발/자유', labelEn: 'Freestyle' },
{ value: 'GENERAL', labelKo: '일반', labelEn: 'General' },
];

// ============================================
// 레벨
// ============================================
export const OPIC_LEVELS = {
IM1: 'IM1',
IM2: 'IM2',
IM3: 'IM3',
IH: 'IH',
AL: 'AL',
}

export const OPIC_LEVEL_LABELS = {
IM1: { ko: 'IM1 (중급 하)', en: 'IM1 (Intermediate Mid 1)' },
IM2: { ko: 'IM2 (중급 중)', en: 'IM2 (Intermediate Mid 2)' },
IM3: { ko: 'IM3 (중급 상)', en: 'IM3 (Intermediate Mid 3)' },
IH: { ko: 'IH (중상급)', en: 'IH (Intermediate High)' },
AL: { ko: 'AL (고급)', en: 'AL (Advanced Low)' },
}

// 레벨별 색상
export const OPIC_LEVEL_COLORS = {
IM1: '#22c55e',
IM2: '#3b82f6',
IM3: '#8b5cf6',
IH: '#f97316',
}

// 레벨별 배경 색상
export const OPIC_LEVEL_BG_COLORS = {
IM1: '#f0fdf4',
IM2: '#eff6ff',
IM3: '#f5f3ff',
IH: '#fff7ed',
}

// ============================================
// UI 번역 (한국어)
// ============================================
export const OPIC_UI_KO = {
// 헤더
title: 'OPIc 스피킹 테스트',
subtitle: 'AI 기반 영어 스피킹 연습 및 피드백',

// 세션 설정
sessionSetup: '세션 설정',
topic: '주제',
subTopic: '세부 주제',
targetLevel: '목표 레벨',
start: '시작하기',

// 질문
question: '질문',
listenQuestion: '질문 듣기',

// 녹음
recordAnswer: '답변 녹음',
startRecording: '녹음 시작',
stopRecording: '녹음 중지',
recording: '녹음 중...',
recordingReady: '녹음 완료',
playToReview: '재생하여 확인하세요',
reRecord: '다시 녹음',

// 제출
getUploadUrl: 'Upload URL 발급',
preparing: '준비 중...',
submitAnswer: '답변 제출',
submitting: '제출 중...',

// 피드백
aiFeedback: 'AI 피드백',
yourAnswer: '내 답변',
correctedAnswer: '교정된 답변',
modelAnswer: '모범 답변',
grammarErrors: '문법 오류',

// 네비게이션
nextQuestion: '다음 질문',
completeSession: '세션 완료',
completing: '완료 중...',

// 결과
sessionCompleted: '세션이 완료되었습니다!',
checkReport: '리포트를 확인하세요.',
overallScore: '종합 점수',
totalQuestions: '총 질문 수',
answeredQuestions: '답변한 질문',

// 에러
errorCreateSession: '세션 생성에 실패했습니다',
errorGetQuestion: '다음 질문을 불러오는데 실패했습니다',
errorStartRecording: '녹음을 시작할 수 없습니다',
errorGetUploadUrl: 'Upload URL 발급에 실패했습니다',
errorSubmitAnswer: '답변 제출에 실패했습니다',
errorCompleteSession: '세션 완료에 실패했습니다',

// 마이크 권한
micPermissionRequired: '마이크 권한이 필요합니다',
micPermissionDenied: '마이크 권한이 거부되었습니다',
}

// ============================================
// UI 번역 (영어)
// ============================================
export const OPIC_UI_EN = {
// 헤더
title: 'OPIc Speaking Test',
subtitle: 'AI-powered English speaking practice & feedback',

// 세션 설정
sessionSetup: 'Session Setup',
topic: 'Topic',
subTopic: 'Sub Topic',
targetLevel: 'Target Level',
start: 'Start Session',

// 질문
question: 'Question',
listenQuestion: 'Listen to question',

// 녹음
recordAnswer: 'Record Answer',
startRecording: 'Start Recording',
stopRecording: 'Stop Recording',
recording: 'Recording...',
recordingReady: 'Recording Ready',
playToReview: 'Play to review',
reRecord: 'Re-record',

// 제출
getUploadUrl: 'Get Upload URL',
preparing: 'Preparing...',
submitAnswer: 'Submit Answer',
submitting: 'Submitting...',

// 피드백
aiFeedback: 'AI Feedback',
yourAnswer: 'Your Answer',
correctedAnswer: 'Corrected Answer',
modelAnswer: 'Model Answer',
grammarErrors: 'Grammar Errors',

// 네비게이션
nextQuestion: 'Next Question',
completeSession: 'Complete Session',
completing: 'Completing...',

// 결과
sessionCompleted: 'Session completed!',
checkReport: 'Check your report.',
overallScore: 'Overall Score',
totalQuestions: 'Total Questions',
answeredQuestions: 'Answered Questions',

// 에러
errorCreateSession: 'Failed to create session',
errorGetQuestion: 'Failed to get next question',
errorStartRecording: 'Failed to start recording',
errorGetUploadUrl: 'Failed to get upload URL',
errorSubmitAnswer: 'Failed to submit answer',
errorCompleteSession: 'Failed to complete session',

// 마이크 권한
micPermissionRequired: 'Microphone permission required',
micPermissionDenied: 'Microphone permission denied',
}

// ============================================
// 점수 등급 (grammarConstants 패턴)
// ============================================
export const OPIC_SCORE_GRADES = {
EXCELLENT: { min: 90, max: 100, label: 'Excellent', labelKo: '훌륭해요!' },
GOOD: { min: 70, max: 89, label: 'Good', labelKo: '잘했어요!' },
FAIR: { min: 50, max: 69, label: 'Fair', labelKo: '괜찮아요' },
POOR: { min: 0, max: 49, label: 'Needs Practice', labelKo: '더 연습해요' },
}

// 점수에 따른 등급 반환
export const getOpicScoreGrade = (score) => {
if (score >= 90) return OPIC_SCORE_GRADES.EXCELLENT
if (score >= 70) return OPIC_SCORE_GRADES.GOOD
if (score >= 50) return OPIC_SCORE_GRADES.FAIR
return OPIC_SCORE_GRADES.POOR
}

// 점수에 따른 색상 반환
export const getOpicScoreColor = (score) => {
if (score >= 90) return '#059669'
if (score >= 70) return '#3b82f6'
if (score >= 50) return '#f97316'
return '#ef4444'
}

// ============================================
// 설정값
// ============================================
export const OPIC_CONFIG = {
DEFAULT_TOTAL_QUESTIONS: 12,
MAX_RECORDING_TIME: 120, // 2분
MIN_RECORDING_TIME: 5, // 5초
AUDIO_MIME_TYPE: 'audio/webm',
FALLBACK_MIME_TYPE: 'audio/mp4',
}

// ============================================
// 헬퍼 함수 (오류 수정됨)
// ============================================

// 언어에 따른 주제 라벨 반환
export const getTopicLabel = (topic, isKorean) => {
const labelObj = OPIC_TOPIC_LABELS[topic];
return labelObj ? (isKorean ? labelObj.ko : labelObj.en) : topic;
}

// 언어에 따른 세부주제 라벨 반환
export const getSubtopicLabel = (subtopicValue, isKorean) => {
const found = OPIC_SUBTOPICS.find(item => item.value === subtopicValue);
if (!found) return subtopicValue;
return isKorean ? found.labelKo : found.labelEn;
}

// 언어에 따른 레벨 라벨 반환
export const getLevelLabel = (level, isKorean) => {
const labelObj = OPIC_LEVEL_LABELS[level];
return labelObj ? (isKorean ? labelObj.ko : labelObj.en) : level;
}

// 녹음 시간 포맷 (mm:ss)
export const formatRecordingTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}

export default {
OPIC_TOPICS,
OPIC_TOPIC_LABELS,
OPIC_TOPIC_ICONS,
OPIC_SUBTOPICS,
OPIC_LEVELS,
OPIC_LEVEL_LABELS,
getTopicLabel,
getSubtopicLabel,
getLevelLabel,
formatRecordingTime,
}
Loading