@readerseye2/cr_type 1.0.124 → 1.0.126
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.
- package/dist/book/book-api.type.d.ts +15 -15
- package/dist/book/book-api.type.js +2 -2
- package/dist/book/book-log.type.d.ts +14 -14
- package/dist/book/book-log.type.js +2 -2
- package/dist/book/book.const.d.ts +274 -274
- package/dist/book/book.const.js +119 -119
- package/dist/book/child-reading-progress.type.d.ts +171 -6
- package/dist/book/child-reading-progress.type.js +8 -0
- package/dist/book/legacy.book.type.d.ts +164 -164
- package/dist/book/legacy.book.type.js +41 -41
- package/package.json +27 -27
package/dist/book/book.const.js
CHANGED
|
@@ -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 =
|
|
7
|
+
export type CalibrationType = "quick" | "full";
|
|
8
8
|
/** 섹션 읽기 중 발생한 캘리브레이션 기간 */
|
|
9
9
|
export interface CalibrationPeriod {
|
|
10
10
|
type: CalibrationType;
|
|
11
|
-
/** 사용한 포인트 수 (quick:
|
|
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
|
-
/** 이 구간 내 눈 깜빡임 횟수 (
|
|
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
|
-
|
|
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 });
|