@nago730/chatbot-library 1.0.0 → 1.1.0
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/LICENSE +21 -0
- package/README.md +369 -90
- package/dist/index.d.mts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +35 -30
- package/src/engine.ts +25 -0
- package/src/examples/firebaseAdapter.example.ts +421 -0
- package/src/index.ts +3 -0
- package/src/types.ts +40 -0
- package/src/useChat.ts +339 -0
package/package.json
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@nago730/chatbot-library",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "청소업체 등 고객 정보 수집을 위한 실시간 계산 챗봇 엔진",
|
|
5
|
-
"main": "./dist/index.js",
|
|
6
|
-
"module": "./dist/index.mjs",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"react"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"react": "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@nago730/chatbot-library",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "청소업체 등 고객 정보 수집을 위한 실시간 계산 챗봇 엔진",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean --minify",
|
|
14
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --clean",
|
|
15
|
+
"lint": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"chatbot",
|
|
19
|
+
"headless",
|
|
20
|
+
"react"
|
|
21
|
+
],
|
|
22
|
+
"author": "Your Name",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=16.8.0",
|
|
26
|
+
"react-dom": ">=16.8.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/react": "^19.0.0",
|
|
30
|
+
"@types/react-dom": "^19.0.0",
|
|
31
|
+
"react": "^19.0.0",
|
|
32
|
+
"react-dom": "^19.0.0",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
}
|
|
31
36
|
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ChatNode } from './types';
|
|
2
|
+
|
|
3
|
+
export class ChatEngine {
|
|
4
|
+
constructor(private flow: Record<string, ChatNode>) {}
|
|
5
|
+
|
|
6
|
+
getCurrentNode(stepId: string): ChatNode {
|
|
7
|
+
const node = this.flow[stepId];
|
|
8
|
+
if (!node) {
|
|
9
|
+
throw new Error(`ChatEngineError: Node with id "${stepId}" not found in flow.`);
|
|
10
|
+
}
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getNextStep(currentStepId: string, answer: any): string {
|
|
15
|
+
const node = this.flow[currentStepId];
|
|
16
|
+
if (!node) {
|
|
17
|
+
throw new Error(`ChatEngineError: Cannot calculate next step from missing node "${currentStepId}".`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof node.next === 'function') {
|
|
21
|
+
return node.next(answer);
|
|
22
|
+
}
|
|
23
|
+
return node.next;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// Firebase Storage Adapter 예제 (프로덕션 레디)
|
|
2
|
+
// 이 파일은 사용자 프로젝트에서 참고할 수 있도록 제공되는 예제입니다.
|
|
3
|
+
// 실제 사용 시 Firebase SDK를 설치하고 초기화해야 합니다.
|
|
4
|
+
|
|
5
|
+
import { StorageAdapter, ChatState } from '@nago730/chatbot-library';
|
|
6
|
+
// import { doc, getDoc, setDoc, serverTimestamp, Timestamp } from 'firebase/firestore';
|
|
7
|
+
// import { db } from './firebaseConfig'; // 사용자의 Firebase 설정
|
|
8
|
+
|
|
9
|
+
// ========================================
|
|
10
|
+
// 타입 정의 및 유틸리티
|
|
11
|
+
// ========================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Firebase에 저장되는 메타데이터 구조
|
|
15
|
+
*/
|
|
16
|
+
interface FirebaseMetadata {
|
|
17
|
+
currentStep: string;
|
|
18
|
+
flowHash: string;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
answerCount: number;
|
|
21
|
+
messageCount: number;
|
|
22
|
+
lastSyncedAt?: any; // serverTimestamp() 결과
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Firebase에 저장되는 전체 상태 구조
|
|
27
|
+
*/
|
|
28
|
+
interface FirebaseFullState extends ChatState {
|
|
29
|
+
lastSyncedAt?: any; // serverTimestamp() 결과
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adapter 설정 옵션
|
|
34
|
+
*/
|
|
35
|
+
interface AdapterOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Firebase 호출 타임아웃 (ms)
|
|
38
|
+
* @default 5000
|
|
39
|
+
*/
|
|
40
|
+
timeout?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 에러 발생 시 로컬 데이터로 폴백할지 여부
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
fallbackToLocal?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 디버그 로그 활성화
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
debug?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Firebase Timestamp를 number로 변환
|
|
57
|
+
* (Issue #3: 데이터 직렬화/역직렬화 대응)
|
|
58
|
+
*/
|
|
59
|
+
const normalizeTimestamp = (value: any): number => {
|
|
60
|
+
if (!value) return Date.now();
|
|
61
|
+
|
|
62
|
+
// Firebase Timestamp 객체인 경우
|
|
63
|
+
if (typeof value === 'object' && 'toMillis' in value) {
|
|
64
|
+
return value.toMillis();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 이미 number인 경우
|
|
68
|
+
if (typeof value === 'number') {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Date 객체인 경우
|
|
73
|
+
if (value instanceof Date) {
|
|
74
|
+
return value.getTime();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 기타 경우 현재 시간 반환
|
|
78
|
+
return Date.now();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* ChatState의 타임스탬프를 정규화
|
|
83
|
+
*/
|
|
84
|
+
const normalizeChatState = (state: any): ChatState => {
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
updatedAt: normalizeTimestamp(state.updatedAt),
|
|
88
|
+
messages: state.messages?.map((msg: any) => ({
|
|
89
|
+
...msg,
|
|
90
|
+
timestamp: normalizeTimestamp(msg.timestamp)
|
|
91
|
+
})) || []
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 타임아웃 기능이 있는 Promise 래퍼
|
|
97
|
+
*/
|
|
98
|
+
const withTimeout = async <T>(
|
|
99
|
+
promise: Promise<T>,
|
|
100
|
+
timeoutMs: number,
|
|
101
|
+
errorMessage: string = 'Operation timed out'
|
|
102
|
+
): Promise<T> => {
|
|
103
|
+
return Promise.race([
|
|
104
|
+
promise,
|
|
105
|
+
new Promise<T>((_, reject) =>
|
|
106
|
+
setTimeout(() => reject(new Error(errorMessage)), timeoutMs)
|
|
107
|
+
)
|
|
108
|
+
]);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ========================================
|
|
112
|
+
// 하이브리드 Firebase Adapter (권장)
|
|
113
|
+
// ========================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 하이브리드 Firebase Adapter
|
|
117
|
+
*
|
|
118
|
+
* 특징:
|
|
119
|
+
* - 로컬스토리지: 모든 대화 내역을 빠르게 저장/불러오기
|
|
120
|
+
* - Firebase: 메타데이터만 저장하여 비용 절감
|
|
121
|
+
* - Guest 모드: 대화 종료 시점에만 서버 전송
|
|
122
|
+
*
|
|
123
|
+
* 개선사항:
|
|
124
|
+
* ✅ Issue #1: 로컬 데이터가 없을 때 서버에서 전체 데이터 가져오기
|
|
125
|
+
* ✅ Issue #2: 에러 핸들링 및 타임아웃 처리
|
|
126
|
+
* ✅ Issue #3: Firebase Timestamp 정규화
|
|
127
|
+
* ✅ Issue #4: Guest/회원 모드 구분 및 비용 최적화
|
|
128
|
+
*/
|
|
129
|
+
export const createHybridFirebaseAdapter = (
|
|
130
|
+
db: any,
|
|
131
|
+
options: AdapterOptions = {}
|
|
132
|
+
): StorageAdapter => {
|
|
133
|
+
const {
|
|
134
|
+
timeout = 5000,
|
|
135
|
+
fallbackToLocal = true,
|
|
136
|
+
debug = false
|
|
137
|
+
} = options;
|
|
138
|
+
|
|
139
|
+
const log = (...args: any[]) => {
|
|
140
|
+
if (debug) console.log('[HybridFirebaseAdapter]', ...args);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
saveState: async (userId, state) => {
|
|
145
|
+
try {
|
|
146
|
+
// 1. 로컬스토리지에 전체 상태 저장 (이미 useChat에서 수행됨)
|
|
147
|
+
// 이 함수는 주로 서버 저장을 담당
|
|
148
|
+
|
|
149
|
+
// 2. Firebase에는 메타데이터만 저장 (비용 절감)
|
|
150
|
+
const metadata: FirebaseMetadata = {
|
|
151
|
+
currentStep: state.currentStep,
|
|
152
|
+
flowHash: state.flowHash,
|
|
153
|
+
updatedAt: state.updatedAt,
|
|
154
|
+
answerCount: Object.keys(state.answers).length,
|
|
155
|
+
messageCount: state.messages.length
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Firebase 저장 로직 (주석 해제 후 사용)
|
|
159
|
+
/*
|
|
160
|
+
const docRef = doc(db, 'chat_metadata', userId);
|
|
161
|
+
await withTimeout(
|
|
162
|
+
setDoc(docRef, {
|
|
163
|
+
...metadata,
|
|
164
|
+
lastSyncedAt: serverTimestamp()
|
|
165
|
+
}, { merge: true }),
|
|
166
|
+
timeout,
|
|
167
|
+
'Firebase save timeout'
|
|
168
|
+
);
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
log('Saved metadata for user:', userId, metadata);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Issue #2: 에러 발생 시에도 로컬 데이터는 유지됨
|
|
174
|
+
console.error('[HybridFirebaseAdapter] Save failed:', error);
|
|
175
|
+
if (!fallbackToLocal) {
|
|
176
|
+
throw error; // 에러를 상위로 전파
|
|
177
|
+
}
|
|
178
|
+
// fallbackToLocal이 true면 조용히 실패 (로컬 데이터는 이미 저장됨)
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
loadState: async (userId) => {
|
|
183
|
+
try {
|
|
184
|
+
// Issue #1 해결: 로컬 데이터 먼저 확인
|
|
185
|
+
const storageKey = `_nago_chat_default_${userId}`; // scenarioId는 useChat에서 관리
|
|
186
|
+
const localData = typeof window !== 'undefined'
|
|
187
|
+
? localStorage.getItem(storageKey)
|
|
188
|
+
: null;
|
|
189
|
+
const localState: ChatState | null = localData ? JSON.parse(localData) : null;
|
|
190
|
+
|
|
191
|
+
// Firebase에서 메타데이터 확인
|
|
192
|
+
// Firebase 로드 로직 (주석 해제 후 사용)
|
|
193
|
+
/*
|
|
194
|
+
const docRef = doc(db, 'chat_metadata', userId);
|
|
195
|
+
const docSnap = await withTimeout(
|
|
196
|
+
getDoc(docRef),
|
|
197
|
+
timeout,
|
|
198
|
+
'Firebase load timeout'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (!docSnap.exists()) {
|
|
202
|
+
log('No server data found for user:', userId);
|
|
203
|
+
return null; // useChat이 로컬 데이터를 사용
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const serverMeta = docSnap.data() as FirebaseMetadata;
|
|
207
|
+
log('Server metadata found:', serverMeta);
|
|
208
|
+
|
|
209
|
+
// Issue #1: 로컬에 데이터가 없다면 서버에서 전체 데이터 가져오기
|
|
210
|
+
if (!localState) {
|
|
211
|
+
log('No local data, fetching full state from server...');
|
|
212
|
+
const fullDocRef = doc(db, 'chat_full_backup', userId);
|
|
213
|
+
const fullDocSnap = await withTimeout(
|
|
214
|
+
getDoc(fullDocRef),
|
|
215
|
+
timeout,
|
|
216
|
+
'Firebase full state load timeout'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (fullDocSnap.exists()) {
|
|
220
|
+
const fullState = fullDocSnap.data() as FirebaseFullState;
|
|
221
|
+
// Issue #3: Timestamp 정규화
|
|
222
|
+
const normalized = normalizeChatState(fullState);
|
|
223
|
+
log('Restored full state from server');
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
log('No full backup found on server');
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 로컬 데이터가 있다면 서버 메타데이터는 검증용으로만 사용
|
|
232
|
+
// (실제 동기화는 useChat의 loadSavedState에서 처리)
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
return null; // useChat이 로컬/서버 비교를 처리
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// Issue #2: 에러 발생 시 로컬 데이터로 폴백
|
|
238
|
+
console.error('[HybridFirebaseAdapter] Load failed:', error);
|
|
239
|
+
if (fallbackToLocal) {
|
|
240
|
+
log('Falling back to local data only');
|
|
241
|
+
return null; // useChat이 로컬스토리지를 읽음
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// ========================================
|
|
250
|
+
// 완전한 Firebase 동기화 Adapter (고급)
|
|
251
|
+
// ========================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 완전한 Firebase 동기화 Adapter
|
|
255
|
+
*
|
|
256
|
+
* 모든 데이터를 서버에 저장하고 불러옵니다.
|
|
257
|
+
* 비용이 더 들지만 기기 간 완벽한 동기화가 필요할 때 사용하세요.
|
|
258
|
+
*
|
|
259
|
+
* 개선사항:
|
|
260
|
+
* ✅ Issue #2: 에러 핸들링 및 타임아웃
|
|
261
|
+
* ✅ Issue #3: Timestamp 정규화
|
|
262
|
+
* ✅ Issue #4: 비용 최적화 가이드 주석 추가
|
|
263
|
+
*/
|
|
264
|
+
export const createFullFirebaseAdapter = (
|
|
265
|
+
db: any,
|
|
266
|
+
options: AdapterOptions = {}
|
|
267
|
+
): StorageAdapter => {
|
|
268
|
+
const {
|
|
269
|
+
timeout = 5000,
|
|
270
|
+
fallbackToLocal = true,
|
|
271
|
+
debug = false
|
|
272
|
+
} = options;
|
|
273
|
+
|
|
274
|
+
const log = (...args: any[]) => {
|
|
275
|
+
if (debug) console.log('[FullFirebaseAdapter]', ...args);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
saveState: async (userId, state) => {
|
|
280
|
+
try {
|
|
281
|
+
// Issue #4: 비용 최적화 팁
|
|
282
|
+
// - Guest 사용자는 useChat에서 isEnd=true일 때만 호출됨
|
|
283
|
+
// - 회원 사용자도 saveStrategy: 'onEnd'를 사용하면 비용 절감 가능
|
|
284
|
+
|
|
285
|
+
/*
|
|
286
|
+
const docRef = doc(db, 'chat_sessions', userId);
|
|
287
|
+
await withTimeout(
|
|
288
|
+
setDoc(docRef, {
|
|
289
|
+
...state,
|
|
290
|
+
lastSyncedAt: serverTimestamp()
|
|
291
|
+
}),
|
|
292
|
+
timeout,
|
|
293
|
+
'Firebase save timeout'
|
|
294
|
+
);
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
log('Saved full state for user:', userId);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error('[FullFirebaseAdapter] Save failed:', error);
|
|
300
|
+
if (!fallbackToLocal) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
loadState: async (userId) => {
|
|
307
|
+
try {
|
|
308
|
+
/*
|
|
309
|
+
const docRef = doc(db, 'chat_sessions', userId);
|
|
310
|
+
const docSnap = await withTimeout(
|
|
311
|
+
getDoc(docRef),
|
|
312
|
+
timeout,
|
|
313
|
+
'Firebase load timeout'
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (!docSnap.exists()) {
|
|
317
|
+
log('No saved state found for user:', userId);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const rawState = docSnap.data() as FirebaseFullState;
|
|
322
|
+
|
|
323
|
+
// Issue #3: Timestamp 정규화 (중요!)
|
|
324
|
+
const normalizedState = normalizeChatState(rawState);
|
|
325
|
+
|
|
326
|
+
log('Loaded and normalized state for user:', userId);
|
|
327
|
+
return normalizedState;
|
|
328
|
+
*/
|
|
329
|
+
|
|
330
|
+
return null;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error('[FullFirebaseAdapter] Load failed:', error);
|
|
333
|
+
if (fallbackToLocal) {
|
|
334
|
+
log('Falling back to local data');
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// ========================================
|
|
344
|
+
// 로컬 전용 Adapter (테스트/개발용)
|
|
345
|
+
// ========================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 로컬 전용 Adapter
|
|
349
|
+
*
|
|
350
|
+
* 서버 없이 LocalStorage만 사용하는 간단한 구현입니다.
|
|
351
|
+
* 테스트 및 개발 단계에서 사용하기 좋습니다.
|
|
352
|
+
*/
|
|
353
|
+
export const localOnlyAdapter: StorageAdapter = {
|
|
354
|
+
saveState: async (userId, state) => {
|
|
355
|
+
// useChat이 이미 로컬에 저장하므로 아무것도 안 함
|
|
356
|
+
console.log('[LocalAdapter] State saved locally for user:', userId);
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
loadState: async (userId) => {
|
|
360
|
+
// useChat이 로컬스토리지를 직접 읽으므로 null 반환
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// ========================================
|
|
366
|
+
// 사용 예제
|
|
367
|
+
// ========================================
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 사용 예제 1: 하이브리드 어댑터 (권장)
|
|
371
|
+
*
|
|
372
|
+
* import { initializeApp } from 'firebase/app';
|
|
373
|
+
* import { getFirestore } from 'firebase/firestore';
|
|
374
|
+
* import { createHybridFirebaseAdapter } from '@nago730/chatbot-library/examples';
|
|
375
|
+
*
|
|
376
|
+
* const app = initializeApp(firebaseConfig);
|
|
377
|
+
* const db = getFirestore(app);
|
|
378
|
+
*
|
|
379
|
+
* const adapter = createHybridFirebaseAdapter(db, {
|
|
380
|
+
* timeout: 5000,
|
|
381
|
+
* fallbackToLocal: true,
|
|
382
|
+
* debug: process.env.NODE_ENV === 'development'
|
|
383
|
+
* });
|
|
384
|
+
*
|
|
385
|
+
* // useChat에서 사용
|
|
386
|
+
* const chat = useChat(flow, userId, 'start', adapter, {
|
|
387
|
+
* saveStrategy: 'onEnd' // 비용 절감을 위해 종료 시에만 저장
|
|
388
|
+
* });
|
|
389
|
+
*/
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 사용 예제 2: 전체 동기화 어댑터
|
|
393
|
+
*
|
|
394
|
+
* const adapter = createFullFirebaseAdapter(db, {
|
|
395
|
+
* timeout: 10000, // 전체 데이터는 더 긴 타임아웃
|
|
396
|
+
* fallbackToLocal: true
|
|
397
|
+
* });
|
|
398
|
+
*
|
|
399
|
+
* const chat = useChat(flow, userId, 'start', adapter, {
|
|
400
|
+
* saveStrategy: 'always' // 매번 저장 (완벽한 동기화)
|
|
401
|
+
* });
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 비용 최적화 전략 가이드:
|
|
406
|
+
*
|
|
407
|
+
* 1. Guest 사용자 (비로그인)
|
|
408
|
+
* - 하이브리드 어댑터 사용
|
|
409
|
+
* - useChat은 자동으로 isEnd=true일 때만 서버에 저장
|
|
410
|
+
* - 로컬스토리지가 주 저장소, 서버는 백업용
|
|
411
|
+
*
|
|
412
|
+
* 2. 회원 사용자 (로그인)
|
|
413
|
+
* - saveStrategy: 'onEnd' 사용 권장
|
|
414
|
+
* - 중요한 체크포인트(isEnd=true)에서만 서버 저장
|
|
415
|
+
* - 비용 절감 + 충분한 데이터 보호
|
|
416
|
+
*
|
|
417
|
+
* 3. 프리미엄/엔터프라이즈
|
|
418
|
+
* - saveStrategy: 'always' 사용
|
|
419
|
+
* - 완전한 Firebase 어댑터
|
|
420
|
+
* - 모든 입력마다 실시간 동기화 (비용 높음)
|
|
421
|
+
*/
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ChatNode {
|
|
2
|
+
id: string;
|
|
3
|
+
question: string;
|
|
4
|
+
type?: 'button' | 'input';
|
|
5
|
+
options?: string[];
|
|
6
|
+
next: string | ((answer: any) => string);
|
|
7
|
+
isEnd?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ChatMessage {
|
|
11
|
+
nodeId: string;
|
|
12
|
+
question: string;
|
|
13
|
+
answer: any;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChatOptions {
|
|
18
|
+
saveStrategy?: 'always' | 'onEnd';
|
|
19
|
+
scenarioId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* 세션 ID 설정
|
|
22
|
+
* - 'auto': 마지막 세션 복구 또는 새 세션 생성 (기본값)
|
|
23
|
+
* - 'new': 항상 새로운 세션 생성
|
|
24
|
+
* - string: 특정 세션 ID로 복구 또는 생성
|
|
25
|
+
*/
|
|
26
|
+
sessionId?: 'auto' | 'new' | string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChatState {
|
|
30
|
+
answers: Record<string, any>;
|
|
31
|
+
currentStep: string;
|
|
32
|
+
messages: ChatMessage[];
|
|
33
|
+
flowHash: string;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StorageAdapter {
|
|
38
|
+
saveState: (userId: string, state: ChatState) => Promise<void>;
|
|
39
|
+
loadState: (userId: string) => Promise<ChatState | null>;
|
|
40
|
+
}
|