Levo — "Elevate + Vocab" | 언어로 나를 끌어올리다
Duolingo 스타일의 외국어 학습 앱 백엔드 서버
항목
내용
런타임
Node.js (v20+)
프레임워크
Express.js
데이터베이스
MongoDB (Mongoose ODM)
인증
JWT + OAuth 2.0 (Google, Apple)
언어
TypeScript
API 스타일
RESTful JSON API
문서화
Swagger (OpenAPI 3.0)
Client (React App)
│
▼
┌─────────────────────────────────────────┐
│ API Gateway (Express) │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Rate │ │ CORS │ │
│ │Middleware│ │ Limiter │ │ Setup │ │
│ └─────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────┤
│ Route Layer │
│ /api/v1/auth /api/v1/users ... │
├─────────────────────────────────────────┤
│ Controller Layer │
│ (요청 검증, 응답 포맷팅) │
├─────────────────────────────────────────┤
│ Service Layer │
│ (비즈니스 로직, 게이미피케이션 규칙) │
├─────────────────────────────────────────┤
│ Model Layer │
│ (Mongoose Schema & Methods) │
├─────────────────────────────────────────┤
│ MongoDB │
│ ┌────────┐ ┌────────┐ ┌────────────┐ │
│ │ Users │ │Contents│ │ Progress │ │
│ └────────┘ └────────┘ └────────────┘ │
└─────────────────────────────────────────┘
모든 학습 콘텐츠는 targetLanguage (학습 대상 언어) 기준으로 완전히 독립적으로 동작합니다.
언어별 독립 콘텐츠 : 단어, 문법, 회화, 듣기, 읽기 콘텐츠는 각 targetLanguage 별로 별도 도큐먼트로 관리
언어별 독립 진행도 : 사용자의 학습 진행도, 레슨 잠금 해제, XP 등은 언어별로 독립 추적
언어 전환 가능 : 사용자가 언어를 변경하면 해당 언어의 진행 상태로 전환 (기존 진행도 보존)
모든 쿼리에 언어 필터 : API 호출 시 targetLanguage 파라미터 또는 사용자의 현재 활성 언어 기준으로 필터링
사용자 A: 영어 학습 중 → 단어장 요청
→ DB 조회: { targetLanguage: 'en', level: 'beginner' }
→ 영어 단어만 반환
사용자 A: 일본어로 변경 → 단어장 요청
→ DB 조회: { targetLanguage: 'ja', level: 'beginner' }
→ 일본어 단어만 반환 (영어 진행도는 그대로 보존)
Levo-be/
├── src/
│ ├── app.ts # Express 앱 설정
│ ├── server.ts # 서버 진입점
│ ├── config/
│ │ ├── index.ts # 환경 변수 설정
│ │ ├── database.ts # MongoDB 연결
│ │ └── swagger.ts # Swagger 설정
│ ├── middleware/
│ │ ├── auth.ts # JWT 인증 미들웨어
│ │ ├── validate.ts # 요청 유효성 검사
│ │ ├── errorHandler.ts # 글로벌 에러 핸들러
│ │ └── rateLimiter.ts # API 요청 제한
│ ├── models/
│ │ ├── User.ts # 사용자 모델
│ │ ├── UserLanguageProfile.ts# 사용자 언어별 프로필
│ │ ├── Vocabulary.ts # 단어 콘텐츠
│ │ ├── Grammar.ts # 문법 콘텐츠
│ │ ├── Conversation.ts # 회화 콘텐츠
│ │ ├── Listening.ts # 듣기 콘텐츠
│ │ ├── Reading.ts # 읽기 콘텐츠
│ │ ├── Lesson.ts # 레슨 구성
│ │ ├── Quiz.ts # 퀴즈 문제
│ │ ├── UserProgress.ts # 사용자 학습 진행도 (언어별)
│ │ ├── UserStreak.ts # 스트릭 기록
│ │ ├── UserBadge.ts # 뱃지 획득 기록
│ │ ├── Badge.ts # 뱃지 정의
│ │ ├── CoinTransaction.ts # 코인 거래 내역
│ │ └── Subscription.ts # 프리미엄 구독
│ ├── routes/
│ │ ├── index.ts # 라우터 통합
│ │ ├── auth.routes.ts
│ │ ├── user.routes.ts
│ │ ├── vocabulary.routes.ts
│ │ ├── grammar.routes.ts
│ │ ├── conversation.routes.ts
│ │ ├── listening.routes.ts
│ │ ├── reading.routes.ts
│ │ ├── lesson.routes.ts
│ │ ├── quiz.routes.ts
│ │ ├── review.routes.ts
│ │ ├── streak.routes.ts
│ │ ├── badge.routes.ts
│ │ ├── coin.routes.ts
│ │ ├── stats.routes.ts
│ │ └── subscription.routes.ts
│ ├── controllers/
│ │ ├── auth.controller.ts
│ │ ├── user.controller.ts
│ │ ├── vocabulary.controller.ts
│ │ ├── grammar.controller.ts
│ │ ├── conversation.controller.ts
│ │ ├── listening.controller.ts
│ │ ├── reading.controller.ts
│ │ ├── lesson.controller.ts
│ │ ├── quiz.controller.ts
│ │ ├── review.controller.ts
│ │ ├── streak.controller.ts
│ │ ├── badge.controller.ts
│ │ ├── coin.controller.ts
│ │ ├── stats.controller.ts
│ │ └── subscription.controller.ts
│ ├── services/
│ │ ├── auth.service.ts
│ │ ├── user.service.ts
│ │ ├── vocabulary.service.ts
│ │ ├── grammar.service.ts
│ │ ├── conversation.service.ts
│ │ ├── listening.service.ts
│ │ ├── reading.service.ts
│ │ ├── lesson.service.ts
│ │ ├── quiz.service.ts
│ │ ├── review.service.ts # 간격 반복 알고리즘
│ │ ├── streak.service.ts
│ │ ├── badge.service.ts
│ │ ├── coin.service.ts
│ │ ├── heart.service.ts # 하트 소모/충전 로직
│ │ ├── stats.service.ts
│ │ └── subscription.service.ts
│ ├── utils/
│ │ ├── ApiError.ts # 커스텀 에러 클래스
│ │ ├── ApiResponse.ts # 표준 응답 포맷
│ │ ├── constants.ts # 상수 정의
│ │ └── helpers.ts # 유틸 함수
│ └── types/
│ ├── express.d.ts # Express 타입 확장
│ └── index.ts # 공통 타입 정의
├── tests/
│ ├── unit/
│ └── integration/
├── .env.example
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md
{
_id: ObjectId,
email: String (unique),
name: String,
profileImage: String,
provider: 'google' | 'apple' | 'email',
providerId: String,
// 현재 활성 학습 언어
activeLanguage: String, // 'en' | 'ja' | 'zh'
// 설정
settings: {
dailyGoalMinutes: Number, // 5 | 10 | 15 | 20
notificationEnabled: Boolean,
notificationHour: Number, // 0-23
soundEnabled: Boolean,
effectsEnabled: Boolean,
},
// 글로벌 (언어 무관)
isPremium: Boolean,
premiumExpiresAt: Date,
coins: Number,
createdAt: Date,
updatedAt: Date,
}
2. UserLanguageProfile (사용자 언어별 프로필)
⭐ 핵심 모델 — 언어별 독립 진행도의 핵심
{
_id: ObjectId,
userId: ObjectId (ref: User),
targetLanguage: String, // 'en' | 'ja' | 'zh'
level: 'beginner' | 'elementary' | 'intermediate' | 'advanced',
xp: Number,
userLevel: Number, // 계산된 사용자 레벨 (Lv.1, Lv.12 등)
// 하트 시스템 (언어별 독립)
hearts: Number, // 현재 하트 수 (max 5)
lastHeartLostAt: Date, // 마지막 하트 소모 시각
// 카테고리별 진행률
vocabularyProgress: Number, // 0-100
grammarProgress: Number,
conversationProgress: Number,
listeningProgress: Number,
readingProgress: Number,
quizProgress: Number,
// 스트릭 실드 보유 수
streakShields: Number,
createdAt: Date,
updatedAt: Date,
}
인덱스 : { userId: 1, targetLanguage: 1 } (unique compound)
{
_id: ObjectId,
userId: ObjectId (ref: User),
targetLanguage: String,
currentStreak: Number, // 현재 연속 일수
longestStreak: Number, // 최장 스트릭
lastStudyDate: Date, // 마지막 학습 날짜 (YYYY-MM-DD)
// 주간 기록
weeklyRecord: [{
date: String, // 'YYYY-MM-DD'
completed: Boolean,
minutesStudied: Number,
}],
// 스트릭 실드 사용 이력
shieldUsedDates: [String], // ['2024-01-15', ...]
createdAt: Date,
updatedAt: Date,
}
인덱스 : { userId: 1, targetLanguage: 1 } (unique compound)
{
_id: ObjectId,
targetLanguage: String, // 'en' | 'ja' | 'zh'
word: String, // 'Apple'
pronunciation: String, // '[ˈæpəl]'
meaning: String, // '사과' (한국어 뜻)
partOfSpeech: String, // '명사'
level: String, // 'beginner' | 'elementary' | ...
chapter: Number, // 챕터 번호
exampleSentence: String, // 'I eat an apple every day.'
exampleTranslation: String, // '나는 매일 사과를 먹어요.'
audioUrl: String, // 발음 오디오 URL
order: Number, // 챕터 내 정렬 순서
createdAt: Date,
}
인덱스 : { targetLanguage: 1, level: 1, chapter: 1 }
{
_id: ObjectId,
targetLanguage: String,
title: String, // '현재진행형'
subtitle: String, // 'be동사 + 동사ing'
englishTitle: String, // 'Present Progressive'
icon: String, // '🔵'
level: String,
order: Number,
// 핵심 공식
formula: String, // 'be동사 (am/is/are) + 동사원형 + ing'
formulaExample: String,
// 설명
explanation: String, // 마크다운 형식 설명
// 예문 목록
examples: [{
sentence: String,
translation: String,
highlight: String, // 강조할 부분
}],
// 연관 퀴즈 ID 목록
quizIds: [ObjectId],
createdAt: Date,
}
인덱스 : { targetLanguage: 1, level: 1 }
{
_id: ObjectId,
targetLanguage: String,
title: String, // '공항에서'
emoji: String, // '✈️'
level: String,
order: Number,
// 대화문
dialogs: [{
speaker: 'A' | 'B',
text: String, // 'Excuse me, where is gate 12?'
translation: String, // '실례합니다, 12번 게이트가 어디인가요?'
isUserRole: Boolean, // 사용자가 연습할 대사인지
audioUrl: String,
}],
// 핵심 표현
keyExpressions: [{
expression: String,
meaning: String,
}],
createdAt: Date,
}
인덱스 : { targetLanguage: 1, level: 1 }
{
_id: ObjectId,
targetLanguage: String,
audioText: String, // 'I like to read books'
correctAnswer: String, // 'I like to read books'
hint: String, // '나는 책 읽는 것을 좋아한다'
difficulty: String, // 'beginner' | 'elementary' | ...
audioUrl: String,
order: Number,
createdAt: Date,
}
{
_id: ObjectId,
targetLanguage: String,
title: String, // 'My Daily Routine'
difficulty: String,
content: String, // 영문 지문
wordCount: Number,
// 관련 퀴즈
quizzes: [{
question: String,
options: [String],
correctAnswer: Number, // 정답 인덱스
explanation: String,
}],
order: Number,
createdAt: Date,
}
{
_id: ObjectId,
targetLanguage: String,
unitNumber: Number, // Unit 번호
unitTitle: String, // 'Unit 1 - 기초 인사'
lessonNumber: Number, // 레슨 번호
lessonTitle: String, // '자기소개하기'
// 레슨 내 포함 콘텐츠
newWords: [ObjectId], // Vocabulary refs
grammarPoints: [ObjectId], // Grammar refs
// 레슨 퀴즈
quizzes: [{
type: 'multiple' | 'listening' | 'grammar' | 'reading',
question: String,
options: [String],
correctAnswer: Number | String,
explanation: String,
}],
estimatedMinutes: Number, // 예상 소요 시간
xpReward: Number, // 완료 시 XP 보상
coinReward: Number, // 완료 시 코인 보상
// 잠금 해제 조건
prerequisiteLessonId: ObjectId, // 선행 레슨 (null이면 첫 레슨)
order: Number,
createdAt: Date,
}
인덱스 : { targetLanguage: 1, unitNumber: 1, order: 1 }
10. UserProgress (사용자 학습 진행도)
{
_id: ObjectId,
userId: ObjectId,
targetLanguage: String,
// 레슨 진행
completedLessons: [ObjectId], // 완료한 레슨 ID 목록
currentLessonId: ObjectId, // 현재 진행 중인 레슨
// 단어 학습 상태
vocabularyStatus: [{
wordId: ObjectId,
status: 'new' | 'learning' | 'completed' | 'wrong',
correctCount: Number,
wrongCount: Number,
lastReviewedAt: Date,
nextReviewAt: Date, // 간격 반복용
}],
// 문법 학습 상태
grammarStatus: [{
grammarId: ObjectId,
progress: Number, // 0-100
quizScore: Number,
lastReviewedAt: Date,
nextReviewAt: Date,
}],
// 회화 학습 상태
conversationStatus: [{
conversationId: ObjectId,
completed: Boolean,
pronunciationScore: Number,
lastReviewedAt: Date,
}],
// 오답 노트
wrongAnswers: [{
type: 'vocabulary' | 'grammar' | 'listening' | 'reading' | 'quiz',
contentId: ObjectId,
question: String,
userAnswer: String,
correctAnswer: String,
createdAt: Date,
}],
createdAt: Date,
updatedAt: Date,
}
인덱스 : { userId: 1, targetLanguage: 1 } (unique compound)
{
_id: ObjectId,
name: String, // '7일 챔피언'
emoji: String, // '🔥'
category: 'streak' | 'learning' | 'level' | 'special',
condition: {
type: String, // 'streak_days' | 'lessons_completed' | ...
value: Number, // 7, 30, 100 등
},
createdAt: Date,
}
12. UserBadge (사용자 뱃지 획득)
{
_id: ObjectId,
userId: ObjectId,
badgeId: ObjectId (ref: Badge),
targetLanguage: String,
achievedAt: Date,
}
13. CoinTransaction (코인 거래)
{
_id: ObjectId,
userId: ObjectId,
type: 'earn' | 'spend',
amount: Number,
reason: 'lesson_complete' | 'ad_watch' | 'daily_check' | 'friend_invite'
| 'heart_refill' | 'streak_shield' | 'hint_purchase' | 'profile_item'
| 'package_purchase',
balanceAfter: Number, // 거래 후 잔액
createdAt: Date,
}
{
_id: ObjectId,
userId: ObjectId,
plan: 'monthly' | 'yearly',
status: 'active' | 'cancelled' | 'expired',
startDate: Date,
endDate: Date,
// 결제 정보
paymentProvider: 'apple' | 'google',
transactionId: String,
createdAt: Date,
updatedAt: Date,
}
Method
Endpoint
설명
POST
/api/v1/auth/google
Google OAuth 로그인
POST
/api/v1/auth/apple
Apple OAuth 로그인
POST
/api/v1/auth/refresh
토큰 갱신
POST
/api/v1/auth/logout
로그아웃
Method
Endpoint
설명
GET
/api/v1/users/me
내 프로필 조회
PATCH
/api/v1/users/me
프로필 수정
PATCH
/api/v1/users/me/settings
설정 변경
PATCH
/api/v1/users/me/language
활성 언어 변경
POST
/api/v1/users/me/onboarding
온보딩 완료 (언어+레벨+목표+알림 일괄)
Method
Endpoint
설명
GET
/api/v1/vocabulary
단어 목록 (언어/레벨/챕터 필터)
GET
/api/v1/vocabulary/:id
단어 상세
GET
/api/v1/vocabulary/flashcards
플래시카드 세트 조회
POST
/api/v1/vocabulary/:id/answer
플래시카드 정답/오답 기록
Method
Endpoint
설명
GET
/api/v1/grammar
문법 토픽 목록
GET
/api/v1/grammar/:id
문법 상세 (공식+예문)
GET
/api/v1/grammar/:id/quiz
문법 퀴즈 문제
POST
/api/v1/grammar/:id/quiz/answer
퀴즈 정답 제출
Method
Endpoint
설명
GET
/api/v1/conversations
회화 상황 목록
GET
/api/v1/conversations/:id
대화문 상세
POST
/api/v1/conversations/:id/practice
발음 연습 결과 저장
Method
Endpoint
설명
GET
/api/v1/listening
듣기 문제 목록
POST
/api/v1/listening/:id/answer
받아쓰기 정답 제출
Method
Endpoint
설명
GET
/api/v1/reading
읽기 지문 목록
GET
/api/v1/reading/:id
지문 상세 + 퀴즈
POST
/api/v1/reading/:id/quiz/answer
독해 퀴즈 정답 제출
Method
Endpoint
설명
GET
/api/v1/lessons
레슨 맵 (유닛별 레슨 + 잠금 상태)
GET
/api/v1/lessons/:id
레슨 상세 (미리보기)
POST
/api/v1/lessons/:id/start
레슨 시작
POST
/api/v1/lessons/:id/complete
레슨 완료 (XP/코인 지급)
Method
Endpoint
설명
GET
/api/v1/quiz/daily
오늘의 종합 퀴즈
POST
/api/v1/quiz/answer
퀴즈 정답 제출
POST
/api/v1/quiz/complete
퀴즈 세션 완료
Method
Endpoint
설명
GET
/api/v1/review
복습 대시보드 (카테고리별 현황)
GET
/api/v1/review/:category
카테고리별 복습 항목
POST
/api/v1/review/:category/complete
복습 완료 기록
Method
Endpoint
설명
GET
/api/v1/hearts
현재 하트 상태
POST
/api/v1/hearts/use
하트 소모 (오답 시)
POST
/api/v1/hearts/refill
하트 충전 (광고/코인)
Method
Endpoint
설명
GET
/api/v1/streak
스트릭 상세 정보
POST
/api/v1/streak/shield
스트릭 실드 사용
Method
Endpoint
설명
GET
/api/v1/badges
전체 뱃지 + 획득 여부
Method
Endpoint
설명
GET
/api/v1/coins
보유 코인 + 거래 내역
POST
/api/v1/coins/earn
코인 획득 (광고/출석/초대)
POST
/api/v1/coins/spend
코인 사용 (아이템 구매)
Method
Endpoint
설명
GET
/api/v1/stats
학습 통계 (기간 필터)
GET
/api/v1/stats/weekly
주간 상세 통계
Method
Endpoint
설명
GET
/api/v1/subscription
현재 구독 상태
POST
/api/v1/subscription/subscribe
구독 시작
POST
/api/v1/subscription/cancel
구독 취소
최대 보유: 5개
소모: 퀴즈/레슨에서 오답 시 1개 차감
자동 충전: 30분마다 1개 (마지막 소모 시점 기준)
즉시 충전: 광고 시청 (전체) 또는 코인 사용 (1개: 20코인, 전체: 50코인)
프리미엄: 무제한
하루에 최소 1개 학습 활동 완료 시 스트릭 유지
자정(KST) 기준으로 날짜 전환
스트릭 실드: 하루 빠져도 스트릭 유지 (코인 100개 / 프리미엄 주 1개 지급)
레슨 완료: +120 XP
플래시카드 세트 완료: +50 XP
복습 완료: +30 XP
레벨업 공식: 필요 XP = level * 100
레슨 완료: +15 코인
광고 시청: +30 코인
출석 체크: +10 코인
친구 초대: +100 코인
1. 클라이언트에서 Google/Apple 로그인 수행
2. OAuth 토큰을 서버로 전송 → POST /api/v1/auth/google
3. 서버에서 토큰 검증 → 사용자 조회 또는 생성
4. JWT Access Token (15분) + Refresh Token (7일) 발급
5. 이후 모든 요청에 Authorization: Bearer <accessToken> 헤더 포함
6. 만료 시 Refresh Token으로 갱신 → POST /api/v1/auth/refresh
# 의존성 설치
npm install
# 환경 변수 설정
cp .env.example .env
# 개발 서버 실행
npm run dev
# 빌드
npm run build
# 프로덕션 실행
npm start
PORT=5000
MONGODB_URI=mongodb://localhost:27017/levo
JWT_SECRET=your-secret-key
JWT_REFRESH_SECRET=your-refresh-secret
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
GOOGLE_CLIENT_ID=your-google-client-id
APPLE_CLIENT_ID=your-apple-client-id