@operato/board 10.0.0-beta.4 → 10.0.0-beta.41

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/dist/src/component/container.js +1 -3
  3. package/dist/src/component/container.js.map +1 -1
  4. package/dist/src/component/etc.js +2 -10
  5. package/dist/src/component/etc.js.map +1 -1
  6. package/dist/src/component/line.js +4 -28
  7. package/dist/src/component/line.js.map +1 -1
  8. package/dist/src/component/shape.js +5 -29
  9. package/dist/src/component/shape.js.map +1 -1
  10. package/dist/src/component/text-and-media.js +2 -25
  11. package/dist/src/component/text-and-media.js.map +1 -1
  12. package/dist/src/data-storage/board-model-cache.d.ts +30 -0
  13. package/dist/src/data-storage/board-model-cache.js +93 -0
  14. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  15. package/dist/src/graphql/playback-buffer.d.ts +79 -0
  16. package/dist/src/graphql/playback-buffer.js +139 -0
  17. package/dist/src/graphql/playback-buffer.js.map +1 -0
  18. package/dist/src/graphql/playback-buffer.test.d.ts +1 -0
  19. package/dist/src/graphql/playback-buffer.test.js +261 -0
  20. package/dist/src/graphql/playback-buffer.test.js.map +1 -0
  21. package/dist/src/graphql/playback-subscription.d.ts +89 -0
  22. package/dist/src/graphql/playback-subscription.js +258 -0
  23. package/dist/src/graphql/playback-subscription.js.map +1 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/index.js +1 -0
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/modeller/edit-toolbar-style.js +38 -1
  28. package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
  29. package/dist/src/modeller/edit-toolbar.d.ts +8 -16
  30. package/dist/src/modeller/edit-toolbar.js +204 -199
  31. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  32. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  33. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +7 -11
  34. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  35. package/dist/src/ox-board-modeller.d.ts +8 -1
  36. package/dist/src/ox-board-modeller.js +125 -6
  37. package/dist/src/ox-board-modeller.js.map +1 -1
  38. package/dist/src/ox-board-viewer.d.ts +50 -1
  39. package/dist/src/ox-board-viewer.js +271 -28
  40. package/dist/src/ox-board-viewer.js.map +1 -1
  41. package/dist/src/ox-playback-controls.d.ts +56 -0
  42. package/dist/src/ox-playback-controls.js +515 -0
  43. package/dist/src/ox-playback-controls.js.map +1 -0
  44. package/dist/src/selector/ox-board-selector.js +11 -1
  45. package/dist/src/selector/ox-board-selector.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +13 -12
@@ -35,16 +35,8 @@ export const textAndMedia = {
35
35
  width: 200,
36
36
  height: 50,
37
37
  text: 'Text',
38
- fillStyle: '#fff',
39
- strokeStyle: '#000',
40
- alpha: 1,
41
- hidden: false,
42
- lineWidth: 5,
43
- lineDash: 'solid',
44
- lineCap: 'butt',
45
38
  textAlign: 'left',
46
39
  textBaseline: 'top',
47
- textWrap: false,
48
40
  fontFamily: 'serif',
49
41
  fontSize: 30
50
42
  }
@@ -59,15 +51,7 @@ export const textAndMedia = {
59
51
  left: 100,
60
52
  top: 100,
61
53
  width: 100,
62
- height: 100,
63
- isGray: false,
64
- fillStyle: '#fff',
65
- strokeStyle: '#000',
66
- alpha: 1,
67
- hidden: false,
68
- lineWidth: 1,
69
- lineDash: 'solid',
70
- lineCap: 'butt'
54
+ height: 100
71
55
  }
72
56
  },
73
57
  {
@@ -81,14 +65,7 @@ export const textAndMedia = {
81
65
  top: 100,
82
66
  width: 100,
83
67
  height: 100,
84
- isGray: true,
85
- fillStyle: '#fff',
86
- strokeStyle: '#000',
87
- alpha: 1,
88
- hidden: false,
89
- lineWidth: 1,
90
- lineDash: 'solid',
91
- lineCap: 'butt'
68
+ isGray: true
92
69
  }
93
70
  },
94
71
  {
@@ -1 +1 @@
1
- {"version":3,"file":"text-and-media.js","sourceRoot":"","sources":["../../../src/component/text-and-media.ts"],"names":[],"mappings":"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","sourcesContent":["import { ComponentGroup } from '../types.js'\n\nconst audio = new URL('../../../icons/components/audio.png', import.meta.url).href\nconst colorImage = new URL('../../../icons/components/color-image.png', import.meta.url).href\nconst gifImage = new URL('../../../icons/components/gif-image.png', import.meta.url).href\nconst grayImage = new URL('../../../icons/components/gray-image.png', import.meta.url).href\nconst text = new URL('../../../icons/components/text.png', import.meta.url).href\n\nconst icon = `\n<svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" viewBox=\"0 0 30 30\" style=\"enable-background:new 0 0 30 30;\" xml:space=\"preserve\">\n <style type=\"text/css\">\n\t .st0{fill:none;stroke:{{strokeColor}};stroke-width:2;stroke-miterlimit:10;}\n\t .st1{fill:{{strokeColor}};}\n </style>\n <g>\n\t <polyline class=\"st0\" points=\"20.6,12.8 20.6,5.1 1.6,5.1 1.6,18 15,18\"/>\n\t <path class=\"st1\" d=\"M5.7,7.9c-0.9,0-1.6,0.7-1.6,1.6s0.7,1.6,1.6,1.6s1.6-0.7,1.6-1.6S6.6,7.9,5.7,7.9z\"/>\n\t <polygon class=\"st1\" points=\"12.8,8.6 8.5,14.7 5.9,12.7 3.2,16.1 15.1,16.1 15.2,13.7\"/>\n\t <path class=\"st1\" d=\"M24.1,15.1h2.1l0.6,2.8h1.7l-0.1-3.8H16.5l-0.1,3.8h1.7l0.4-2.8h2.1c0.1,2,0.1,3.1,0.1,5.2V21\n\t\t c0,1.6,0,1.9,0,2.7l-1.8,0.2V25h6.9v-1.1L24,23.7c0-0.9,0-1.1,0-2.7v-0.7C24,18.1,24,17,24.1,15.1z\"/>\n </g>\n</svg>\n`\n\nexport const textAndMedia: ComponentGroup = {\n name: 'textAndMedia',\n description: 'a group of text and various media components',\n icon,\n templates: [\n {\n type: 'text',\n description: 'text',\n icon: text,\n group: 'textAndMedia',\n model: {\n type: 'text',\n left: 100,\n top: 100,\n width: 200,\n height: 50,\n text: 'Text',\n fillStyle: '#fff',\n strokeStyle: '#000',\n alpha: 1,\n hidden: false,\n lineWidth: 5,\n lineDash: 'solid',\n lineCap: 'butt',\n textAlign: 'left',\n textBaseline: 'top',\n textWrap: false,\n fontFamily: 'serif',\n fontSize: 30\n }\n },\n {\n type: 'color image',\n description: 'color image',\n icon: colorImage,\n group: 'textAndMedia',\n model: {\n type: 'image-view',\n left: 100,\n top: 100,\n width: 100,\n height: 100,\n isGray: false,\n fillStyle: '#fff',\n strokeStyle: '#000',\n alpha: 1,\n hidden: false,\n lineWidth: 1,\n lineDash: 'solid',\n lineCap: 'butt'\n }\n },\n {\n type: 'gray image',\n description: 'gray image',\n icon: grayImage,\n group: 'textAndMedia',\n model: {\n type: 'image-view',\n left: 100,\n top: 100,\n width: 100,\n height: 100,\n isGray: true,\n fillStyle: '#fff',\n strokeStyle: '#000',\n alpha: 1,\n hidden: false,\n lineWidth: 1,\n lineDash: 'solid',\n lineCap: 'butt'\n }\n },\n {\n type: 'gif image',\n description: 'gif image',\n icon: gifImage,\n group: 'textAndMedia',\n model: {\n type: 'gif-view',\n left: 100,\n top: 100,\n width: 100,\n height: 100\n }\n },\n {\n type: 'audio',\n description: 'audio',\n icon: audio,\n group: 'textAndMedia',\n model: {\n type: 'audio',\n left: 100,\n top: 100,\n width: 100,\n height: 100\n }\n }\n ]\n}\n"]}
1
+ {"version":3,"file":"text-and-media.js","sourceRoot":"","sources":["../../../src/component/text-and-media.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,qCAAqC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAClF,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,2CAA2C,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAC7F,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,yCAAyC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AACzF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,0CAA0C,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAC3F,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,oCAAoC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAEhF,MAAM,IAAI,GAAG;;;;;;;;;;;;;;CAcZ,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAmB;IAC1C,IAAI,EAAE,cAAc;IACpB,WAAW,EAAE,8CAA8C;IAC3D,IAAI;IACJ,SAAS,EAAE;QACT;YACE,IAAI,EAAE,MAAM;YACZ,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,cAAc;YACrB,KAAK,EAAE;gBACL,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,GAAG;gBACT,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,EAAE;gBACV,IAAI,EAAE,MAAM;gBACZ,SAAS,EAAE,MAAM;gBACjB,YAAY,EAAE,KAAK;gBACnB,UAAU,EAAE,OAAO;gBACnB,QAAQ,EAAE,EAAE;aACb;SACF;QACD;YACE,IAAI,EAAE,aAAa;YACnB,WAAW,EAAE,aAAa;YAC1B,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,cAAc;YACrB,KAAK,EAAE;gBACL,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,GAAG;gBACT,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,GAAG;aACZ;SACF;QACD;YACE,IAAI,EAAE,YAAY;YAClB,WAAW,EAAE,YAAY;YACzB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,cAAc;YACrB,KAAK,EAAE;gBACL,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,GAAG;gBACT,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,GAAG;gBACX,MAAM,EAAE,IAAI;aACb;SACF;QACD;YACE,IAAI,EAAE,WAAW;YACjB,WAAW,EAAE,WAAW;YACxB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,cAAc;YACrB,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,GAAG;gBACT,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,GAAG;aACZ;SACF;QACD;YACE,IAAI,EAAE,OAAO;YACb,WAAW,EAAE,OAAO;YACpB,IAAI,EAAE,KAAK;YACX,KAAK,EAAE,cAAc;YACrB,KAAK,EAAE;gBACL,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,GAAG;gBACT,GAAG,EAAE,GAAG;gBACR,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,GAAG;aACZ;SACF;KACF;CACF,CAAA","sourcesContent":["import { ComponentGroup } from '../types.js'\n\nconst audio = new URL('../../../icons/components/audio.png', import.meta.url).href\nconst colorImage = new URL('../../../icons/components/color-image.png', import.meta.url).href\nconst gifImage = new URL('../../../icons/components/gif-image.png', import.meta.url).href\nconst grayImage = new URL('../../../icons/components/gray-image.png', import.meta.url).href\nconst text = new URL('../../../icons/components/text.png', import.meta.url).href\n\nconst icon = `\n<svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" viewBox=\"0 0 30 30\" style=\"enable-background:new 0 0 30 30;\" xml:space=\"preserve\">\n <style type=\"text/css\">\n\t .st0{fill:none;stroke:{{strokeColor}};stroke-width:2;stroke-miterlimit:10;}\n\t .st1{fill:{{strokeColor}};}\n </style>\n <g>\n\t <polyline class=\"st0\" points=\"20.6,12.8 20.6,5.1 1.6,5.1 1.6,18 15,18\"/>\n\t <path class=\"st1\" d=\"M5.7,7.9c-0.9,0-1.6,0.7-1.6,1.6s0.7,1.6,1.6,1.6s1.6-0.7,1.6-1.6S6.6,7.9,5.7,7.9z\"/>\n\t <polygon class=\"st1\" points=\"12.8,8.6 8.5,14.7 5.9,12.7 3.2,16.1 15.1,16.1 15.2,13.7\"/>\n\t <path class=\"st1\" d=\"M24.1,15.1h2.1l0.6,2.8h1.7l-0.1-3.8H16.5l-0.1,3.8h1.7l0.4-2.8h2.1c0.1,2,0.1,3.1,0.1,5.2V21\n\t\t c0,1.6,0,1.9,0,2.7l-1.8,0.2V25h6.9v-1.1L24,23.7c0-0.9,0-1.1,0-2.7v-0.7C24,18.1,24,17,24.1,15.1z\"/>\n </g>\n</svg>\n`\n\nexport const textAndMedia: ComponentGroup = {\n name: 'textAndMedia',\n description: 'a group of text and various media components',\n icon,\n templates: [\n {\n type: 'text',\n description: 'text',\n icon: text,\n group: 'textAndMedia',\n model: {\n type: 'text',\n left: 100,\n top: 100,\n width: 200,\n height: 50,\n text: 'Text',\n textAlign: 'left',\n textBaseline: 'top',\n fontFamily: 'serif',\n fontSize: 30\n }\n },\n {\n type: 'color image',\n description: 'color image',\n icon: colorImage,\n group: 'textAndMedia',\n model: {\n type: 'image-view',\n left: 100,\n top: 100,\n width: 100,\n height: 100\n }\n },\n {\n type: 'gray image',\n description: 'gray image',\n icon: grayImage,\n group: 'textAndMedia',\n model: {\n type: 'image-view',\n left: 100,\n top: 100,\n width: 100,\n height: 100,\n isGray: true\n }\n },\n {\n type: 'gif image',\n description: 'gif image',\n icon: gifImage,\n group: 'textAndMedia',\n model: {\n type: 'gif-view',\n left: 100,\n top: 100,\n width: 100,\n height: 100\n }\n },\n {\n type: 'audio',\n description: 'audio',\n icon: audio,\n group: 'textAndMedia',\n model: {\n type: 'audio',\n left: 100,\n top: 100,\n width: 100,\n height: 100\n }\n }\n ]\n}\n"]}
@@ -0,0 +1,30 @@
1
+ export interface CachedBoard {
2
+ id: string;
3
+ name: string;
4
+ model: any;
5
+ updatedAt: string;
6
+ }
7
+ export declare class BoardModelCache {
8
+ /**
9
+ * 캐시에서 보드 모델 조회
10
+ * @returns 캐시된 보드 또는 null
11
+ */
12
+ static get(boardId: string): Promise<CachedBoard | null>;
13
+ /**
14
+ * 보드 모델을 캐시에 저장
15
+ */
16
+ static put(boardId: string, name: string, model: string, updatedAt: string): Promise<void>;
17
+ /**
18
+ * 캐시가 최신인지 확인
19
+ * @returns true면 캐시 유효, false면 갱신 필요
20
+ */
21
+ static isValid(boardId: string, serverUpdatedAt: string): Promise<boolean>;
22
+ /**
23
+ * 특정 보드 캐시 삭제
24
+ */
25
+ static remove(boardId: string): Promise<void>;
26
+ /**
27
+ * 전체 캐시 클리어
28
+ */
29
+ static clear(): Promise<void>;
30
+ }
@@ -0,0 +1,93 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * BoardModelCache — 보드 모델 JSON을 IndexedDB에 캐싱.
5
+ *
6
+ * 캐시 히트 시 네트워크 대기 없이 즉시 scene 생성 가능.
7
+ * updatedAt 비교로 캐시 무효화.
8
+ */
9
+ import Dexie from 'dexie';
10
+ class BoardModelDatabase extends Dexie {
11
+ constructor() {
12
+ super('board-model-cache');
13
+ this.version(1).stores({
14
+ board_models: 'id'
15
+ });
16
+ }
17
+ }
18
+ const db = new BoardModelDatabase();
19
+ export class BoardModelCache {
20
+ /**
21
+ * 캐시에서 보드 모델 조회
22
+ * @returns 캐시된 보드 또는 null
23
+ */
24
+ static async get(boardId) {
25
+ try {
26
+ const record = await db.board_models.get(boardId);
27
+ if (!record)
28
+ return null;
29
+ return {
30
+ id: record.id,
31
+ name: record.name,
32
+ model: JSON.parse(record.model),
33
+ updatedAt: record.updatedAt
34
+ };
35
+ }
36
+ catch (_a) {
37
+ return null;
38
+ }
39
+ }
40
+ /**
41
+ * 보드 모델을 캐시에 저장
42
+ */
43
+ static async put(boardId, name, model, updatedAt) {
44
+ try {
45
+ await db.board_models.put({
46
+ id: boardId,
47
+ name,
48
+ model,
49
+ updatedAt,
50
+ cachedAt: Date.now()
51
+ });
52
+ }
53
+ catch (_a) {
54
+ // 캐시 저장 실패는 무시 — 기능에 영향 없음
55
+ }
56
+ }
57
+ /**
58
+ * 캐시가 최신인지 확인
59
+ * @returns true면 캐시 유효, false면 갱신 필요
60
+ */
61
+ static async isValid(boardId, serverUpdatedAt) {
62
+ try {
63
+ const record = await db.board_models.get(boardId);
64
+ return (record === null || record === void 0 ? void 0 : record.updatedAt) === serverUpdatedAt;
65
+ }
66
+ catch (_a) {
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * 특정 보드 캐시 삭제
72
+ */
73
+ static async remove(boardId) {
74
+ try {
75
+ await db.board_models.delete(boardId);
76
+ }
77
+ catch (_a) {
78
+ // ignore
79
+ }
80
+ }
81
+ /**
82
+ * 전체 캐시 클리어
83
+ */
84
+ static async clear() {
85
+ try {
86
+ await db.board_models.clear();
87
+ }
88
+ catch (_a) {
89
+ // ignore
90
+ }
91
+ }
92
+ }
93
+ //# sourceMappingURL=board-model-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-model-cache.js","sourceRoot":"","sources":["../../../src/data-storage/board-model-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,MAAM,kBAAmB,SAAQ,KAAK;IAGpC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAA;QAC1B,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACrB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAA;IACJ,CAAC;CACF;AAED,MAAM,EAAE,GAAG,IAAI,kBAAkB,EAAE,CAAA;AASnC,MAAM,OAAO,eAAe;IAC1B;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAe;QAC9B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjD,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YAExB,OAAO;gBACL,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC/B,SAAS,EAAE,MAAM,CAAC,SAAS;aAC5B,CAAA;QACH,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,OAAe,EAAE,IAAY,EAAE,KAAa,EAAE,SAAiB;QAC9E,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC;gBACxB,EAAE,EAAE,OAAO;gBACX,IAAI;gBACJ,KAAK;gBACL,SAAS;gBACT,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;aACrB,CAAC,CAAA;QACJ,CAAC;QAAC,WAAM,CAAC;YACP,2BAA2B;QAC7B,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,eAAuB;QAC3D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjD,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,SAAS,MAAK,eAAe,CAAA;QAC9C,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAe;QACjC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACvC,CAAC;QAAC,WAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,KAAK;QAChB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,CAAA;QAC/B,CAAC;QAAC,WAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;CACF","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * BoardModelCache — 보드 모델 JSON을 IndexedDB에 캐싱.\n *\n * 캐시 히트 시 네트워크 대기 없이 즉시 scene 생성 가능.\n * updatedAt 비교로 캐시 무효화.\n */\n\nimport Dexie from 'dexie'\n\ninterface IBoardModelRecord {\n id: string // board ID (primary key)\n name: string\n model: string // JSON string (파싱 전)\n updatedAt: string // 서버의 updatedAt\n cachedAt: number // 로컬 캐시 시점\n}\n\nclass BoardModelDatabase extends Dexie {\n board_models!: Dexie.Table<IBoardModelRecord, string>\n\n constructor() {\n super('board-model-cache')\n this.version(1).stores({\n board_models: 'id'\n })\n }\n}\n\nconst db = new BoardModelDatabase()\n\nexport interface CachedBoard {\n id: string\n name: string\n model: any // 파싱된 모델 객체\n updatedAt: string\n}\n\nexport class BoardModelCache {\n /**\n * 캐시에서 보드 모델 조회\n * @returns 캐시된 보드 또는 null\n */\n static async get(boardId: string): Promise<CachedBoard | null> {\n try {\n const record = await db.board_models.get(boardId)\n if (!record) return null\n\n return {\n id: record.id,\n name: record.name,\n model: JSON.parse(record.model),\n updatedAt: record.updatedAt\n }\n } catch {\n return null\n }\n }\n\n /**\n * 보드 모델을 캐시에 저장\n */\n static async put(boardId: string, name: string, model: string, updatedAt: string): Promise<void> {\n try {\n await db.board_models.put({\n id: boardId,\n name,\n model,\n updatedAt,\n cachedAt: Date.now()\n })\n } catch {\n // 캐시 저장 실패는 무시 — 기능에 영향 없음\n }\n }\n\n /**\n * 캐시가 최신인지 확인\n * @returns true면 캐시 유효, false면 갱신 필요\n */\n static async isValid(boardId: string, serverUpdatedAt: string): Promise<boolean> {\n try {\n const record = await db.board_models.get(boardId)\n return record?.updatedAt === serverUpdatedAt\n } catch {\n return false\n }\n }\n\n /**\n * 특정 보드 캐시 삭제\n */\n static async remove(boardId: string): Promise<void> {\n try {\n await db.board_models.delete(boardId)\n } catch {\n // ignore\n }\n }\n\n /**\n * 전체 캐시 클리어\n */\n static async clear(): Promise<void> {\n try {\n await db.board_models.clear()\n } catch {\n // ignore\n }\n }\n}\n"]}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * PlaybackBuffer — YouTube 스트리밍 방식의 청크 기반 플레이백 버퍼
3
+ *
4
+ * - 10분 단위 청크로 데이터를 fetch
5
+ * - 남은 데이터가 1분 이하이면 다음 청크를 prefetch
6
+ * - seek 시 기존 버퍼 전체 폐기 후 새 위치에서 fresh fetch
7
+ */
8
+ export interface PlaybackSnapshot {
9
+ timestamp: number;
10
+ data: Record<string, any>;
11
+ }
12
+ export interface PlaybackChunk {
13
+ fromTime: number;
14
+ toTime: number;
15
+ snapshots: PlaybackSnapshot[];
16
+ }
17
+ export interface ChunkFetcher {
18
+ (fromTime: Date, toTime: Date): Promise<PlaybackSnapshot[]>;
19
+ }
20
+ declare const CHUNK_DURATION: number;
21
+ declare const PREFETCH_THRESHOLD: number;
22
+ export declare class PlaybackBuffer {
23
+ private _chunks;
24
+ private _fetcher;
25
+ private _prefetching;
26
+ private _totalRange;
27
+ constructor(fetcher: ChunkFetcher, totalRange: {
28
+ from: Date;
29
+ to: Date;
30
+ });
31
+ get totalRange(): {
32
+ from: number;
33
+ to: number;
34
+ };
35
+ /**
36
+ * 버퍼에 로드된 시간 범위들을 반환 (시크바 버퍼 표시용)
37
+ */
38
+ getBufferedRanges(): {
39
+ from: number;
40
+ to: number;
41
+ }[];
42
+ /**
43
+ * 현재 playHead 시점의 스냅샷을 찾는다.
44
+ * playHead 이하인 스냅샷 중 가장 가까운 것을 반환.
45
+ */
46
+ getSnapshotAt(playHead: number): PlaybackSnapshot | null;
47
+ /**
48
+ * playHead와 가장 가까운 다음 스냅샷의 timestamp를 반환.
49
+ * 타이머 간격 계산에 사용.
50
+ */
51
+ getNextSnapshotTime(playHead: number): number | null;
52
+ /**
53
+ * 버퍼의 마지막 시점 (가장 먼 미래)
54
+ */
55
+ getBufferedEnd(): number;
56
+ /**
57
+ * 초기 로드: 지정 시점부터 10분 fetch
58
+ */
59
+ loadInitial(fromTime: number): Promise<void>;
60
+ /**
61
+ * prefetch 필요 여부 확인 + 자동 실행
62
+ * playHead: 현재 재생 위치 (ms)
63
+ */
64
+ checkPrefetch(playHead: number): Promise<void>;
65
+ /**
66
+ * seek: 기존 버퍼 전체 폐기 + 새 위치에서 fresh fetch
67
+ */
68
+ seek(toTime: number): Promise<void>;
69
+ /**
70
+ * 버퍼 전체 폐기
71
+ */
72
+ clear(): void;
73
+ /**
74
+ * 특정 시점이 버퍼에 있는지 확인
75
+ */
76
+ hasDataAt(time: number): boolean;
77
+ private _fetchChunk;
78
+ }
79
+ export { CHUNK_DURATION, PREFETCH_THRESHOLD };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * PlaybackBuffer — YouTube 스트리밍 방식의 청크 기반 플레이백 버퍼
3
+ *
4
+ * - 10분 단위 청크로 데이터를 fetch
5
+ * - 남은 데이터가 1분 이하이면 다음 청크를 prefetch
6
+ * - seek 시 기존 버퍼 전체 폐기 후 새 위치에서 fresh fetch
7
+ */
8
+ const CHUNK_DURATION = 10 * 60 * 1000; // 10분 (ms)
9
+ const PREFETCH_THRESHOLD = 1 * 60 * 1000; // 1분 남았을 때 prefetch
10
+ export class PlaybackBuffer {
11
+ constructor(fetcher, totalRange) {
12
+ this._chunks = [];
13
+ this._prefetching = false;
14
+ this._fetcher = fetcher;
15
+ this._totalRange = { from: totalRange.from.getTime(), to: totalRange.to.getTime() };
16
+ }
17
+ get totalRange() {
18
+ return this._totalRange;
19
+ }
20
+ /**
21
+ * 버퍼에 로드된 시간 범위들을 반환 (시크바 버퍼 표시용)
22
+ */
23
+ getBufferedRanges() {
24
+ return this._chunks.map(c => ({ from: c.fromTime, to: c.toTime }));
25
+ }
26
+ /**
27
+ * 현재 playHead 시점의 스냅샷을 찾는다.
28
+ * playHead 이하인 스냅샷 중 가장 가까운 것을 반환.
29
+ */
30
+ getSnapshotAt(playHead) {
31
+ let best = null;
32
+ for (const chunk of this._chunks) {
33
+ if (chunk.fromTime > playHead)
34
+ break;
35
+ for (const snap of chunk.snapshots) {
36
+ if (snap.timestamp <= playHead) {
37
+ best = snap;
38
+ }
39
+ else {
40
+ break;
41
+ }
42
+ }
43
+ }
44
+ return best;
45
+ }
46
+ /**
47
+ * playHead와 가장 가까운 다음 스냅샷의 timestamp를 반환.
48
+ * 타이머 간격 계산에 사용.
49
+ */
50
+ getNextSnapshotTime(playHead) {
51
+ for (const chunk of this._chunks) {
52
+ for (const snap of chunk.snapshots) {
53
+ if (snap.timestamp > playHead) {
54
+ return snap.timestamp;
55
+ }
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * 버퍼의 마지막 시점 (가장 먼 미래)
62
+ */
63
+ getBufferedEnd() {
64
+ if (this._chunks.length === 0)
65
+ return 0;
66
+ return this._chunks[this._chunks.length - 1].toTime;
67
+ }
68
+ /**
69
+ * 초기 로드: 지정 시점부터 10분 fetch
70
+ */
71
+ async loadInitial(fromTime) {
72
+ this._chunks = [];
73
+ await this._fetchChunk(fromTime);
74
+ }
75
+ /**
76
+ * prefetch 필요 여부 확인 + 자동 실행
77
+ * playHead: 현재 재생 위치 (ms)
78
+ */
79
+ async checkPrefetch(playHead) {
80
+ if (this._prefetching)
81
+ return;
82
+ const bufferedEnd = this.getBufferedEnd();
83
+ const remaining = bufferedEnd - playHead;
84
+ if (remaining <= PREFETCH_THRESHOLD && bufferedEnd < this._totalRange.to) {
85
+ this._prefetching = true;
86
+ try {
87
+ await this._fetchChunk(bufferedEnd);
88
+ }
89
+ finally {
90
+ this._prefetching = false;
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * seek: 기존 버퍼 전체 폐기 + 새 위치에서 fresh fetch
96
+ */
97
+ async seek(toTime) {
98
+ this._chunks = [];
99
+ this._prefetching = false;
100
+ await this._fetchChunk(toTime);
101
+ }
102
+ /**
103
+ * 버퍼 전체 폐기
104
+ */
105
+ clear() {
106
+ this._chunks = [];
107
+ this._prefetching = false;
108
+ }
109
+ /**
110
+ * 특정 시점이 버퍼에 있는지 확인
111
+ */
112
+ hasDataAt(time) {
113
+ return this._chunks.some(c => c.fromTime <= time && time < c.toTime);
114
+ }
115
+ async _fetchChunk(fromTime) {
116
+ const toTime = Math.min(fromTime + CHUNK_DURATION, this._totalRange.to);
117
+ if (fromTime >= toTime)
118
+ return;
119
+ const snapshots = await this._fetcher(new Date(fromTime), new Date(toTime));
120
+ // 중복 청크 방지
121
+ if (this._chunks.some(c => c.fromTime === fromTime))
122
+ return;
123
+ const chunk = {
124
+ fromTime,
125
+ toTime,
126
+ snapshots: snapshots.sort((a, b) => a.timestamp - b.timestamp)
127
+ };
128
+ // 시간순 삽입
129
+ const insertIdx = this._chunks.findIndex(c => c.fromTime > fromTime);
130
+ if (insertIdx === -1) {
131
+ this._chunks.push(chunk);
132
+ }
133
+ else {
134
+ this._chunks.splice(insertIdx, 0, chunk);
135
+ }
136
+ }
137
+ }
138
+ export { CHUNK_DURATION, PREFETCH_THRESHOLD };
139
+ //# sourceMappingURL=playback-buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-buffer.js","sourceRoot":"","sources":["../../../src/graphql/playback-buffer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PlaybackBuffer — YouTube 스트리밍 방식의 청크 기반 플레이백 버퍼\n *\n * - 10분 단위 청크로 데이터를 fetch\n * - 남은 데이터가 1분 이하이면 다음 청크를 prefetch\n * - seek 시 기존 버퍼 전체 폐기 후 새 위치에서 fresh fetch\n */\n\nexport interface PlaybackSnapshot {\n timestamp: number // ms\n data: Record<string, any> // { tag: value }\n}\n\nexport interface PlaybackChunk {\n fromTime: number // ms\n toTime: number // ms\n snapshots: PlaybackSnapshot[]\n}\n\nexport interface ChunkFetcher {\n (fromTime: Date, toTime: Date): Promise<PlaybackSnapshot[]>\n}\n\nconst CHUNK_DURATION = 10 * 60 * 1000 // 10분 (ms)\nconst PREFETCH_THRESHOLD = 1 * 60 * 1000 // 1분 남았을 때 prefetch\n\nexport class PlaybackBuffer {\n private _chunks: PlaybackChunk[] = []\n private _fetcher: ChunkFetcher\n private _prefetching: boolean = false\n private _totalRange: { from: number; to: number } // 전체 플레이백 범위\n\n constructor(fetcher: ChunkFetcher, totalRange: { from: Date; to: Date }) {\n this._fetcher = fetcher\n this._totalRange = { from: totalRange.from.getTime(), to: totalRange.to.getTime() }\n }\n\n get totalRange() {\n return this._totalRange\n }\n\n /**\n * 버퍼에 로드된 시간 범위들을 반환 (시크바 버퍼 표시용)\n */\n getBufferedRanges(): { from: number; to: number }[] {\n return this._chunks.map(c => ({ from: c.fromTime, to: c.toTime }))\n }\n\n /**\n * 현재 playHead 시점의 스냅샷을 찾는다.\n * playHead 이하인 스냅샷 중 가장 가까운 것을 반환.\n */\n getSnapshotAt(playHead: number): PlaybackSnapshot | null {\n let best: PlaybackSnapshot | null = null\n\n for (const chunk of this._chunks) {\n if (chunk.fromTime > playHead) break\n\n for (const snap of chunk.snapshots) {\n if (snap.timestamp <= playHead) {\n best = snap\n } else {\n break\n }\n }\n }\n\n return best\n }\n\n /**\n * playHead와 가장 가까운 다음 스냅샷의 timestamp를 반환.\n * 타이머 간격 계산에 사용.\n */\n getNextSnapshotTime(playHead: number): number | null {\n for (const chunk of this._chunks) {\n for (const snap of chunk.snapshots) {\n if (snap.timestamp > playHead) {\n return snap.timestamp\n }\n }\n }\n return null\n }\n\n /**\n * 버퍼의 마지막 시점 (가장 먼 미래)\n */\n getBufferedEnd(): number {\n if (this._chunks.length === 0) return 0\n return this._chunks[this._chunks.length - 1].toTime\n }\n\n /**\n * 초기 로드: 지정 시점부터 10분 fetch\n */\n async loadInitial(fromTime: number): Promise<void> {\n this._chunks = []\n await this._fetchChunk(fromTime)\n }\n\n /**\n * prefetch 필요 여부 확인 + 자동 실행\n * playHead: 현재 재생 위치 (ms)\n */\n async checkPrefetch(playHead: number): Promise<void> {\n if (this._prefetching) return\n\n const bufferedEnd = this.getBufferedEnd()\n const remaining = bufferedEnd - playHead\n\n if (remaining <= PREFETCH_THRESHOLD && bufferedEnd < this._totalRange.to) {\n this._prefetching = true\n try {\n await this._fetchChunk(bufferedEnd)\n } finally {\n this._prefetching = false\n }\n }\n }\n\n /**\n * seek: 기존 버퍼 전체 폐기 + 새 위치에서 fresh fetch\n */\n async seek(toTime: number): Promise<void> {\n this._chunks = []\n this._prefetching = false\n await this._fetchChunk(toTime)\n }\n\n /**\n * 버퍼 전체 폐기\n */\n clear(): void {\n this._chunks = []\n this._prefetching = false\n }\n\n /**\n * 특정 시점이 버퍼에 있는지 확인\n */\n hasDataAt(time: number): boolean {\n return this._chunks.some(c => c.fromTime <= time && time < c.toTime)\n }\n\n private async _fetchChunk(fromTime: number): Promise<void> {\n const toTime = Math.min(fromTime + CHUNK_DURATION, this._totalRange.to)\n if (fromTime >= toTime) return\n\n const snapshots = await this._fetcher(new Date(fromTime), new Date(toTime))\n\n // 중복 청크 방지\n if (this._chunks.some(c => c.fromTime === fromTime)) return\n\n const chunk: PlaybackChunk = {\n fromTime,\n toTime,\n snapshots: snapshots.sort((a, b) => a.timestamp - b.timestamp)\n }\n\n // 시간순 삽입\n const insertIdx = this._chunks.findIndex(c => c.fromTime > fromTime)\n if (insertIdx === -1) {\n this._chunks.push(chunk)\n } else {\n this._chunks.splice(insertIdx, 0, chunk)\n }\n }\n}\n\nexport { CHUNK_DURATION, PREFETCH_THRESHOLD }\n"]}
@@ -0,0 +1 @@
1
+ export {};