@readerseye2/cr_type 1.0.124 → 1.0.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,119 +1,119 @@
1
- "use strict";
2
- // ========== 리터럴 유니온 타입 ==========
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.getAgeOptions = exports.getAgeRatingOptions = exports.getEnLevelOptions = exports.getGenreOptions = exports.getLengthOptions = exports.getLanguageOptions = exports.calcSoundMinutes = exports.getLexileDescription = exports.getARDescription = exports.LEXILE_INDEX_RANGES = exports.AR_INDEX_RANGES = exports.DEFAULT_BOOK_META = exports.BOOK_AGE_MAX = exports.BOOK_AGE_MIN = exports.PublishAgeRatingLabel = exports.BookEnLevelLabel = exports.BookGenreLabel = exports.BookLengthLabel = exports.BookLanguageLabel = void 0;
5
- // ========== 라벨 (UI 표시용) ==========
6
- exports.BookLanguageLabel = {
7
- ko: '한국어',
8
- en: 'English',
9
- };
10
- exports.BookLengthLabel = {
11
- short: '단편',
12
- medium: '중편',
13
- long: '장편',
14
- };
15
- exports.BookGenreLabel = {
16
- fiction: '문학',
17
- 'non-fiction': '비문학',
18
- other: '기타',
19
- };
20
- exports.BookEnLevelLabel = {
21
- story: '스토리',
22
- readers: '리더스',
23
- 'early-chapter': '얼리챕터',
24
- 'middle-chapter': '미들챕터',
25
- chapter: '챕터',
26
- novel: '노블',
27
- };
28
- exports.PublishAgeRatingLabel = {
29
- all: '전체이용가',
30
- '12': '12세 이용가',
31
- '15': '15세 이용가',
32
- '19': '19세 이용가',
33
- };
34
- // ========== 상수 ==========
35
- /** 권장 연령 범위 (5~19세) */
36
- exports.BOOK_AGE_MIN = 5;
37
- exports.BOOK_AGE_MAX = 19;
38
- /** 새 책 생성 시 기본값 */
39
- exports.DEFAULT_BOOK_META = {
40
- title: '나의 책',
41
- language: 'en',
42
- age: 5,
43
- length: 'short',
44
- genre: 'fiction',
45
- quiz_retry_allowed: true,
46
- ar_index: 3.0,
47
- lexile_index: 600,
48
- };
49
- // ========== AR / Lexile 지수 ==========
50
- /** AR 지수 범위별 설명 (소수점 1자리 기준) */
51
- exports.AR_INDEX_RANGES = [
52
- { min: 0.0, max: 1.9, level: '유아 ~ 초1', description: '그림책, 아주 짧은 문장' },
53
- { min: 2.0, max: 2.9, level: '초2', description: '기초 리더북' },
54
- { min: 3.0, max: 3.9, level: '초3', description: '챕터북 시작' },
55
- { min: 4.0, max: 4.9, level: '초4', description: '본격적인 이야기 구조' },
56
- { min: 5.0, max: 5.9, level: '초5', description: '어휘·문장 길이 증가' },
57
- { min: 6.0, max: 6.9, level: '초6', description: '논픽션 비중 증가' },
58
- { min: 7.0, max: 8.9, level: '중학생', description: '복합 문장, 추론 필요' },
59
- { min: 9.0, max: 10.9, level: '고등학생', description: '문학 작품, 추상 개념' },
60
- { min: 11.0, max: 12.0, level: '고급', description: '성인 소설·고전' },
61
- ];
62
- /** Lexile 지수 범위별 설명 */
63
- exports.LEXILE_INDEX_RANGES = [
64
- { min: 0, max: 200, level: '유아', description: '알파벳·기초 단어' },
65
- { min: 200, max: 400, level: '초1', description: '아주 쉬운 문장' },
66
- { min: 400, max: 600, level: '초2', description: '간단한 스토리' },
67
- { min: 600, max: 800, level: '초3', description: '챕터북' },
68
- { min: 800, max: 1000, level: '초4', description: '정보량 증가' },
69
- { min: 1000, max: 1200, level: '초5~6', description: '교과서 수준' },
70
- { min: 1200, max: 1400, level: '중학생', description: '추론·비판적 읽기' },
71
- { min: 1400, max: 1600, level: '고등학생', description: '문학·비문학 혼합' },
72
- { min: 1600, max: 2000, level: '성인', description: '학술·고전 문학' },
73
- ];
74
- /** AR 지수로 해당 범위 설명 조회 */
75
- const getARDescription = (ar) => exports.AR_INDEX_RANGES.find(r => ar >= r.min && ar <= r.max);
76
- exports.getARDescription = getARDescription;
77
- /** Lexile 지수로 해당 범위 설명 조회 */
78
- const getLexileDescription = (lexile) => exports.LEXILE_INDEX_RANGES.find(r => lexile >= r.min && lexile <= r.max);
79
- exports.getLexileDescription = getLexileDescription;
80
- // ========== 헬퍼 함수 ==========
81
- /** 어절 수 → 재생 분수 계산 (200 WPM 기준) */
82
- const calcSoundMinutes = (wordCount) => Math.ceil(wordCount / 200);
83
- exports.calcSoundMinutes = calcSoundMinutes;
84
- /** 언어 옵션 (UI Select용) */
85
- const getLanguageOptions = () => Object.entries(exports.BookLanguageLabel).map(([value, label]) => ({
86
- value: value,
87
- label,
88
- }));
89
- exports.getLanguageOptions = getLanguageOptions;
90
- /** 길이 옵션 (UI Select용) */
91
- const getLengthOptions = () => Object.entries(exports.BookLengthLabel).map(([value, label]) => ({
92
- value: value,
93
- label,
94
- }));
95
- exports.getLengthOptions = getLengthOptions;
96
- /** 장르 옵션 (UI Select용) */
97
- const getGenreOptions = () => Object.entries(exports.BookGenreLabel).map(([value, label]) => ({
98
- value: value,
99
- label,
100
- }));
101
- exports.getGenreOptions = getGenreOptions;
102
- /** 영어레벨 옵션 (UI Select용) */
103
- const getEnLevelOptions = () => Object.entries(exports.BookEnLevelLabel).map(([value, label]) => ({
104
- value: value,
105
- label,
106
- }));
107
- exports.getEnLevelOptions = getEnLevelOptions;
108
- /** 출판 연령등급 옵션 (UI Select용) */
109
- const getAgeRatingOptions = () => Object.entries(exports.PublishAgeRatingLabel).map(([value, label]) => ({
110
- value: value,
111
- label,
112
- }));
113
- exports.getAgeRatingOptions = getAgeRatingOptions;
114
- /** 권장 연령 옵션 (5~19세) */
115
- const getAgeOptions = () => Array.from({ length: exports.BOOK_AGE_MAX - exports.BOOK_AGE_MIN + 1 }, (_, i) => ({
116
- value: exports.BOOK_AGE_MIN + i,
117
- label: `만 ${exports.BOOK_AGE_MIN + i}세`,
118
- }));
119
- exports.getAgeOptions = getAgeOptions;
1
+ "use strict";
2
+ // ========== 리터럴 유니온 타입 ==========
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getAgeOptions = exports.getAgeRatingOptions = exports.getEnLevelOptions = exports.getGenreOptions = exports.getLengthOptions = exports.getLanguageOptions = exports.calcSoundMinutes = exports.getLexileDescription = exports.getARDescription = exports.LEXILE_INDEX_RANGES = exports.AR_INDEX_RANGES = exports.DEFAULT_BOOK_META = exports.BOOK_AGE_MAX = exports.BOOK_AGE_MIN = exports.PublishAgeRatingLabel = exports.BookEnLevelLabel = exports.BookGenreLabel = exports.BookLengthLabel = exports.BookLanguageLabel = void 0;
5
+ // ========== 라벨 (UI 표시용) ==========
6
+ exports.BookLanguageLabel = {
7
+ ko: '한국어',
8
+ en: 'English',
9
+ };
10
+ exports.BookLengthLabel = {
11
+ short: '단편',
12
+ medium: '중편',
13
+ long: '장편',
14
+ };
15
+ exports.BookGenreLabel = {
16
+ fiction: '문학',
17
+ 'non-fiction': '비문학',
18
+ other: '기타',
19
+ };
20
+ exports.BookEnLevelLabel = {
21
+ story: '스토리',
22
+ readers: '리더스',
23
+ 'early-chapter': '얼리챕터',
24
+ 'middle-chapter': '미들챕터',
25
+ chapter: '챕터',
26
+ novel: '노블',
27
+ };
28
+ exports.PublishAgeRatingLabel = {
29
+ all: '전체이용가',
30
+ '12': '12세 이용가',
31
+ '15': '15세 이용가',
32
+ '19': '19세 이용가',
33
+ };
34
+ // ========== 상수 ==========
35
+ /** 권장 연령 범위 (5~19세) */
36
+ exports.BOOK_AGE_MIN = 5;
37
+ exports.BOOK_AGE_MAX = 19;
38
+ /** 새 책 생성 시 기본값 */
39
+ exports.DEFAULT_BOOK_META = {
40
+ title: '나의 책',
41
+ language: 'en',
42
+ age: 5,
43
+ length: 'short',
44
+ genre: 'fiction',
45
+ quiz_retry_allowed: true,
46
+ ar_index: 3.0,
47
+ lexile_index: 600,
48
+ };
49
+ // ========== AR / Lexile 지수 ==========
50
+ /** AR 지수 범위별 설명 (소수점 1자리 기준) */
51
+ exports.AR_INDEX_RANGES = [
52
+ { min: 0.0, max: 1.9, level: '유아 ~ 초1', description: '그림책, 아주 짧은 문장' },
53
+ { min: 2.0, max: 2.9, level: '초2', description: '기초 리더북' },
54
+ { min: 3.0, max: 3.9, level: '초3', description: '챕터북 시작' },
55
+ { min: 4.0, max: 4.9, level: '초4', description: '본격적인 이야기 구조' },
56
+ { min: 5.0, max: 5.9, level: '초5', description: '어휘·문장 길이 증가' },
57
+ { min: 6.0, max: 6.9, level: '초6', description: '논픽션 비중 증가' },
58
+ { min: 7.0, max: 8.9, level: '중학생', description: '복합 문장, 추론 필요' },
59
+ { min: 9.0, max: 10.9, level: '고등학생', description: '문학 작품, 추상 개념' },
60
+ { min: 11.0, max: 12.0, level: '고급', description: '성인 소설·고전' },
61
+ ];
62
+ /** Lexile 지수 범위별 설명 */
63
+ exports.LEXILE_INDEX_RANGES = [
64
+ { min: 0, max: 200, level: '유아', description: '알파벳·기초 단어' },
65
+ { min: 200, max: 400, level: '초1', description: '아주 쉬운 문장' },
66
+ { min: 400, max: 600, level: '초2', description: '간단한 스토리' },
67
+ { min: 600, max: 800, level: '초3', description: '챕터북' },
68
+ { min: 800, max: 1000, level: '초4', description: '정보량 증가' },
69
+ { min: 1000, max: 1200, level: '초5~6', description: '교과서 수준' },
70
+ { min: 1200, max: 1400, level: '중학생', description: '추론·비판적 읽기' },
71
+ { min: 1400, max: 1600, level: '고등학생', description: '문학·비문학 혼합' },
72
+ { min: 1600, max: 2000, level: '성인', description: '학술·고전 문학' },
73
+ ];
74
+ /** AR 지수로 해당 범위 설명 조회 */
75
+ const getARDescription = (ar) => exports.AR_INDEX_RANGES.find(r => ar >= r.min && ar <= r.max);
76
+ exports.getARDescription = getARDescription;
77
+ /** Lexile 지수로 해당 범위 설명 조회 */
78
+ const getLexileDescription = (lexile) => exports.LEXILE_INDEX_RANGES.find(r => lexile >= r.min && lexile <= r.max);
79
+ exports.getLexileDescription = getLexileDescription;
80
+ // ========== 헬퍼 함수 ==========
81
+ /** 어절 수 → 재생 분수 계산 (200 WPM 기준) */
82
+ const calcSoundMinutes = (wordCount) => Math.ceil(wordCount / 200);
83
+ exports.calcSoundMinutes = calcSoundMinutes;
84
+ /** 언어 옵션 (UI Select용) */
85
+ const getLanguageOptions = () => Object.entries(exports.BookLanguageLabel).map(([value, label]) => ({
86
+ value: value,
87
+ label,
88
+ }));
89
+ exports.getLanguageOptions = getLanguageOptions;
90
+ /** 길이 옵션 (UI Select용) */
91
+ const getLengthOptions = () => Object.entries(exports.BookLengthLabel).map(([value, label]) => ({
92
+ value: value,
93
+ label,
94
+ }));
95
+ exports.getLengthOptions = getLengthOptions;
96
+ /** 장르 옵션 (UI Select용) */
97
+ const getGenreOptions = () => Object.entries(exports.BookGenreLabel).map(([value, label]) => ({
98
+ value: value,
99
+ label,
100
+ }));
101
+ exports.getGenreOptions = getGenreOptions;
102
+ /** 영어레벨 옵션 (UI Select용) */
103
+ const getEnLevelOptions = () => Object.entries(exports.BookEnLevelLabel).map(([value, label]) => ({
104
+ value: value,
105
+ label,
106
+ }));
107
+ exports.getEnLevelOptions = getEnLevelOptions;
108
+ /** 출판 연령등급 옵션 (UI Select용) */
109
+ const getAgeRatingOptions = () => Object.entries(exports.PublishAgeRatingLabel).map(([value, label]) => ({
110
+ value: value,
111
+ label,
112
+ }));
113
+ exports.getAgeRatingOptions = getAgeRatingOptions;
114
+ /** 권장 연령 옵션 (5~19세) */
115
+ const getAgeOptions = () => Array.from({ length: exports.BOOK_AGE_MAX - exports.BOOK_AGE_MIN + 1 }, (_, i) => ({
116
+ value: exports.BOOK_AGE_MIN + i,
117
+ label: `만 ${exports.BOOK_AGE_MIN + i}세`,
118
+ }));
119
+ exports.getAgeOptions = getAgeOptions;
@@ -4,11 +4,11 @@ export interface ReadRange {
4
4
  to: number;
5
5
  }
6
6
  /** 캘리브레이션 종류 */
7
- export type CalibrationType = 'quick' | 'full';
7
+ export type CalibrationType = "quick" | "full";
8
8
  /** 섹션 읽기 중 발생한 캘리브레이션 기간 */
9
9
  export interface CalibrationPeriod {
10
10
  type: CalibrationType;
11
- /** 사용한 포인트 수 (quick: 1~2, full: 5) */
11
+ /** 사용한 포인트 수 (quick: 3, full: 5) */
12
12
  points: number;
13
13
  startedAt: string;
14
14
  endedAt: string;
@@ -16,6 +16,32 @@ export interface CalibrationPeriod {
16
16
  /** 캘리브레이션 결과 품질 (0~1, 기기 제공) */
17
17
  quality?: number;
18
18
  }
19
+ /** 퀴즈 시도 결과 (개별 퀴즈) */
20
+ export interface QuizAttemptResult {
21
+ quizId: string;
22
+ /** 획득 점수 */
23
+ score: number;
24
+ /** 만점 */
25
+ maxScore: number;
26
+ isCorrect: boolean;
27
+ /** 소요 시간 (ms) */
28
+ timeMs: number;
29
+ /** 타임아웃 여부 */
30
+ timedOut: boolean;
31
+ attemptedAt: string;
32
+ }
33
+ /** 찾아본 단어 기록 */
34
+ export interface LookedUpWord {
35
+ /** 원문 텍스트 */
36
+ text: string;
37
+ /** 시작 GI */
38
+ startGI: number;
39
+ /** 끝 GI */
40
+ endGI: number;
41
+ /** 번역 결과 (있으면) */
42
+ translatedText?: string;
43
+ lookedUpAt: string;
44
+ }
19
45
  export interface ReadingProgressReport {
20
46
  bookIdx: number;
21
47
  sectionId: string;
@@ -34,8 +60,26 @@ export interface ReadingProgressReport {
34
60
  * 예: { "101": 300, "102": 450, "103": 67 }
35
61
  */
36
62
  gazeDwellMap: Record<string, number>;
37
- /** 이 구간 내 눈 깜빡임 횟수 (Tobii valid 전환 감지) */
63
+ /** 이 구간 내 눈 깜빡임 횟수 (valid 전환 감지) */
38
64
  blinkCount: number;
65
+ /**
66
+ * 이 구간에서 시선이 텍스트([data-g]) 위에 있었던 시간 (ms)
67
+ * → 집중도(concentration) 계산용: gazeOnTextMs / durationMs * 100
68
+ * → 시선 추적 없으면 0
69
+ */
70
+ gazeOnTextMs: number;
71
+ /**
72
+ * 오디오 재생 중 시선이 현재 하이라이트 GI 근처에 있었던 시간 (ms)
73
+ * → 시선일치도(gazeAlignment) 계산용
74
+ * → 오디오 미재생 또는 시선추적 없으면 null
75
+ */
76
+ gazeAlignedMs: number | null;
77
+ /** 오디오 재생 중 시선추적이 활성이었던 총 시간 (ms) */
78
+ audioGazeActiveMs: number | null;
79
+ /** 이 구간에서 찾아본 단어 */
80
+ lookedUpWords: LookedUpWord[];
81
+ /** 이 구간에서 완료한 퀴즈 */
82
+ quizResults: QuizAttemptResult[];
39
83
  /** 소요 시간 ms */
40
84
  durationMs: number;
41
85
  }
@@ -44,16 +88,48 @@ export interface ChildSectionProgress {
44
88
  bookIdx: number;
45
89
  sectionId: string;
46
90
  sectionGIMax: number;
91
+ /** 섹션 단어 수 (SectionSummary.word_count, WPM 계산용) */
92
+ sectionWordCount: number;
47
93
  scrolledRanges: ReadRange[];
94
+ /** unique GI 수 (mergeReadRanges 후) */
48
95
  scrolledCount: number;
96
+ /** scrolledCount / sectionGIMax */
49
97
  scrolledCoverage: number;
50
98
  gazeReadRanges: ReadRange[];
51
99
  gazeReadCount: number;
52
100
  gazeReadCoverage: number;
101
+ /** 모든 flush의 (scrolledTo - scrolledFrom + 1) 합산 */
102
+ totalRawScrolledGIs: number;
103
+ /** 모든 flush의 gaze dwell ≥ threshold GI 수 합산 */
104
+ totalRawGazeReadGIs: number;
53
105
  totalReadMs: number;
54
106
  lastGlobalIndex: number;
55
107
  lastReadAt: string;
108
+ /** 검증된 읽기속도: gazeReadCount / (totalReadMs / 60000) — WPM 근사 (GI ≈ word) */
109
+ readingSpeedWPM: number | null;
110
+ /** 미검증 읽기속도: scrolledCount / (totalReadMs / 60000) */
111
+ unverifiedReadingSpeedWPM: number | null;
112
+ /** 누적: 시선이 텍스트 위에 있었던 시간 (ms) */
113
+ totalGazeOnTextMs: number;
114
+ /** 집중도: totalGazeOnTextMs / totalReadMs * 100 (%, 시선추적 없으면 null) */
115
+ concentrationPercent: number | null;
116
+ /** 누적: 오디오 재생 중 시선이 하이라이트 GI 근처에 있었던 시간 */
117
+ totalGazeAlignedMs: number;
118
+ /** 누적: 오디오 재생 중 시선추적이 활성이었던 시간 */
119
+ totalAudioGazeActiveMs: number;
120
+ /** 시선일치도: totalGazeAlignedMs / totalAudioGazeActiveMs (0~1, 데이터 없으면 null) */
121
+ gazeAlignmentScore: number | null;
56
122
  totalBlinks: number;
123
+ /** 찾아본 단어 목록 (unique text 기준) */
124
+ lookedUpWords: LookedUpWord[];
125
+ quizScore: number;
126
+ quizMaxScore: number;
127
+ quizCorrectCount: number;
128
+ quizTotalCount: number;
129
+ /** quizCorrectCount / quizTotalCount (0~1, 퀴즈 없으면 null) */
130
+ quizAccuracy: number | null;
131
+ /** 누적 읽기 점수 (알고리즘 산출, 읽기마다 추가) */
132
+ readingScore: number;
57
133
  calibrations: CalibrationPeriod[];
58
134
  createdAt: string;
59
135
  updatedAt: string;
@@ -68,6 +144,13 @@ export interface ChildBookBookmark {
68
144
  totalScrolledCoverage: number;
69
145
  totalGazeReadCount: number;
70
146
  totalGazeReadCoverage: number;
147
+ totalReadingScore: number;
148
+ /** 책 전체 평균 WPM (검증된 속도) */
149
+ avgReadingSpeedWPM: number | null;
150
+ /** 책 전체 집중도 (%) */
151
+ avgConcentrationPercent: number | null;
152
+ /** 책 전체 퀴즈 정답률 */
153
+ totalQuizAccuracy: number | null;
71
154
  createdAt: string;
72
155
  updatedAt: string;
73
156
  }
@@ -80,6 +163,15 @@ export interface ChildReadingLog {
80
163
  /** 전체 dwell map 그대로 보존 */
81
164
  gazeDwellMap: Record<string, number>;
82
165
  blinkCount: number;
166
+ /** 시선이 텍스트 위에 있었던 시간 */
167
+ gazeOnTextMs: number;
168
+ /** 오디오 재생 중 시선 일치 시간 */
169
+ gazeAlignedMs: number | null;
170
+ audioGazeActiveMs: number | null;
171
+ /** 찾아본 단어 */
172
+ lookedUpWords: LookedUpWord[];
173
+ /** 퀴즈 결과 */
174
+ quizResults: QuizAttemptResult[];
83
175
  durationMs: number;
84
176
  createdAt: string;
85
177
  }
@@ -102,6 +194,7 @@ export interface SegmentReadingSummary {
102
194
  sectionId: string;
103
195
  bookIdx: number;
104
196
  sectionGIMax: number;
197
+ sectionWordCount: number;
105
198
  startedAt: string;
106
199
  endedAt: string;
107
200
  durationMs: number;
@@ -109,15 +202,27 @@ export interface SegmentReadingSummary {
109
202
  scrolledTo: number;
110
203
  scrolledGICount: number;
111
204
  gazeReadGICount: number;
205
+ rawScrolledGICount: number;
206
+ rawGazeReadGICount: number;
207
+ /** gazeReadGICount / (durationMs / 60000) — WPM 근사 */
208
+ readingSpeedWPM: number | null;
209
+ /** scrolledGICount / (durationMs / 60000) */
210
+ unverifiedReadingSpeedWPM: number;
112
211
  /** gazeReadGICount / scrolledGICount (시선 없으면 null) */
113
212
  focusRatio: number | null;
114
- /** 평균 읽기 속도 (GI per minute) */
115
- readingSpeedGPM: number;
116
213
  /** 평균 dwell time (ms per GI) */
117
214
  avgDwellMs: number | null;
215
+ /** 집중도: gazeOnTextMs / durationMs * 100 */
216
+ concentrationPercent: number | null;
217
+ /** 시선일치도 (0~1, 오디오+시선추적 시에만) */
218
+ gazeAlignmentScore: number | null;
118
219
  totalBlinks: number;
119
220
  /** 분당 깜빡임 횟수 */
120
221
  blinksPerMinute: number;
222
+ lookedUpWordCount: number;
223
+ quizScore: number;
224
+ quizMaxScore: number;
225
+ quizAccuracy: number | null;
121
226
  calibrations: CalibrationPeriod[];
122
227
  }
123
228
  export interface ChildReadingSessionSummary {
@@ -130,9 +235,67 @@ export interface ChildReadingSessionSummary {
130
235
  totalScrolledGICount: number;
131
236
  totalGazeReadGICount: number;
132
237
  overallFocusRatio: number | null;
133
- avgReadingSpeedGPM: number;
238
+ avgReadingSpeedWPM: number | null;
239
+ avgUnverifiedReadingSpeedWPM: number;
240
+ overallConcentrationPercent: number | null;
241
+ overallGazeAlignmentScore: number | null;
134
242
  totalBlinks: number;
135
243
  avgBlinksPerMinute: number;
244
+ totalLookedUpWords: number;
245
+ totalQuizScore: number;
246
+ totalQuizMaxScore: number;
247
+ overallQuizAccuracy: number | null;
248
+ /** 이 세션에서 획득한 읽기 점수 */
249
+ sessionReadingScore: number;
250
+ }
251
+ /**
252
+ * 읽기 점수 산출 입력값
253
+ * - 각 메트릭 가중치를 적용하여 종합 점수 산출
254
+ * - 점수는 누적 (읽을수록 올라감)
255
+ */
256
+ export interface ReadingScoreInput {
257
+ /** 진도 (중첩비허용 coverage, 0~1) */
258
+ scrolledCoverage: number;
259
+ /** 검증된 진도 (gazeRead coverage, 0~1) */
260
+ gazeReadCoverage: number;
261
+ /** 집중도 (%, 0~100) */
262
+ concentrationPercent: number | null;
263
+ /** 시선일치도 (0~1) */
264
+ gazeAlignmentScore: number | null;
265
+ /** 퀴즈 정답률 (0~1) */
266
+ quizAccuracy: number | null;
267
+ /** 읽기 시간 (ms) */
268
+ durationMs: number;
269
+ /** 찾아본 단어 수 */
270
+ lookedUpWordCount: number;
271
+ }
272
+ /**
273
+ * 현재 섹션 읽기 중 클라이언트에서 실시간 누적하는 상태
274
+ * - readingProgress.service.ts 모듈 스코프에서 관리
275
+ * - 5초 flush마다 서버에 delta 전송 + 로컬 누적 갱신
276
+ * - 섹션 변경/세션 종료 시 리셋
277
+ */
278
+ export interface ReadingAccumulatedState {
279
+ bookIdx: number;
280
+ sectionId: string;
281
+ sectionGIMax: number;
282
+ sectionWordCount: number;
283
+ scrolledRanges: ReadRange[];
284
+ scrolledCoverage: number;
285
+ totalRawScrolledGIs: number;
286
+ totalReadMs: number;
287
+ lastGlobalIndex: number;
288
+ totalGazeOnTextMs: number;
289
+ totalGazeAlignedMs: number;
290
+ totalAudioGazeActiveMs: number;
291
+ concentrationPercent: number | null;
292
+ gazeAlignmentScore: number | null;
293
+ totalBlinks: number;
294
+ lookedUpWords: LookedUpWord[];
295
+ quizResults: QuizAttemptResult[];
296
+ quizScore: number;
297
+ quizMaxScore: number;
298
+ estimatedReadingScore: number;
136
299
  }
137
300
  /** GET /api/reading-progress/:bookIdx */
138
301
  export interface ChildBookProgressResponse {
@@ -144,6 +307,8 @@ export interface DailyReadingStat {
144
307
  date: string;
145
308
  totalMs: number;
146
309
  books: number[];
310
+ /** 그 날 획득한 읽기 점수 */
311
+ readingScore: number;
147
312
  }
148
313
  /** GET /api/reading-progress/daily-stats */
149
314
  export interface DailyReadingStatsResponse {
@@ -1,4 +1,12 @@
1
1
  "use strict";
2
2
  // src/book/child-reading-progress.type.ts
3
3
  // 자녀 읽기 진행 데이터 — MongoDB 5컬렉션 + WS/REST 타입
4
+ //
5
+ // 메트릭 개요:
6
+ // 누가(childIdx) 언제(timestamps) 무엇을(bookIdx+sectionId)
7
+ // 진도(coverage, 중첩비허용) 읽은양(raw, 중첩허용) 읽기속도(WPM)
8
+ // 시선일치도(gazeAlignment, 오디오재생+시선추적시)
9
+ // 집중도(concentration, gazeOnText/total %)
10
+ // 찾아본단어(lookedUpWords) 퀴즈점수/정답률
11
+ // 미검증 읽은양/속도(scrollOnly) 종합 읽기점수(readingScore, 누적)
4
12
  Object.defineProperty(exports, "__esModule", { value: true });