@things-factory/board-ui 10.0.0-beta.6 → 10.0.0-beta.64

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 (81) hide show
  1. package/dist-client/apptools/favorite-tool.js +5 -5
  2. package/dist-client/apptools/favorite-tool.js.map +1 -1
  3. package/dist-client/board-list/board-tile-list.d.ts +6 -1
  4. package/dist-client/board-list/board-tile-list.js +291 -44
  5. package/dist-client/board-list/board-tile-list.js.map +1 -1
  6. package/dist-client/board-list/group-bar.js +3 -3
  7. package/dist-client/board-list/group-bar.js.map +1 -1
  8. package/dist-client/board-list/play-group-bar.d.ts +0 -1
  9. package/dist-client/board-list/play-group-bar.js +3 -6
  10. package/dist-client/board-list/play-group-bar.js.map +1 -1
  11. package/dist-client/board-provider.js +20 -8
  12. package/dist-client/board-provider.js.map +1 -1
  13. package/dist-client/data-grist/board-editor.js +4 -4
  14. package/dist-client/data-grist/board-editor.js.map +1 -1
  15. package/dist-client/data-grist/board-renderer.js +4 -4
  16. package/dist-client/data-grist/board-renderer.js.map +1 -1
  17. package/dist-client/graphql/board-template.js +1 -1
  18. package/dist-client/graphql/board-template.js.map +1 -1
  19. package/dist-client/graphql/board.d.ts +1 -0
  20. package/dist-client/graphql/board.js +28 -2
  21. package/dist-client/graphql/board.js.map +1 -1
  22. package/dist-client/graphql/group.js +1 -1
  23. package/dist-client/graphql/group.js.map +1 -1
  24. package/dist-client/graphql/play-group.js +3 -3
  25. package/dist-client/graphql/play-group.js.map +1 -1
  26. package/dist-client/pages/attachment-list-page.d.ts +16 -0
  27. package/dist-client/pages/attachment-list-page.js +63 -2
  28. package/dist-client/pages/attachment-list-page.js.map +1 -1
  29. package/dist-client/pages/board-list-page.d.ts +23 -3
  30. package/dist-client/pages/board-list-page.js +165 -77
  31. package/dist-client/pages/board-list-page.js.map +1 -1
  32. package/dist-client/pages/board-modeller-page.d.ts +122 -0
  33. package/dist-client/pages/board-modeller-page.js +705 -54
  34. package/dist-client/pages/board-modeller-page.js.map +1 -1
  35. package/dist-client/pages/board-player-by-name-page.js.map +1 -1
  36. package/dist-client/pages/board-player-page.js +14 -26
  37. package/dist-client/pages/board-player-page.js.map +1 -1
  38. package/dist-client/pages/board-viewer-by-name-page.d.ts +8 -1
  39. package/dist-client/pages/board-viewer-by-name-page.js +9 -1
  40. package/dist-client/pages/board-viewer-by-name-page.js.map +1 -1
  41. package/dist-client/pages/board-viewer-page.d.ts +2 -1
  42. package/dist-client/pages/board-viewer-page.js +52 -48
  43. package/dist-client/pages/board-viewer-page.js.map +1 -1
  44. package/dist-client/pages/play-list-page.d.ts +0 -1
  45. package/dist-client/pages/play-list-page.js +26 -33
  46. package/dist-client/pages/play-list-page.js.map +1 -1
  47. package/dist-client/pages/printable-board-viewer-page.js +2 -2
  48. package/dist-client/pages/printable-board-viewer-page.js.map +1 -1
  49. package/dist-client/route.d.ts +1 -1
  50. package/dist-client/route.js.map +1 -1
  51. package/dist-client/setting-let/board-view-setting-let.js +1 -1
  52. package/dist-client/setting-let/board-view-setting-let.js.map +1 -1
  53. package/dist-client/tsconfig.tsbuildinfo +1 -1
  54. package/dist-client/utils/notify-helper.d.ts +7 -0
  55. package/dist-client/utils/notify-helper.js +28 -0
  56. package/dist-client/utils/notify-helper.js.map +1 -0
  57. package/dist-client/utils/query-utils.d.ts +1 -0
  58. package/dist-client/utils/query-utils.js +20 -0
  59. package/dist-client/utils/query-utils.js.map +1 -0
  60. package/dist-client/viewparts/board-basic-info.js +9 -13
  61. package/dist-client/viewparts/board-basic-info.js.map +1 -1
  62. package/dist-client/viewparts/board-template-info.d.ts +0 -1
  63. package/dist-client/viewparts/board-template-info.js +5 -13
  64. package/dist-client/viewparts/board-template-info.js.map +1 -1
  65. package/dist-client/viewparts/board-versions.js +1 -1
  66. package/dist-client/viewparts/board-versions.js.map +1 -1
  67. package/dist-client/viewparts/group-info-basic.js +2 -2
  68. package/dist-client/viewparts/group-info-basic.js.map +1 -1
  69. package/dist-client/viewparts/group-info-import.js +2 -2
  70. package/dist-client/viewparts/group-info-import.js.map +1 -1
  71. package/dist-client/viewparts/link-builder.js +1 -1
  72. package/dist-client/viewparts/link-builder.js.map +1 -1
  73. package/dist-client/viewparts/play-group-info-basic.js +2 -2
  74. package/dist-client/viewparts/play-group-info-basic.js.map +1 -1
  75. package/dist-server/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +5 -4
  77. package/translations/en.json +3 -29
  78. package/translations/ja.json +3 -29
  79. package/translations/ko.json +3 -29
  80. package/translations/ms.json +3 -29
  81. package/translations/zh.json +3 -29
@@ -5,8 +5,10 @@ import { customElement, query } from 'lit/decorators.js';
5
5
  import gql from 'graphql-tag';
6
6
  import { i18next, localize } from '@operato/i18n';
7
7
  import { PageView } from '@operato/shell';
8
+ import { CommonButtonStyles } from '@operato/styles';
8
9
  import { OxAttachmentList } from '@operato/attachment/ox-attachment-list.js';
9
10
  import { client } from '@operato/graphql';
11
+ import { notify } from '@operato/layout';
10
12
  async function fetchDataFromURL(fileURL) {
11
13
  try {
12
14
  const response = await fetch(fileURL);
@@ -57,7 +59,14 @@ let AttachmentListPage = class AttachmentListPage extends localize(i18next)(Page
57
59
  },
58
60
  importable: {
59
61
  handler: this.importHandler.bind(this)
60
- }
62
+ },
63
+ actions: [
64
+ {
65
+ title: i18next.t('button.backfill thumbnails'),
66
+ action: this.backfillThumbnails.bind(this),
67
+ ...CommonButtonStyles.save
68
+ }
69
+ ]
61
70
  };
62
71
  }
63
72
  render() {
@@ -91,11 +100,63 @@ let AttachmentListPage = class AttachmentListPage extends localize(i18next)(Page
91
100
  });
92
101
  }
93
102
  catch (err) {
94
- console.log(name, err);
103
+ console.warn('Failed to process attachment:', name, err);
95
104
  }
96
105
  }
97
106
  return data;
98
107
  }
108
+ /**
109
+ * 썸네일이 없는 기존 첨부파일들에 대해 서버에서 일괄 썸네일 생성.
110
+ * 한 배치당 20 개씩, remaining 이 0 이 될 때까지 반복 호출.
111
+ */
112
+ async backfillThumbnails() {
113
+ const BATCH = 20;
114
+ let total = { attempted: 0, succeeded: 0, failed: 0 };
115
+ let remaining = Number.POSITIVE_INFINITY;
116
+ notify({ message: i18next.t('text.backfilling thumbnails') });
117
+ while (remaining > 0) {
118
+ try {
119
+ const { data } = await client.mutate({
120
+ mutation: gql `
121
+ mutation BackfillAttachmentThumbnails($limit: Int) {
122
+ backfillAttachmentThumbnails(limit: $limit) {
123
+ attempted
124
+ succeeded
125
+ failed
126
+ remaining
127
+ }
128
+ }
129
+ `,
130
+ variables: { limit: BATCH }
131
+ });
132
+ const r = data?.backfillAttachmentThumbnails;
133
+ if (!r)
134
+ break;
135
+ total.attempted += r.attempted;
136
+ total.succeeded += r.succeeded;
137
+ total.failed += r.failed;
138
+ remaining = r.remaining;
139
+ if (r.attempted === 0)
140
+ break; // 더 이상 처리할 게 없음
141
+ // 한 배치에서 아무것도 성공하지 못했다면 무한 루프 방지를 위해 중단.
142
+ // 동일 후보들이 계속 실패하는 상황이므로 서버 측 원인 해결이 선행되어야 함.
143
+ if (r.succeeded === 0)
144
+ break;
145
+ }
146
+ catch (err) {
147
+ console.error('backfill error:', err);
148
+ notify({ level: 'error', message: String(err) });
149
+ return;
150
+ }
151
+ }
152
+ notify({
153
+ message: i18next.t('text.thumbnail backfill done', {
154
+ succeeded: total.succeeded,
155
+ failed: total.failed
156
+ })
157
+ });
158
+ this.grist.fetch();
159
+ }
99
160
  async importHandler(data, file) {
100
161
  await client.mutate({
101
162
  mutation: gql `
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-list-page.js","sourceRoot":"","sources":["../../client/pages/attachment-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/attachment/ox-attachment-list.js'\n\nimport { css, html } from 'lit'\nimport { customElement, query } from 'lit/decorators.js'\nimport gql from 'graphql-tag'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { PageView } from '@operato/shell'\nimport { OxAttachmentList } from '@operato/attachment/ox-attachment-list.js'\nimport { client } from '@operato/graphql'\n\nasync function fetchDataFromURL(fileURL: string) {\n try {\n const response = await fetch(fileURL)\n\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n\n const blob = await response.blob() // 파일 데이터를 Blob으로 변환\n\n if (blob.size === 0) {\n throw new Error('Content is empty') // Content가 비어 있으면 예외\n }\n\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = (event: any) => {\n const dataURL = event.target.result\n resolve(dataURL)\n }\n\n reader.onerror = (event: any) => {\n reject(event.error)\n }\n\n reader.readAsDataURL(blob) // Blob을 Data URL로 읽기\n })\n } catch (error) {\n throw error\n }\n}\n\n@customElement('attachment-list-page')\nexport class AttachmentListPage extends localize(i18next)(PageView) {\n static styles = [\n css`\n :host {\n display: flex;\n }\n `\n ]\n\n @query('ox-attachment-list') attachmentList!: OxAttachmentList\n\n get context() {\n return {\n title: i18next.t('title.attachment list'),\n search: {\n handler: search => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n board_topmenu: true,\n exportable: {\n name: i18next.t('title.attachment list'),\n data: this.exportHandler.bind(this)\n },\n importable: {\n handler: this.importHandler.bind(this)\n }\n }\n }\n\n render() {\n return html` <ox-attachment-list .creatable=${true} without-search></ox-attachment-list> `\n }\n\n get grist() {\n return this.attachmentList.grist\n }\n\n languageUpdated(i18next) {\n this.attachmentList.requestUpdate()\n }\n\n async pageInitialized() {\n await this.updateComplete\n\n this.attachmentList.refreshAttachments()\n }\n\n async exportHandler() {\n const records = this.grist.data.records\n const data = {} as any\n\n for (const record of records) {\n const { id, name, mimetype, encoding, category, fullpath } = record\n\n try {\n const dataURL = await fetchDataFromURL(fullpath)\n\n id &&\n (data[id!] = {\n id,\n name,\n mimetype,\n encoding,\n category,\n contents: dataURL\n })\n } catch (err) {\n console.log(name, err)\n }\n }\n\n return data\n }\n\n async importHandler(data, file) {\n await client.mutate({\n mutation: gql`\n mutation importAttachments($file: Upload!) {\n importAttachments(file: $file) {\n id\n name\n description\n path\n }\n }\n `,\n variables: {\n file\n },\n context: { hasUpload: true }\n })\n\n this.grist.fetch()\n }\n}\n"]}
1
+ {"version":3,"file":"attachment-list-page.js","sourceRoot":"","sources":["../../client/pages/attachment-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/attachment/ox-attachment-list.js'\n\nimport { css, html } from 'lit'\nimport { customElement, query } from 'lit/decorators.js'\nimport gql from 'graphql-tag'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { PageView } from '@operato/shell'\nimport { CommonButtonStyles } from '@operato/styles'\nimport { OxAttachmentList } from '@operato/attachment/ox-attachment-list.js'\nimport { client } from '@operato/graphql'\nimport { notify } from '@operato/layout'\n\nasync function fetchDataFromURL(fileURL: string) {\n try {\n const response = await fetch(fileURL)\n\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n\n const blob = await response.blob() // 파일 데이터를 Blob으로 변환\n\n if (blob.size === 0) {\n throw new Error('Content is empty') // Content가 비어 있으면 예외\n }\n\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = (event: any) => {\n const dataURL = event.target.result\n resolve(dataURL)\n }\n\n reader.onerror = (event: any) => {\n reject(event.error)\n }\n\n reader.readAsDataURL(blob) // Blob을 Data URL로 읽기\n })\n } catch (error) {\n throw error\n }\n}\n\n@customElement('attachment-list-page')\nexport class AttachmentListPage extends localize(i18next)(PageView) {\n static styles = [\n css`\n :host {\n display: flex;\n }\n `\n ]\n\n @query('ox-attachment-list') attachmentList!: OxAttachmentList\n\n get context() {\n return {\n title: i18next.t('title.attachment list'),\n search: {\n handler: search => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n board_topmenu: true,\n exportable: {\n name: i18next.t('title.attachment list'),\n data: this.exportHandler.bind(this)\n },\n importable: {\n handler: this.importHandler.bind(this)\n },\n actions: [\n {\n title: i18next.t('button.backfill thumbnails'),\n action: this.backfillThumbnails.bind(this),\n ...CommonButtonStyles.save\n }\n ]\n }\n }\n\n render() {\n return html` <ox-attachment-list .creatable=${true} without-search></ox-attachment-list> `\n }\n\n get grist() {\n return this.attachmentList.grist\n }\n\n languageUpdated(i18next) {\n this.attachmentList.requestUpdate()\n }\n\n async pageInitialized() {\n await this.updateComplete\n\n this.attachmentList.refreshAttachments()\n }\n\n async exportHandler() {\n const records = this.grist.data.records\n const data = {} as any\n\n for (const record of records) {\n const { id, name, mimetype, encoding, category, fullpath } = record\n\n try {\n const dataURL = await fetchDataFromURL(fullpath)\n\n id &&\n (data[id!] = {\n id,\n name,\n mimetype,\n encoding,\n category,\n contents: dataURL\n })\n } catch (err) {\n console.warn('Failed to process attachment:', name, err)\n }\n }\n\n return data\n }\n\n /**\n * 썸네일이 없는 기존 첨부파일들에 대해 서버에서 일괄 썸네일 생성.\n * 한 배치당 20 개씩, remaining 이 0 이 될 때까지 반복 호출.\n */\n async backfillThumbnails() {\n const BATCH = 20\n let total = { attempted: 0, succeeded: 0, failed: 0 }\n let remaining = Number.POSITIVE_INFINITY\n\n notify({ message: i18next.t('text.backfilling thumbnails') })\n\n while (remaining > 0) {\n try {\n const { data } = await client.mutate({\n mutation: gql`\n mutation BackfillAttachmentThumbnails($limit: Int) {\n backfillAttachmentThumbnails(limit: $limit) {\n attempted\n succeeded\n failed\n remaining\n }\n }\n `,\n variables: { limit: BATCH }\n })\n const r = data?.backfillAttachmentThumbnails\n if (!r) break\n total.attempted += r.attempted\n total.succeeded += r.succeeded\n total.failed += r.failed\n remaining = r.remaining\n\n if (r.attempted === 0) break // 더 이상 처리할 게 없음\n // 한 배치에서 아무것도 성공하지 못했다면 무한 루프 방지를 위해 중단.\n // 동일 후보들이 계속 실패하는 상황이므로 서버 측 원인 해결이 선행되어야 함.\n if (r.succeeded === 0) break\n } catch (err) {\n console.error('backfill error:', err)\n notify({ level: 'error', message: String(err) })\n return\n }\n }\n\n notify({\n message: i18next.t('text.thumbnail backfill done', {\n succeeded: total.succeeded,\n failed: total.failed\n })\n })\n this.grist.fetch()\n }\n\n async importHandler(data, file) {\n await client.mutate({\n mutation: gql`\n mutation importAttachments($file: Upload!) {\n importAttachments(file: $file) {\n id\n name\n description\n path\n }\n }\n `,\n variables: {\n file\n },\n context: { hasUpload: true }\n })\n\n this.grist.fetch()\n }\n}\n"]}
@@ -16,6 +16,8 @@ export declare class BoardListPage extends BoardListPageBase {
16
16
  private _page;
17
17
  private _total;
18
18
  private page?;
19
+ private _lastFetchTime?;
20
+ private _updatedBoardIds;
19
21
  get context(): {
20
22
  title: {
21
23
  icon: string;
@@ -36,15 +38,30 @@ export declare class BoardListPage extends BoardListPageBase {
36
38
  page?: number | undefined;
37
39
  limit?: any;
38
40
  }): Promise<any>;
41
+ /**
42
+ * favor/mywork 같이 "search text + pagination"만 받는 간단한 리스트 조회 공용 헬퍼.
43
+ * 그룹/타입 필터 등 복합 조건을 쓰는 일반 목록(getBoards)과 구분해 중복 제거.
44
+ */
45
+ private _fetchSimpleBoardList;
39
46
  getFavoriteBoards({ page, limit }?: {
40
47
  page?: number | undefined;
41
48
  limit?: any;
42
- }): Promise<any>;
49
+ }): Promise<any[]>;
43
50
  getMyBoards({ page, limit }?: {
44
51
  page?: number | undefined;
45
52
  limit?: any;
46
- }): Promise<any>;
53
+ }): Promise<any[]>;
47
54
  refreshBoards(): Promise<void>;
55
+ /**
56
+ * 컨텐츠가 스크롤 영역을 채우지 못하면 다음 페이지를 자동 로드한다.
57
+ */
58
+ private _filling;
59
+ private fillViewport;
60
+ /**
61
+ * 기존 목록을 유지하면서 변경된 보드만 갱신한다.
62
+ * 스크롤 위치가 보존된다.
63
+ */
64
+ incrementalRefresh(): Promise<void>;
48
65
  appendBoards(): Promise<void>;
49
66
  scrollAction(): Promise<void>;
50
67
  pageInitialized(): Promise<void>;
@@ -64,6 +81,10 @@ export declare class BoardListPage extends BoardListPageBase {
64
81
  files: any;
65
82
  overwrite: any;
66
83
  }): Promise<void>;
84
+ onReorderBoards({ boardIds, movedId }: {
85
+ boardIds: any;
86
+ movedId: any;
87
+ }): Promise<void>;
67
88
  onCreateBoard(board: any): Promise<void>;
68
89
  onCloneBoard({ id, name, description, targetSubdomain, targetGroupId }: {
69
90
  id: any;
@@ -94,6 +115,5 @@ export declare class BoardListPage extends BoardListPageBase {
94
115
  visibility: any;
95
116
  }): Promise<void>;
96
117
  refreshFavorites(): Promise<void>;
97
- _notify(level: 'info' | 'error' | 'warn', message: any, ex?: any): void;
98
118
  }
99
119
  export {};
@@ -15,7 +15,8 @@ import { ScrollbarStyles } from '@operato/styles';
15
15
  import InfiniteScrollable from '@operato/utils/mixins/infinite-scrollable.js';
16
16
  import { UPDATE_FAVORITES } from '@things-factory/fav-base/client/index.js';
17
17
  import { swipe } from '@things-factory/utils/src/index.js';
18
- import { cloneBoard, releaseBoard, revertBoardVersion, createBoard, createGroup, deleteBoard, deleteGroup, fetchBoardList, fetchFavoriteBoardList, fetchMyBoardList, fetchGroupList, updateBoard, updateGroup, importBoards } from '../graphql';
18
+ import { notify } from '../utils/notify-helper.js';
19
+ import { cloneBoard, releaseBoard, revertBoardVersion, createBoard, createGroup, deleteBoard, deleteGroup, fetchBoardList, fetchBoardsUpdatedSince, fetchFavoriteBoardList, fetchMyBoardList, fetchGroupList, updateBoard, updateGroup, importBoards } from '../graphql';
19
20
  const BoardListPageBase = localize(i18next)(InfiniteScrollable(PageView));
20
21
  let BoardListPage = class BoardListPage extends BoardListPageBase {
21
22
  static { this.styles = [
@@ -42,8 +43,8 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
42
43
  `
43
44
  ]; }
44
45
  get context() {
45
- var groupId = this.groupId;
46
- var group = this.groups && this.groups.find(group => group.id === groupId);
46
+ const groupId = this.groupId;
47
+ const group = this.groups && this.groups.find(group => group.id === groupId);
47
48
  return {
48
49
  title: {
49
50
  icon: 'dashboard',
@@ -59,6 +60,15 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
59
60
  handler: search => {
60
61
  this.searchText = search;
61
62
  this.refreshBoards();
63
+ // URL에 검색어 반영 → 페이지 복귀 시 유지
64
+ const url = new URL(window.location.href);
65
+ if (search) {
66
+ url.searchParams.set('search', search);
67
+ }
68
+ else {
69
+ url.searchParams.delete('search');
70
+ }
71
+ history.replaceState(null, '', url.href);
62
72
  },
63
73
  value: this.searchText || ''
64
74
  },
@@ -74,6 +84,11 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
74
84
  this.searchText = '';
75
85
  this._page = 1;
76
86
  this._total = 0; /* required for infinite-scrolling */
87
+ this._updatedBoardIds = [];
88
+ /**
89
+ * 컨텐츠가 스크롤 영역을 채우지 못하면 다음 페이지를 자동 로드한다.
90
+ */
91
+ this._filling = false;
77
92
  this._infiniteScrollOptions.limit = 50;
78
93
  }
79
94
  render() {
@@ -92,6 +107,7 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
92
107
  .favorites=${this.favorites}
93
108
  .groups=${this.groups}
94
109
  .group=${this.groupId}
110
+ .updatedIds=${this._updatedBoardIds}
95
111
  search-text=${this.searchText}
96
112
  @info-board=${e => this.onInfoBoard(e.detail)}
97
113
  @scroll=${e => {
@@ -100,7 +116,9 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
100
116
  }}
101
117
  @create-board=${e => this.onCreateBoard(e.detail)}
102
118
  @refresh-favorites=${e => this.refreshFavorites()}
119
+ @reordered=${e => this.onReorderBoards(e.detail)}
103
120
  creatable
121
+ reorderable
104
122
  ></board-tile-list>
105
123
  `;
106
124
  }
@@ -149,9 +167,13 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
149
167
  value: 'main'
150
168
  });
151
169
  }
152
- var listParam = {
170
+ const listParam = {
153
171
  filters,
154
172
  sortings: [
173
+ {
174
+ name: 'sortOrder',
175
+ desc: false
176
+ },
155
177
  {
156
178
  name: 'name',
157
179
  desc: false
@@ -164,7 +186,11 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
164
186
  };
165
187
  return (await fetchBoardList(listParam)).boards;
166
188
  }
167
- async getFavoriteBoards({ page = 1, limit = this._infiniteScrollOptions.limit } = {}) {
189
+ /**
190
+ * favor/mywork 같이 "search text + pagination"만 받는 간단한 리스트 조회 공용 헬퍼.
191
+ * 그룹/타입 필터 등 복합 조건을 쓰는 일반 목록(getBoards)과 구분해 중복 제거.
192
+ */
193
+ async _fetchSimpleBoardList(fetcher, resultKey, { page, limit }) {
168
194
  const filters = [];
169
195
  if (this.searchText) {
170
196
  filters.push({
@@ -173,55 +199,88 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
173
199
  value: '%' + this.searchText + '%'
174
200
  });
175
201
  }
176
- var listParam = {
177
- filters,
178
- pagination: {
179
- page,
180
- limit
181
- }
182
- };
183
- return (await fetchFavoriteBoardList(listParam)).favoriteBoards;
202
+ const result = await fetcher({ filters, pagination: { page, limit } });
203
+ return result[resultKey];
204
+ }
205
+ async getFavoriteBoards({ page = 1, limit = this._infiniteScrollOptions.limit } = {}) {
206
+ return this._fetchSimpleBoardList(fetchFavoriteBoardList, 'favoriteBoards', { page, limit });
184
207
  }
185
208
  async getMyBoards({ page = 1, limit = this._infiniteScrollOptions.limit } = {}) {
186
- const filters = [];
187
- if (this.searchText) {
188
- filters.push({
189
- name: 'name',
190
- operator: 'search',
191
- value: '%' + this.searchText + '%'
192
- });
193
- }
194
- var listParam = {
195
- filters,
196
- pagination: {
197
- page,
198
- limit
199
- }
200
- };
201
- return (await fetchMyBoardList(listParam)).boardsCreatedByMe;
209
+ return this._fetchSimpleBoardList(fetchMyBoardList, 'boardsCreatedByMe', { page, limit });
202
210
  }
203
211
  async refreshBoards() {
204
212
  if (!this.groups) {
205
213
  await this.refresh();
206
214
  return;
207
215
  }
208
- var { items: boards, total } = await this.getBoards();
216
+ const { items: boards, total } = await this.getBoards();
209
217
  this.boards = boards;
210
218
  this._page = 1;
211
219
  this._total = total;
220
+ this._lastFetchTime = new Date();
212
221
  this.updateContext();
213
222
  this.scrollTargetEl.style.transition = '';
214
223
  this.scrollTargetEl.style.transform = `translate3d(0, 0, 0)`;
224
+ this.fillViewport();
225
+ }
226
+ async fillViewport() {
227
+ if (this._filling)
228
+ return;
229
+ this._filling = true;
230
+ try {
231
+ const el = this.scrollTargetEl;
232
+ if (!el)
233
+ return;
234
+ const { limit } = this._infiniteScrollOptions;
235
+ while (this._page < this._total / limit && el.scrollHeight <= el.clientHeight) {
236
+ await this.appendBoards();
237
+ await this.updateComplete;
238
+ }
239
+ }
240
+ finally {
241
+ this._filling = false;
242
+ }
243
+ }
244
+ /**
245
+ * 기존 목록을 유지하면서 변경된 보드만 갱신한다.
246
+ * 스크롤 위치가 보존된다.
247
+ */
248
+ async incrementalRefresh() {
249
+ if (!this._lastFetchTime || this.boards.length === 0) {
250
+ return this.refreshBoards();
251
+ }
252
+ const updatedBoards = await fetchBoardsUpdatedSince(this._lastFetchTime);
253
+ if (updatedBoards && updatedBoards.length > 0) {
254
+ const updatedMap = new Map(updatedBoards.map((b) => [b.id, b]));
255
+ const changedIds = [];
256
+ this.boards = this.boards
257
+ .map(board => {
258
+ const updated = updatedMap.get(board.id);
259
+ if (!updated)
260
+ return board;
261
+ // 삭제되었거나 현재 그룹에서 벗어난 경우 제거
262
+ if (updated.deletedAt || (this.groupId && updated.group?.id !== this.groupId))
263
+ return null;
264
+ changedIds.push(updated.id);
265
+ return updated;
266
+ })
267
+ .filter(Boolean);
268
+ this._updatedBoardIds = changedIds;
269
+ }
270
+ this._lastFetchTime = new Date();
215
271
  }
216
272
  async appendBoards() {
217
273
  if (!this.groups) {
218
274
  await this.refresh();
219
275
  return;
220
276
  }
221
- var { items: boards, total } = await this.getBoards({ page: this._page + 1 });
222
- this.boards = [...this.boards, ...boards];
277
+ const { items: appendedBoards, total } = await this.getBoards({ page: this._page + 1 });
278
+ const existingIds = new Set(this.boards.map(b => b.id));
279
+ const newBoards = appendedBoards.filter(b => !existingIds.has(b.id));
280
+ this.boards = [...this.boards, ...newBoards];
223
281
  this._page = this._page + 1;
224
282
  this._total = total;
283
+ this._lastFetchTime = new Date();
225
284
  }
226
285
  async scrollAction() {
227
286
  return this.appendBoards();
@@ -231,11 +290,23 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
231
290
  }
232
291
  async pageUpdated(changes, lifecycle) {
233
292
  if (this.active) {
293
+ const prevGroupId = this.groupId;
294
+ const prevSearchText = this.searchText;
234
295
  this.page = lifecycle.page;
235
296
  this.groupId = lifecycle.resourceId;
236
297
  this.searchText = lifecycle.params?.search || '';
237
298
  await this.updateComplete;
238
- this.refreshBoards();
299
+ // 검색어가 변경되었으면 전체 갱신
300
+ // 같은 그룹으로 복귀하고 기존 데이터가 있으면 변경분만 갱신 (스크롤 유지)
301
+ if (prevSearchText !== this.searchText) {
302
+ this.refreshBoards();
303
+ }
304
+ else if (prevGroupId === this.groupId && this.boards.length > 0 && this._lastFetchTime) {
305
+ this.incrementalRefresh();
306
+ }
307
+ else {
308
+ this.refreshBoards();
309
+ }
239
310
  }
240
311
  }
241
312
  connectedCallback() {
@@ -264,8 +335,8 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
264
335
  container: list,
265
336
  animates: {
266
337
  dragging: async (d, opts) => {
267
- var groups = [{ id: '' }, { id: 'favor' }, ...this.groups];
268
- var currentIndex = groups.findIndex(group => group.id == this.groupId);
338
+ const groups = [{ id: '' }, { id: 'favor' }, ...this.groups];
339
+ const currentIndex = groups.findIndex(group => group.id == this.groupId);
269
340
  if ((d > 0 && currentIndex <= 0) || (d < 0 && currentIndex >= groups.length - 1)) {
270
341
  /* TODO blocked gesture */
271
342
  return false;
@@ -280,8 +351,8 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
280
351
  });
281
352
  },
282
353
  swiping: async (d, opts) => {
283
- var groups = [{ id: '' }, { id: 'favor' }, ...this.groups];
284
- var currentIndex = groups.findIndex(group => group.id == this.groupId);
354
+ const groups = [{ id: '' }, { id: 'favor' }, ...this.groups];
355
+ const currentIndex = groups.findIndex(group => group.id == this.groupId);
285
356
  if ((d > 0 && currentIndex <= 0) || (d < 0 && currentIndex >= groups.length - 1)) {
286
357
  list.style.transition = '';
287
358
  list.style.transform = `translate3d(0, 0, 0)`;
@@ -329,43 +400,65 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
329
400
  async onCreateGroup(group) {
330
401
  try {
331
402
  const data = await createGroup(group);
332
- data && this._notify('info', i18next.t('text.group created', { group: group.name }));
403
+ data && notify('info', i18next.t('text.group created', { group: group.name }));
333
404
  }
334
405
  catch (ex) {
335
- this._notify('error', ex, ex);
406
+ notify('error', ex, ex);
336
407
  }
337
408
  this.refresh();
338
409
  }
339
410
  async onUpdateGroup(group) {
340
411
  try {
341
412
  const data = await updateGroup(group);
342
- data && this._notify('info', i18next.t('text.group updated', { group: group.name }));
413
+ data && notify('info', i18next.t('text.group updated', { group: group.name }));
343
414
  }
344
415
  catch (ex) {
345
- this._notify('error', ex, ex);
416
+ notify('error', ex, ex);
346
417
  }
347
418
  this.refresh();
348
419
  }
349
420
  async onDeleteGroup(group) {
350
421
  try {
351
422
  const data = await deleteGroup(group.id);
352
- data && this._notify('info', i18next.t('text.group deleted', { group: group.name }));
423
+ data && notify('info', i18next.t('text.group deleted', { group: group.name }));
353
424
  }
354
425
  catch (ex) {
355
- this._notify('error', ex, ex);
426
+ notify('error', ex, ex);
356
427
  }
357
428
  this.refresh();
358
429
  }
359
430
  async onImportBoards({ group, files, overwrite }) {
360
431
  try {
361
432
  const data = await importBoards({ groupId: group.id, files, overwrite });
362
- data && this._notify('info', i18next.t('text.boards imported', { count: files.length, group: group.name }));
433
+ data && notify('info', i18next.t('text.boards imported', { count: files.length, group: group.name }));
363
434
  }
364
435
  catch (ex) {
365
- this._notify('error', ex, ex);
436
+ notify('error', ex, ex);
366
437
  }
367
438
  this.refresh();
368
439
  }
440
+ async onReorderBoards({ boardIds, movedId }) {
441
+ if (!movedId)
442
+ return;
443
+ const newIndex = boardIds.indexOf(movedId);
444
+ const prevId = newIndex > 0 ? boardIds[newIndex - 1] : undefined;
445
+ const nextId = newIndex < boardIds.length - 1 ? boardIds[newIndex + 1] : undefined;
446
+ this.boards = boardIds.map(id => this.boards.find(b => b.id === id)).filter(Boolean);
447
+ try {
448
+ await client.mutate({
449
+ mutation: gql `
450
+ mutation ($id: String!, $prevId: String, $nextId: String) {
451
+ reorderBoard(id: $id, prevId: $prevId, nextId: $nextId)
452
+ }
453
+ `,
454
+ variables: { id: movedId, prevId, nextId }
455
+ });
456
+ }
457
+ catch (ex) {
458
+ notify('error', ex, ex);
459
+ this.refreshBoards();
460
+ }
461
+ }
369
462
  async onCreateBoard(board) {
370
463
  try {
371
464
  if (!board.model) {
@@ -375,10 +468,10 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
375
468
  };
376
469
  }
377
470
  const data = await createBoard(board);
378
- data && this._notify('info', i18next.t('text.board created', { board: board.name }));
471
+ data && notify('info', i18next.t('text.board created', { board: board.name }));
379
472
  }
380
473
  catch (ex) {
381
- this._notify('error', ex, ex);
474
+ notify('error', ex, ex);
382
475
  }
383
476
  this.refreshBoards();
384
477
  }
@@ -391,52 +484,52 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
391
484
  targetSubdomain,
392
485
  targetGroupId
393
486
  });
394
- data && this._notify('info', i18next.t('text.board cloned', { board: name }));
487
+ data && notify('info', i18next.t('text.board cloned', { board: name }));
395
488
  }
396
489
  catch (ex) {
397
- this._notify('error', ex, ex);
490
+ notify('error', ex, ex);
398
491
  }
399
- this.refreshBoards();
492
+ this.incrementalRefresh();
400
493
  }
401
494
  async onReleaseBoard(board) {
402
495
  try {
403
496
  const data = await releaseBoard(board);
404
- data && this._notify('info', i18next.t('text.board released', { board: board.name }));
497
+ data && notify('info', i18next.t('text.board released', { board: board.name }));
405
498
  }
406
499
  catch (ex) {
407
- this._notify('error', ex, ex);
500
+ notify('error', ex, ex);
408
501
  }
409
- this.refreshBoards();
502
+ this.incrementalRefresh();
410
503
  }
411
504
  async onRevertBoardVersion({ id, version }) {
412
505
  try {
413
506
  const board = await revertBoardVersion(id, version);
414
- board && this._notify('info', i18next.t('text.board reverted', { board: board.name, version }));
507
+ board && notify('info', i18next.t('text.board reverted', { board: board.name, version }));
415
508
  }
416
509
  catch (ex) {
417
- this._notify('error', ex, ex);
510
+ notify('error', ex, ex);
418
511
  }
419
- this.refreshBoards();
512
+ this.incrementalRefresh();
420
513
  }
421
514
  async onUpdateBoard(board) {
422
515
  try {
423
516
  const data = await updateBoard(board);
424
- data && this._notify('info', i18next.t('text.board updated', { board: board.name }));
517
+ data && notify('info', i18next.t('text.board updated', { board: board.name }));
425
518
  }
426
519
  catch (ex) {
427
- this._notify('error', ex, ex);
520
+ notify('error', ex, ex);
428
521
  }
429
- this.refreshBoards();
522
+ this.incrementalRefresh();
430
523
  }
431
524
  async onDeleteBoard(board) {
432
525
  try {
433
526
  const data = await deleteBoard(board.id);
434
- data && this._notify('info', i18next.t('text.board deleted', { board: board.name }));
527
+ data && notify('info', i18next.t('text.board deleted', { board: board.name }));
435
528
  }
436
529
  catch (ex) {
437
- this._notify('error', ex, ex);
530
+ notify('error', ex, ex);
438
531
  }
439
- this.refreshBoards();
532
+ this.incrementalRefresh();
440
533
  }
441
534
  async onJoinPlayGroup({ board, playGroup }) {
442
535
  try {
@@ -454,10 +547,10 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
454
547
  }
455
548
  });
456
549
  data &&
457
- this._notify('info', i18next.t('text.joined into play-group', { board: board.name, playGroup: playGroup.name }));
550
+ notify('info', i18next.t('text.joined into play-group', { board: board.name, playGroup: playGroup.name }));
458
551
  }
459
552
  catch (ex) {
460
- this._notify('error', ex, ex);
553
+ notify('error', ex, ex);
461
554
  }
462
555
  }
463
556
  async onLeavePlayGroup({ board, playGroup }) {
@@ -476,10 +569,10 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
476
569
  }
477
570
  });
478
571
  data &&
479
- this._notify('info', i18next.t('text.leaved from play-group', { board: board.name, playGroup: playGroup.name }));
572
+ notify('info', i18next.t('text.leaved from play-group', { board: board.name, playGroup: playGroup.name }));
480
573
  }
481
574
  catch (ex) {
482
- this._notify('error', ex, ex);
575
+ notify('error', ex, ex);
483
576
  }
484
577
  }
485
578
  async onRegisterTemplate({ id, name, description, visibility }) {
@@ -500,12 +593,12 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
500
593
  }
501
594
  });
502
595
  data &&
503
- this._notify('info', i18next.t('text.info_x_successfully', { x: i18next.t('text.register-template') }), {
596
+ notify('info', i18next.t('text.info_x_successfully', { x: i18next.t('text.register-template') }), {
504
597
  name
505
598
  });
506
599
  }
507
600
  catch (ex) {
508
- this._notify('error', ex, ex);
601
+ notify('error', ex, ex);
509
602
  }
510
603
  }
511
604
  async refreshFavorites() {
@@ -525,15 +618,6 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
525
618
  favorites: data.myFavorites.map(favorite => favorite.routing)
526
619
  });
527
620
  }
528
- _notify(level, message, ex) {
529
- document.dispatchEvent(new CustomEvent('notify', {
530
- detail: {
531
- level,
532
- message,
533
- ex
534
- }
535
- }));
536
- }
537
621
  };
538
622
  __decorate([
539
623
  property({ type: String }),
@@ -563,6 +647,10 @@ __decorate([
563
647
  query('board-tile-list'),
564
648
  __metadata("design:type", HTMLElement)
565
649
  ], BoardListPage.prototype, "scrollTargetEl", void 0);
650
+ __decorate([
651
+ state(),
652
+ __metadata("design:type", Array)
653
+ ], BoardListPage.prototype, "_updatedBoardIds", void 0);
566
654
  BoardListPage = __decorate([
567
655
  customElement('board-list-page'),
568
656
  __metadata("design:paramtypes", [])