@things-factory/board-service 10.0.0-beta.47 → 10.0.0-beta.49

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.
@@ -9,5 +9,5 @@ export declare class BoardMutation {
9
9
  revertBoardVersion(id: string, version: number, context: ResolverContext): Promise<Board>;
10
10
  deleteBoard(id: string, context: ResolverContext): Promise<boolean>;
11
11
  importBoards(groupId: string, files: FileUpload[], overwrite: boolean, context: ResolverContext): Promise<Board[]>;
12
- reorderBoard(id: string, sortOrder: number, context: ResolverContext): Promise<boolean>;
12
+ reorderBoard(id: string, prevId: string, nextId: string, context: ResolverContext): Promise<boolean>;
13
13
  }
@@ -11,6 +11,24 @@ const group_js_1 = require("../group/group.js");
11
11
  const board_js_1 = require("./board.js");
12
12
  const board_history_js_1 = require("./board-history.js");
13
13
  const board_type_js_1 = require("./board-type.js");
14
+ /**
15
+ * prev와 next 사이에서 자릿수가 최소인 중간값을 찾는다.
16
+ * (prev, next) open interval에서 가장 깔끔한 숫자를 선택.
17
+ */
18
+ function optimalMidValue(prev, next) {
19
+ if (prev >= next)
20
+ return prev - 1;
21
+ const mid = (prev + next) / 2;
22
+ for (let p = 0; p <= 10; p++) {
23
+ const scale = 10 ** p;
24
+ const low = Math.floor(prev * scale) + 1;
25
+ const high = Math.ceil(next * scale) - 1;
26
+ if (low <= high) {
27
+ return Math.min(Math.max(Math.round(mid * scale), low), high) / scale;
28
+ }
29
+ }
30
+ return mid;
31
+ }
14
32
  async function parseJSONFile(uploadedFile) {
15
33
  var { createReadStream } = await uploadedFile;
16
34
  return new Promise((resolve, reject) => {
@@ -357,9 +375,56 @@ let BoardMutation = class BoardMutation {
357
375
  });
358
376
  return boards;
359
377
  }
360
- async reorderBoard(id, sortOrder, context) {
378
+ async reorderBoard(id, prevId, nextId, context) {
361
379
  const { domain, tx } = context.state;
362
- await tx.getRepository(board_js_1.Board).update({ id, domain: { id: domain.id } }, { sortOrder });
380
+ const repo = tx.getRepository(board_js_1.Board);
381
+ const domainId = domain.id;
382
+ // prev/next의 sortOrder 조회
383
+ let prevOrder = null;
384
+ let nextOrder = null;
385
+ if (prevId) {
386
+ const prev = await repo.findOne({ where: { id: prevId, domain: { id: domainId } } });
387
+ prevOrder = prev?.sortOrder ?? null;
388
+ }
389
+ if (nextId) {
390
+ const next = await repo.findOne({ where: { id: nextId, domain: { id: domainId } } });
391
+ nextOrder = next?.sortOrder ?? null;
392
+ }
393
+ // sortOrder 계산
394
+ let prev = prevOrder ?? (nextOrder != null ? nextOrder - 2 : 0);
395
+ let next = nextOrder ?? prev + 2;
396
+ // prev와 next의 간격이 너무 좁으면 전체 보드 sortOrder 정규화
397
+ if (next - prev < 0.001) {
398
+ const allBoards = await repo.find({
399
+ where: { domain: { id: domainId } }
400
+ });
401
+ // DB 독립적인 정렬 (NULL → 맨 뒤, sortOrder ASC, name ASC)
402
+ allBoards.sort((a, b) => {
403
+ const sa = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
404
+ const sb = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
405
+ if (sa !== sb)
406
+ return sa - sb;
407
+ return (a.name || '').localeCompare(b.name || '');
408
+ });
409
+ for (let i = 0; i < allBoards.length; i++) {
410
+ if (allBoards[i].sortOrder !== i) {
411
+ await repo.update({ id: allBoards[i].id }, { sortOrder: i });
412
+ allBoards[i].sortOrder = i;
413
+ }
414
+ }
415
+ // 정규화된 값으로 prev/next 재조회
416
+ if (prevId) {
417
+ prev = allBoards.find(b => b.id === prevId)?.sortOrder ?? prev;
418
+ }
419
+ if (nextId) {
420
+ next = allBoards.find(b => b.id === nextId)?.sortOrder ?? next;
421
+ }
422
+ else {
423
+ next = prev + 2;
424
+ }
425
+ }
426
+ const sortOrder = optimalMidValue(prev, next);
427
+ await repo.update({ id, domain: { id: domainId } }, { sortOrder });
363
428
  return true;
364
429
  }
365
430
  };
@@ -446,14 +511,15 @@ tslib_1.__decorate([
446
511
  tslib_1.__metadata("design:returntype", Promise)
447
512
  ], BoardMutation.prototype, "importBoards", null);
448
513
  tslib_1.__decorate([
449
- (0, type_graphql_1.Mutation)(returns => Boolean, { description: 'Updates the sortOrder of a single board.' }),
514
+ (0, type_graphql_1.Mutation)(returns => Boolean, { description: 'Reorders a board between two adjacent boards by ID.' }),
450
515
  (0, type_graphql_1.Directive)('@privilege(category: "board", privilege: "mutation")'),
451
516
  (0, type_graphql_1.Directive)('@transaction'),
452
517
  tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
453
- tslib_1.__param(1, (0, type_graphql_1.Arg)('sortOrder')),
454
- tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
518
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('prevId', { nullable: true })),
519
+ tslib_1.__param(2, (0, type_graphql_1.Arg)('nextId', { nullable: true })),
520
+ tslib_1.__param(3, (0, type_graphql_1.Ctx)()),
455
521
  tslib_1.__metadata("design:type", Function),
456
- tslib_1.__metadata("design:paramtypes", [String, Number, Object]),
522
+ tslib_1.__metadata("design:paramtypes", [String, String, String, Object]),
457
523
  tslib_1.__metadata("design:returntype", Promise)
458
524
  ], BoardMutation.prototype, "reorderBoard", null);
459
525
  exports.BoardMutation = BoardMutation = tslib_1.__decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"board-mutation.js","sourceRoot":"","sources":["../../../server/service/board/board-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Resolver, Directive } from 'type-graphql'\nimport { EntityManager, In } from 'typeorm'\nimport type { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport { Domain, getDataSource, getRedirectSubdomainPath, getRepository } from '@things-factory/shell'\n\nimport { thumbnail } from '../../controllers/thumbnail.js'\nimport { Group } from '../group/group.js'\nimport { Board } from './board.js'\nimport { BoardHistory } from './board-history.js'\nimport { BoardPatch, NewBoard } from './board-type.js'\n\nasync function parseJSONFile(uploadedFile: FileUpload): Promise<any> {\n var { createReadStream } = await uploadedFile\n\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = []\n\n createReadStream()\n .on('data', (chunk: Uint8Array) => {\n chunks.push(chunk)\n })\n .on('end', () => {\n try {\n const fileContents = Buffer.concat(chunks).toString('utf-8')\n const jsonData = JSON.parse(fileContents)\n resolve(jsonData)\n } catch (error) {\n reject(error)\n }\n })\n .on('error', (error: Error) => {\n reject(error)\n })\n })\n}\n\n@Resolver(Board)\nexport class BoardMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, { description: 'Creates a new board.' })\n async createBoard(@Arg('board') board: NewBoard, @Ctx() context: ResolverContext): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n const groupRepository = getRepository(Group, tx)\n\n const oldBoard: Board = await repository.findOneBy({\n name: board.name,\n domain: { id: domain.id }\n })\n\n if (oldBoard) {\n throw new Error(context.t('error.board name is already taken', { name: board.name }))\n }\n\n const newBoard: Board = {\n ...board\n }\n\n newBoard.thumbnail ||=\n 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* empty thumbnail */\n\n if (board.groupId) {\n newBoard.group = await groupRepository.findOneBy({\n id: board.groupId\n })\n }\n\n // 새 보드는 맨 앞에 배치\n const { minSortOrder } = (await repository\n .createQueryBuilder('board')\n .select('MIN(board.sortOrder)', 'minSortOrder')\n .where('board.domain = :domainId', { domainId: domain.id })\n .getRawOne()) || { minSortOrder: null }\n\n const created = await repository.save({\n domain,\n ...newBoard,\n sortOrder: minSortOrder != null ? minSortOrder - 1 : 0,\n state: 'draft',\n creator: user,\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${created.name}' created`,\n body: `Board '${created.name}' created by ${user.name}\\n${created.description}`,\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${created.id}`)\n })\n\n return created\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, {\n description: 'Clones a board from an existing one, potentially into a different domain.'\n })\n async cloneBoard(\n @Arg('id') id: string,\n @Arg('patch') patch: BoardPatch,\n @Arg('targetSubdomain') targetSubdomain: string,\n @Arg('targetGroupId', { nullable: true }) targetGroupId: string,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const { t } = context\n const repository = getRepository(Board, tx)\n\n const board = await repository.findOneBy({ domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id })\n\n if (!patch.name || (patch.name == board.name && targetSubdomain == domain.subdomain)) {\n throw new Error(t('error.name must be unique from the original board', { name: patch.name }))\n }\n\n if (targetSubdomain != domain.subdomain) {\n if ((await repository.count({ where: { domain: { subdomain: targetSubdomain }, name: patch.name } })) > 0) {\n throw new Error(t('error.name must be unique from the original board', { name: patch.name }))\n }\n }\n\n var targetDomain = domain\n if (targetDomain && domain.subdomain != targetSubdomain) {\n targetDomain = await getRepository(Domain, tx).findOneBy({ subdomain: targetSubdomain })\n if (!targetDomain) {\n throw new Error(`given subdomain(${targetSubdomain}) not found`)\n }\n }\n\n var targetGroup = null\n if (targetGroupId) {\n targetGroup = await getRepository(Group, tx).findOneBy({ domain: { id: targetDomain.id }, id: targetGroupId })\n if (!targetGroup) {\n throw new Error(`given group(${targetGroupId}) in domain(${targetSubdomain}) not found`)\n }\n }\n\n const { id: excluded, ...clone } = board\n\n const cloned = await repository.save({\n domain: targetDomain,\n ...clone,\n ...patch,\n group: targetGroup,\n version: 0,\n state: 'draft',\n updater: user,\n creator: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${cloned.name}' cloned`,\n body: `Board '${cloned.name}' cloned by ${user.name}\\n${cloned.description}`,\n image: getRedirectSubdomainPath(context, targetDomain, `/thumbnail/${cloned.id}`),\n url: getRedirectSubdomainPath(context, targetDomain, `/board-viewer/${cloned.id}`)\n })\n\n return cloned\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, {\n description: 'Updates a board. If a model is provided, it also generates a new thumbnail.'\n })\n async updateBoard(\n @Arg('id') id: string,\n @Arg('patch') patch: BoardPatch,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n\n const board = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['creator']\n })\n\n if (!board) {\n throw new Error('board not found')\n }\n\n if (patch.model) {\n const thumbnailPromise = thumbnail({\n model: patch.model,\n context\n })\n\n try {\n const thumbnailBase64 = await Promise.race([\n thumbnailPromise,\n new Promise((_, reject) => setTimeout(() => reject(new Error('5 seconds timeout')), 5000))\n ])\n\n patch.thumbnail = 'data:image/png;base64,' + thumbnailBase64.toString('base64')\n } catch (e) {\n console.warn(`Failed to get thumbnail for '${board.name}' in first 5 seconds`)\n // 5초 안에 썸네일이 생성되지 않았으므로 모델만 저장합니다.\n patch.thumbnail =\n 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* empty thumbnail */\n\n Promise.race([\n thumbnailPromise,\n new Promise((_, reject) => setTimeout(() => reject(new Error('Thumbnail extended timeout')), 30000))\n ])\n .then(async thumbnailBase64 => {\n /* use new resource manager */\n await getDataSource().transaction(async (tx: EntityManager) => {\n await getRepository(Board, tx).save({\n id: updated.id,\n thumbnail: 'data:image/png;base64,' + thumbnailBase64.toString('base64')\n })\n })\n })\n .catch(error => {\n console.error(`Failed to save thumbnail for '${board.name}' even after extended time:`, error)\n })\n }\n }\n\n const { groupId, ...patched } = patch\n\n if (groupId !== undefined) {\n const groupRepository = getRepository(Group, tx)\n board.group = groupId\n ? (await groupRepository.findOneBy({\n domain: { id: domain.id },\n id: groupId\n })) || null\n : null\n }\n\n const updated = await repository.save({\n domain,\n ...board,\n ...patched,\n state: 'draft',\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${updated.name}' updated`,\n body: `Board '${updated.name}' updated by ${user.name}\\n${updated.description}`,\n image: getRedirectSubdomainPath(context, domain, `/thumbnail/${updated.id}`),\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${updated.id}`)\n })\n\n return updated\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, { description: 'Releases a board, making it public and creating a version history.' })\n async releaseBoard(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n const historyRepository = getRepository(BoardHistory, tx)\n\n const board = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['creator']\n })\n\n if (!board) {\n throw new Error(`Board given id(${id}) is not found`)\n }\n\n if (board.state == 'released') {\n throw new Error(`Board given id(${id}) is already released`)\n }\n\n // 히스토리에서 max version 가져오기\n const maxHistory = await historyRepository\n .createQueryBuilder('history')\n .select('MAX(history.version)', 'max')\n .where('history.originalId = :id', { id: board.id })\n .getRawOne()\n\n const nextVersion = maxHistory?.max ? Number(maxHistory.max) + 1 : 1\n\n const updated = await repository.save({\n domain,\n ...board,\n version: nextVersion,\n state: 'released',\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${updated.name}' released`,\n body: `Board '${updated.name}' released by ${user.name}\\n${updated.description}`,\n image: getRedirectSubdomainPath(context, domain, `/thumbnail/${updated.id}`),\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${updated.id}`)\n })\n\n return updated\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, { description: 'Reverts a board to a specific historical version.' })\n async revertBoardVersion(\n @Arg('id') id: string,\n @Arg('version') version: number,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n\n const board = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['creator']\n })\n\n if (!board) {\n throw new Error(`Board with id(${id}) is not found`)\n }\n\n const historyRepository = getRepository(BoardHistory, tx)\n\n const boardHistory = await historyRepository.findOne({\n where: { domain: { id: domain.id }, originalId: id, version },\n order: { version: 'DESC' }\n })\n\n if (!boardHistory) {\n throw new Error(`Board with id:version(${id}:${version}) is not found`)\n }\n\n const updated = await repository.save({\n domain,\n ...board,\n model: boardHistory.model,\n thumbnail: boardHistory.thumbnail,\n state: 'draft',\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${updated.name}' updated`,\n body: `Board '${updated.name}' updated by ${user.name}\\n${updated.description}`,\n image: getRedirectSubdomainPath(context, domain, `/thumbnail/${updated.id}`),\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${updated.id}`)\n })\n\n return updated\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'Deletes a board.' })\n async deleteBoard(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n const board = await repository.findOneBy({ domain: { id: domain.id }, id })\n\n const deleted = await repository.softDelete(id)\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${board.name}' deleted`,\n body: `Board '${board.name}' deleted by ${user.name}\\n${board.description}`\n })\n\n return true\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Board], { description: 'Imports multiple boards from JSON files.' })\n async importBoards(\n @Arg('groupId') groupId: string,\n @Arg('files', () => [GraphQLUpload]) files: FileUpload[],\n @Arg('overwrite') overwrite: boolean,\n @Ctx() context: ResolverContext\n ): Promise<Board[]> {\n const { domain, user, notify, tx } = context.state\n const groupRepository = getRepository(Group, tx)\n const boardRepository = getRepository(Board, tx)\n const group = await groupRepository.findOneBy({ domain: { id: domain.id }, id: groupId })\n\n if (!group) {\n throw new Error(`Group with id(${groupId}) is not found`)\n }\n\n const boards = []\n\n for (const file of files) {\n const { id, name, description, model, thumbnail } = await parseJSONFile(file)\n\n var sameNameBoard = await boardRepository.findOneBy({ domain: { id: domain.id }, name })\n var sameIdBoard = await boardRepository.findOneBy({ id })\n\n if (overwrite) {\n var board = {} as any\n\n if (sameIdBoard) {\n if (overwrite && sameIdBoard.domainId != domain.id) {\n throw new Error(`Board with id(${id}) is already taken in another domain`)\n }\n\n board = {\n ...sameIdBoard,\n name\n }\n\n if (sameNameBoard && sameIdBoard.id != sameNameBoard.id) {\n /* 이름 충돌 회피 */\n board.name = `${board.name}(${Date.now()})`\n }\n } else {\n board = {\n id,\n name: sameNameBoard ? `${name}(${Date.now()})` : name,\n version: 0,\n creator: user\n }\n }\n } else {\n board = {\n name,\n version: 0,\n creator: user\n }\n\n /* ID가 없으면, 사용해도 됨 */\n if (!sameIdBoard) {\n board.id = id\n }\n\n /* 이름 충돌 회피 */\n if (sameNameBoard) {\n board.name = `${board.name}(${Date.now()})`\n }\n }\n\n boards.push(\n await boardRepository.save({\n ...board,\n domain,\n description,\n model: typeof model != 'string' ? JSON.stringify(model) : model,\n thumbnail,\n group,\n state: 'draft',\n updater: user\n })\n )\n }\n\n notify &&\n notify({\n mode: 'in-app',\n title: `${boards.length} Board(s) are imported`,\n body: `${boards.length} Board(s) are imported into group ${group.name} by ${user.name}`\n })\n\n return boards\n }\n\n @Mutation(returns => Boolean, { description: 'Updates the sortOrder of a single board.' })\n @Directive('@privilege(category: \"board\", privilege: \"mutation\")')\n @Directive('@transaction')\n async reorderBoard(\n @Arg('id') id: string,\n @Arg('sortOrder') sortOrder: number,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n const { domain, tx } = context.state\n await tx.getRepository(Board).update({ id, domain: { id: domain.id } }, { sortOrder })\n return true\n }\n}\n"]}
1
+ {"version":3,"file":"board-mutation.js","sourceRoot":"","sources":["../../../server/service/board/board-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Resolver, Directive } from 'type-graphql'\nimport { EntityManager, In } from 'typeorm'\nimport type { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport { Domain, getDataSource, getRedirectSubdomainPath, getRepository } from '@things-factory/shell'\n\nimport { thumbnail } from '../../controllers/thumbnail.js'\nimport { Group } from '../group/group.js'\nimport { Board } from './board.js'\nimport { BoardHistory } from './board-history.js'\nimport { BoardPatch, NewBoard } from './board-type.js'\n\n/**\n * prev와 next 사이에서 자릿수가 최소인 중간값을 찾는다.\n * (prev, next) open interval에서 가장 깔끔한 숫자를 선택.\n */\nfunction optimalMidValue(prev: number, next: number): number {\n if (prev >= next) return prev - 1\n\n const mid = (prev + next) / 2\n for (let p = 0; p <= 10; p++) {\n const scale = 10 ** p\n const low = Math.floor(prev * scale) + 1\n const high = Math.ceil(next * scale) - 1\n if (low <= high) {\n return Math.min(Math.max(Math.round(mid * scale), low), high) / scale\n }\n }\n return mid\n}\n\nasync function parseJSONFile(uploadedFile: FileUpload): Promise<any> {\n var { createReadStream } = await uploadedFile\n\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = []\n\n createReadStream()\n .on('data', (chunk: Uint8Array) => {\n chunks.push(chunk)\n })\n .on('end', () => {\n try {\n const fileContents = Buffer.concat(chunks).toString('utf-8')\n const jsonData = JSON.parse(fileContents)\n resolve(jsonData)\n } catch (error) {\n reject(error)\n }\n })\n .on('error', (error: Error) => {\n reject(error)\n })\n })\n}\n\n@Resolver(Board)\nexport class BoardMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, { description: 'Creates a new board.' })\n async createBoard(@Arg('board') board: NewBoard, @Ctx() context: ResolverContext): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n const groupRepository = getRepository(Group, tx)\n\n const oldBoard: Board = await repository.findOneBy({\n name: board.name,\n domain: { id: domain.id }\n })\n\n if (oldBoard) {\n throw new Error(context.t('error.board name is already taken', { name: board.name }))\n }\n\n const newBoard: Board = {\n ...board\n }\n\n newBoard.thumbnail ||=\n 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* empty thumbnail */\n\n if (board.groupId) {\n newBoard.group = await groupRepository.findOneBy({\n id: board.groupId\n })\n }\n\n // 새 보드는 맨 앞에 배치\n const { minSortOrder } = (await repository\n .createQueryBuilder('board')\n .select('MIN(board.sortOrder)', 'minSortOrder')\n .where('board.domain = :domainId', { domainId: domain.id })\n .getRawOne()) || { minSortOrder: null }\n\n const created = await repository.save({\n domain,\n ...newBoard,\n sortOrder: minSortOrder != null ? minSortOrder - 1 : 0,\n state: 'draft',\n creator: user,\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${created.name}' created`,\n body: `Board '${created.name}' created by ${user.name}\\n${created.description}`,\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${created.id}`)\n })\n\n return created\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, {\n description: 'Clones a board from an existing one, potentially into a different domain.'\n })\n async cloneBoard(\n @Arg('id') id: string,\n @Arg('patch') patch: BoardPatch,\n @Arg('targetSubdomain') targetSubdomain: string,\n @Arg('targetGroupId', { nullable: true }) targetGroupId: string,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const { t } = context\n const repository = getRepository(Board, tx)\n\n const board = await repository.findOneBy({ domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id })\n\n if (!patch.name || (patch.name == board.name && targetSubdomain == domain.subdomain)) {\n throw new Error(t('error.name must be unique from the original board', { name: patch.name }))\n }\n\n if (targetSubdomain != domain.subdomain) {\n if ((await repository.count({ where: { domain: { subdomain: targetSubdomain }, name: patch.name } })) > 0) {\n throw new Error(t('error.name must be unique from the original board', { name: patch.name }))\n }\n }\n\n var targetDomain = domain\n if (targetDomain && domain.subdomain != targetSubdomain) {\n targetDomain = await getRepository(Domain, tx).findOneBy({ subdomain: targetSubdomain })\n if (!targetDomain) {\n throw new Error(`given subdomain(${targetSubdomain}) not found`)\n }\n }\n\n var targetGroup = null\n if (targetGroupId) {\n targetGroup = await getRepository(Group, tx).findOneBy({ domain: { id: targetDomain.id }, id: targetGroupId })\n if (!targetGroup) {\n throw new Error(`given group(${targetGroupId}) in domain(${targetSubdomain}) not found`)\n }\n }\n\n const { id: excluded, ...clone } = board\n\n const cloned = await repository.save({\n domain: targetDomain,\n ...clone,\n ...patch,\n group: targetGroup,\n version: 0,\n state: 'draft',\n updater: user,\n creator: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${cloned.name}' cloned`,\n body: `Board '${cloned.name}' cloned by ${user.name}\\n${cloned.description}`,\n image: getRedirectSubdomainPath(context, targetDomain, `/thumbnail/${cloned.id}`),\n url: getRedirectSubdomainPath(context, targetDomain, `/board-viewer/${cloned.id}`)\n })\n\n return cloned\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, {\n description: 'Updates a board. If a model is provided, it also generates a new thumbnail.'\n })\n async updateBoard(\n @Arg('id') id: string,\n @Arg('patch') patch: BoardPatch,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n\n const board = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['creator']\n })\n\n if (!board) {\n throw new Error('board not found')\n }\n\n if (patch.model) {\n const thumbnailPromise = thumbnail({\n model: patch.model,\n context\n })\n\n try {\n const thumbnailBase64 = await Promise.race([\n thumbnailPromise,\n new Promise((_, reject) => setTimeout(() => reject(new Error('5 seconds timeout')), 5000))\n ])\n\n patch.thumbnail = 'data:image/png;base64,' + thumbnailBase64.toString('base64')\n } catch (e) {\n console.warn(`Failed to get thumbnail for '${board.name}' in first 5 seconds`)\n // 5초 안에 썸네일이 생성되지 않았으므로 모델만 저장합니다.\n patch.thumbnail =\n 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* empty thumbnail */\n\n Promise.race([\n thumbnailPromise,\n new Promise((_, reject) => setTimeout(() => reject(new Error('Thumbnail extended timeout')), 30000))\n ])\n .then(async thumbnailBase64 => {\n /* use new resource manager */\n await getDataSource().transaction(async (tx: EntityManager) => {\n await getRepository(Board, tx).save({\n id: updated.id,\n thumbnail: 'data:image/png;base64,' + thumbnailBase64.toString('base64')\n })\n })\n })\n .catch(error => {\n console.error(`Failed to save thumbnail for '${board.name}' even after extended time:`, error)\n })\n }\n }\n\n const { groupId, ...patched } = patch\n\n if (groupId !== undefined) {\n const groupRepository = getRepository(Group, tx)\n board.group = groupId\n ? (await groupRepository.findOneBy({\n domain: { id: domain.id },\n id: groupId\n })) || null\n : null\n }\n\n const updated = await repository.save({\n domain,\n ...board,\n ...patched,\n state: 'draft',\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${updated.name}' updated`,\n body: `Board '${updated.name}' updated by ${user.name}\\n${updated.description}`,\n image: getRedirectSubdomainPath(context, domain, `/thumbnail/${updated.id}`),\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${updated.id}`)\n })\n\n return updated\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, { description: 'Releases a board, making it public and creating a version history.' })\n async releaseBoard(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n const historyRepository = getRepository(BoardHistory, tx)\n\n const board = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['creator']\n })\n\n if (!board) {\n throw new Error(`Board given id(${id}) is not found`)\n }\n\n if (board.state == 'released') {\n throw new Error(`Board given id(${id}) is already released`)\n }\n\n // 히스토리에서 max version 가져오기\n const maxHistory = await historyRepository\n .createQueryBuilder('history')\n .select('MAX(history.version)', 'max')\n .where('history.originalId = :id', { id: board.id })\n .getRawOne()\n\n const nextVersion = maxHistory?.max ? Number(maxHistory.max) + 1 : 1\n\n const updated = await repository.save({\n domain,\n ...board,\n version: nextVersion,\n state: 'released',\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${updated.name}' released`,\n body: `Board '${updated.name}' released by ${user.name}\\n${updated.description}`,\n image: getRedirectSubdomainPath(context, domain, `/thumbnail/${updated.id}`),\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${updated.id}`)\n })\n\n return updated\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Board, { description: 'Reverts a board to a specific historical version.' })\n async revertBoardVersion(\n @Arg('id') id: string,\n @Arg('version') version: number,\n @Ctx() context: ResolverContext\n ): Promise<Board> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n\n const board = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['creator']\n })\n\n if (!board) {\n throw new Error(`Board with id(${id}) is not found`)\n }\n\n const historyRepository = getRepository(BoardHistory, tx)\n\n const boardHistory = await historyRepository.findOne({\n where: { domain: { id: domain.id }, originalId: id, version },\n order: { version: 'DESC' }\n })\n\n if (!boardHistory) {\n throw new Error(`Board with id:version(${id}:${version}) is not found`)\n }\n\n const updated = await repository.save({\n domain,\n ...board,\n model: boardHistory.model,\n thumbnail: boardHistory.thumbnail,\n state: 'draft',\n updater: user\n })\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${updated.name}' updated`,\n body: `Board '${updated.name}' updated by ${user.name}\\n${updated.description}`,\n image: getRedirectSubdomainPath(context, domain, `/thumbnail/${updated.id}`),\n url: getRedirectSubdomainPath(context, domain, `/board-viewer/${updated.id}`)\n })\n\n return updated\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'Deletes a board.' })\n async deleteBoard(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n const { domain, user, notify, tx } = context.state\n const repository = getRepository(Board, tx)\n const board = await repository.findOneBy({ domain: { id: domain.id }, id })\n\n const deleted = await repository.softDelete(id)\n\n notify &&\n notify({\n mode: 'in-app',\n title: `Board '${board.name}' deleted`,\n body: `Board '${board.name}' deleted by ${user.name}\\n${board.description}`\n })\n\n return true\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"board\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Board], { description: 'Imports multiple boards from JSON files.' })\n async importBoards(\n @Arg('groupId') groupId: string,\n @Arg('files', () => [GraphQLUpload]) files: FileUpload[],\n @Arg('overwrite') overwrite: boolean,\n @Ctx() context: ResolverContext\n ): Promise<Board[]> {\n const { domain, user, notify, tx } = context.state\n const groupRepository = getRepository(Group, tx)\n const boardRepository = getRepository(Board, tx)\n const group = await groupRepository.findOneBy({ domain: { id: domain.id }, id: groupId })\n\n if (!group) {\n throw new Error(`Group with id(${groupId}) is not found`)\n }\n\n const boards = []\n\n for (const file of files) {\n const { id, name, description, model, thumbnail } = await parseJSONFile(file)\n\n var sameNameBoard = await boardRepository.findOneBy({ domain: { id: domain.id }, name })\n var sameIdBoard = await boardRepository.findOneBy({ id })\n\n if (overwrite) {\n var board = {} as any\n\n if (sameIdBoard) {\n if (overwrite && sameIdBoard.domainId != domain.id) {\n throw new Error(`Board with id(${id}) is already taken in another domain`)\n }\n\n board = {\n ...sameIdBoard,\n name\n }\n\n if (sameNameBoard && sameIdBoard.id != sameNameBoard.id) {\n /* 이름 충돌 회피 */\n board.name = `${board.name}(${Date.now()})`\n }\n } else {\n board = {\n id,\n name: sameNameBoard ? `${name}(${Date.now()})` : name,\n version: 0,\n creator: user\n }\n }\n } else {\n board = {\n name,\n version: 0,\n creator: user\n }\n\n /* ID가 없으면, 사용해도 됨 */\n if (!sameIdBoard) {\n board.id = id\n }\n\n /* 이름 충돌 회피 */\n if (sameNameBoard) {\n board.name = `${board.name}(${Date.now()})`\n }\n }\n\n boards.push(\n await boardRepository.save({\n ...board,\n domain,\n description,\n model: typeof model != 'string' ? JSON.stringify(model) : model,\n thumbnail,\n group,\n state: 'draft',\n updater: user\n })\n )\n }\n\n notify &&\n notify({\n mode: 'in-app',\n title: `${boards.length} Board(s) are imported`,\n body: `${boards.length} Board(s) are imported into group ${group.name} by ${user.name}`\n })\n\n return boards\n }\n\n @Mutation(returns => Boolean, { description: 'Reorders a board between two adjacent boards by ID.' })\n @Directive('@privilege(category: \"board\", privilege: \"mutation\")')\n @Directive('@transaction')\n async reorderBoard(\n @Arg('id') id: string,\n @Arg('prevId', { nullable: true }) prevId: string,\n @Arg('nextId', { nullable: true }) nextId: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n const { domain, tx } = context.state\n const repo = tx.getRepository(Board)\n const domainId = domain.id\n\n // prev/next의 sortOrder 조회\n let prevOrder: number | null = null\n let nextOrder: number | null = null\n\n if (prevId) {\n const prev = await repo.findOne({ where: { id: prevId, domain: { id: domainId } } as any })\n prevOrder = prev?.sortOrder ?? null\n }\n if (nextId) {\n const next = await repo.findOne({ where: { id: nextId, domain: { id: domainId } } as any })\n nextOrder = next?.sortOrder ?? null\n }\n\n // sortOrder 계산\n let prev = prevOrder ?? (nextOrder != null ? nextOrder - 2 : 0)\n let next = nextOrder ?? prev + 2\n\n // prev와 next의 간격이 너무 좁으면 전체 보드 sortOrder 정규화\n if (next - prev < 0.001) {\n const allBoards = await repo.find({\n where: { domain: { id: domainId } } as any\n })\n\n // DB 독립적인 정렬 (NULL → 맨 뒤, sortOrder ASC, name ASC)\n allBoards.sort((a, b) => {\n const sa = a.sortOrder ?? Number.MAX_SAFE_INTEGER\n const sb = b.sortOrder ?? Number.MAX_SAFE_INTEGER\n if (sa !== sb) return sa - sb\n return (a.name || '').localeCompare(b.name || '')\n })\n\n for (let i = 0; i < allBoards.length; i++) {\n if (allBoards[i].sortOrder !== i) {\n await repo.update({ id: allBoards[i].id! }, { sortOrder: i })\n allBoards[i].sortOrder = i\n }\n }\n\n // 정규화된 값으로 prev/next 재조회\n if (prevId) {\n prev = allBoards.find(b => b.id === prevId)?.sortOrder ?? prev\n }\n if (nextId) {\n next = allBoards.find(b => b.id === nextId)?.sortOrder ?? next\n } else {\n next = prev + 2\n }\n }\n\n const sortOrder = optimalMidValue(prev, next)\n\n await repo.update({ id, domain: { id: domainId } }, { sortOrder })\n return true\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Board Reorder — 서버 측 sortOrder 계산 및 충돌 해결 테스트
3
+ */
4
+ declare function optimalMidValue(prev: number, next: number): number;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Board Reorder — 서버 측 sortOrder 계산 및 충돌 해결 테스트
3
+ */
4
+ // --- optimalMidValue (서버 board-mutation.ts와 동일) ---
5
+ function optimalMidValue(prev, next) {
6
+ if (prev >= next)
7
+ return prev - 1;
8
+ const mid = (prev + next) / 2;
9
+ for (let p = 0; p <= 10; p++) {
10
+ const scale = 10 ** p;
11
+ const low = Math.floor(prev * scale) + 1;
12
+ const high = Math.ceil(next * scale) - 1;
13
+ if (low <= high) {
14
+ return Math.min(Math.max(Math.round(mid * scale), low), high) / scale;
15
+ }
16
+ }
17
+ return mid;
18
+ }
19
+ // --- optimalMidValue 단위 테스트 ---
20
+ describe('optimalMidValue', () => {
21
+ test('정수 간격이 있으면 정수 반환', () => {
22
+ expect(optimalMidValue(1, 3)).toBe(2);
23
+ expect(optimalMidValue(0, 2)).toBe(1);
24
+ expect(optimalMidValue(10, 20)).toBe(15);
25
+ });
26
+ test('정수 간격이 없으면 소수 1자리', () => {
27
+ expect(optimalMidValue(1, 2)).toBe(1.5);
28
+ expect(optimalMidValue(3, 4)).toBe(3.5);
29
+ });
30
+ test('소수 1자리 간격에서 중간값', () => {
31
+ expect(optimalMidValue(1.3, 1.7)).toBe(1.5);
32
+ expect(optimalMidValue(1.1, 1.9)).toBe(1.5);
33
+ });
34
+ test('소수 1자리 인접 → 소수 2자리', () => {
35
+ expect(optimalMidValue(1.3, 1.4)).toBe(1.35);
36
+ expect(optimalMidValue(2.1, 2.2)).toBe(2.15);
37
+ });
38
+ test('결과값이 prev와 next 사이 (open interval)', () => {
39
+ const cases = [[1, 2], [1, 3], [1.3, 1.7], [1.3, 1.4], [0, 100], [-5, -3], [-1, 1]];
40
+ for (const [prev, next] of cases) {
41
+ const result = optimalMidValue(prev, next);
42
+ expect(result).toBeGreaterThan(prev);
43
+ expect(result).toBeLessThan(next);
44
+ }
45
+ });
46
+ test('prev == next → prev - 1', () => {
47
+ expect(optimalMidValue(5, 5)).toBe(4);
48
+ expect(optimalMidValue(0, 0)).toBe(-1);
49
+ });
50
+ test('prev > next (역전) → prev - 1', () => {
51
+ expect(optimalMidValue(5, 3)).toBe(4);
52
+ expect(optimalMidValue(10, 1)).toBe(9);
53
+ });
54
+ test('음수 범위', () => {
55
+ expect(optimalMidValue(-3, -1)).toBe(-2);
56
+ expect(optimalMidValue(-10, -5)).toBe(-7);
57
+ });
58
+ test('0을 포함하는 범위', () => {
59
+ expect(optimalMidValue(-1, 1)).toBe(0);
60
+ expect(optimalMidValue(-0.5, 0.5)).toBe(0);
61
+ });
62
+ test('매우 큰 간격', () => {
63
+ const result = optimalMidValue(0, 10000);
64
+ expect(result).toBe(5000);
65
+ expect(Number.isInteger(result)).toBe(true);
66
+ });
67
+ test('매우 작은 간격 (소수 3자리)', () => {
68
+ const result = optimalMidValue(1.31, 1.32);
69
+ expect(result).toBe(1.315);
70
+ expect(result).toBeGreaterThan(1.31);
71
+ expect(result).toBeLessThan(1.32);
72
+ });
73
+ test('반복 삽입해도 자릿수 제한 (10회)', () => {
74
+ let prev = 1;
75
+ let next = 2;
76
+ for (let i = 0; i < 10; i++) {
77
+ const mid = optimalMidValue(prev, next);
78
+ expect(mid).toBeGreaterThan(prev);
79
+ expect(mid).toBeLessThan(next);
80
+ const decimalPlaces = (mid.toString().split('.')[1] || '').length;
81
+ expect(decimalPlaces).toBeLessThanOrEqual(4);
82
+ next = mid;
83
+ }
84
+ });
85
+ test('왼쪽에 반복 삽입 (prev 쪽)', () => {
86
+ let prev = 1;
87
+ let next = 2;
88
+ for (let i = 0; i < 5; i++) {
89
+ const mid = optimalMidValue(prev, next);
90
+ expect(mid).toBeGreaterThan(prev);
91
+ expect(mid).toBeLessThan(next);
92
+ next = mid;
93
+ }
94
+ });
95
+ test('오른쪽에 반복 삽입 (next 쪽)', () => {
96
+ let prev = 1;
97
+ let next = 2;
98
+ for (let i = 0; i < 5; i++) {
99
+ const mid = optimalMidValue(prev, next);
100
+ expect(mid).toBeGreaterThan(prev);
101
+ expect(mid).toBeLessThan(next);
102
+ prev = mid;
103
+ }
104
+ });
105
+ });
106
+ // --- reorderBoard 서버 로직 시뮬레이션 ---
107
+ describe('reorderBoard 서버 로직', () => {
108
+ function simulateReorderBoard(boards, movedId, prevId, nextId) {
109
+ const prevBoard = prevId ? boards.find(b => b.id === prevId) : null;
110
+ const nextBoard = nextId ? boards.find(b => b.id === nextId) : null;
111
+ let prevOrder = prevBoard?.sortOrder ?? (nextBoard?.sortOrder != null ? nextBoard.sortOrder - 2 : 0);
112
+ let nextOrder = nextBoard?.sortOrder ?? (prevOrder != null ? prevOrder + 2 : 2);
113
+ // null이면 기본값으로 대체
114
+ if (prevOrder == null)
115
+ prevOrder = 0;
116
+ if (nextOrder == null)
117
+ nextOrder = prevOrder + 2;
118
+ // 간격이 너무 좁으면 전체 정규화
119
+ if (nextOrder - prevOrder < 0.001) {
120
+ boards.sort((a, b) => {
121
+ const sa = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
122
+ const sb = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
123
+ if (sa !== sb)
124
+ return sa - sb;
125
+ return (a.name || a.id).localeCompare(b.name || b.id);
126
+ });
127
+ boards.forEach((b, i) => { b.sortOrder = i; });
128
+ if (prevBoard)
129
+ prevOrder = prevBoard.sortOrder;
130
+ if (nextBoard) {
131
+ nextOrder = nextBoard.sortOrder;
132
+ }
133
+ else {
134
+ nextOrder = prevOrder + 2;
135
+ }
136
+ }
137
+ const sortOrder = optimalMidValue(prevOrder, nextOrder);
138
+ const moved = boards.find(b => b.id === movedId);
139
+ moved.sortOrder = sortOrder;
140
+ return boards.sort((a, b) => (a.sortOrder ?? Number.MAX_SAFE_INTEGER) - (b.sortOrder ?? Number.MAX_SAFE_INTEGER));
141
+ }
142
+ // --- 기본 이동 ---
143
+ test('중간으로 이동', () => {
144
+ const boards = [
145
+ { id: 'A', sortOrder: 1 },
146
+ { id: 'B', sortOrder: 5 },
147
+ { id: 'C', sortOrder: 3 }
148
+ ];
149
+ simulateReorderBoard(boards, 'B', 'A', 'C');
150
+ expect(boards.map(b => b.id)).toEqual(['A', 'B', 'C']);
151
+ expect(boards[1].sortOrder).toBe(2);
152
+ });
153
+ test('맨 앞으로 이동 (prevId 없음)', () => {
154
+ const boards = [
155
+ { id: 'A', sortOrder: 1 },
156
+ { id: 'B', sortOrder: 2 },
157
+ { id: 'C', sortOrder: 3 }
158
+ ];
159
+ simulateReorderBoard(boards, 'C', undefined, 'A');
160
+ expect(boards[0].id).toBe('C');
161
+ expect(boards[0].sortOrder).toBeLessThan(1);
162
+ });
163
+ test('맨 뒤로 이동 (nextId 없음)', () => {
164
+ const boards = [
165
+ { id: 'A', sortOrder: 1 },
166
+ { id: 'B', sortOrder: 2 },
167
+ { id: 'C', sortOrder: 3 }
168
+ ];
169
+ simulateReorderBoard(boards, 'A', 'C', undefined);
170
+ expect(boards[boards.length - 1].id).toBe('A');
171
+ expect(boards[boards.length - 1].sortOrder).toBeGreaterThan(3);
172
+ });
173
+ // --- 인접 이동 ---
174
+ test('바로 옆으로 이동 (A,B → B,A)', () => {
175
+ const boards = [
176
+ { id: 'A', sortOrder: 1 },
177
+ { id: 'B', sortOrder: 2 },
178
+ { id: 'C', sortOrder: 3 }
179
+ ];
180
+ simulateReorderBoard(boards, 'B', undefined, 'A');
181
+ expect(boards[0].id).toBe('B');
182
+ expect(boards[0].sortOrder).toBeLessThan(1);
183
+ });
184
+ test('인접한 두 보드 사이에 삽입', () => {
185
+ const boards = [
186
+ { id: 'A', sortOrder: 1 },
187
+ { id: 'B', sortOrder: 2 },
188
+ { id: 'C', sortOrder: 3 }
189
+ ];
190
+ simulateReorderBoard(boards, 'C', 'A', 'B');
191
+ expect(boards.map(b => b.id)).toEqual(['A', 'C', 'B']);
192
+ expect(boards[1].sortOrder).toBe(1.5);
193
+ });
194
+ // --- sortOrder 충돌/정규화 ---
195
+ test('prev와 next의 sortOrder가 같은 경우 → 정규화', () => {
196
+ const boards = [
197
+ { id: 'A', sortOrder: 5 },
198
+ { id: 'B', sortOrder: 5 },
199
+ { id: 'C', sortOrder: 10 }
200
+ ];
201
+ simulateReorderBoard(boards, 'C', 'A', 'B');
202
+ // 정규화 후 A=0, B=1, C=2 → C가 A(0)와 B(1) 사이 → 0.5
203
+ const sorted = boards.sort((a, b) => a.sortOrder - b.sortOrder);
204
+ expect(sorted.every((b, i) => i === 0 || sorted[i - 1].sortOrder <= b.sortOrder)).toBe(true);
205
+ });
206
+ test('모든 보드가 같은 sortOrder → 정규화', () => {
207
+ const boards = [
208
+ { id: 'A', sortOrder: 0 },
209
+ { id: 'B', sortOrder: 0 },
210
+ { id: 'C', sortOrder: 0 },
211
+ { id: 'D', sortOrder: 0 }
212
+ ];
213
+ simulateReorderBoard(boards, 'D', 'A', 'B');
214
+ const sorted = boards.sort((a, b) => a.sortOrder - b.sortOrder);
215
+ // 정규화 후 모든 보드가 서로 다른 sortOrder를 가짐
216
+ const uniqueOrders = new Set(sorted.map(b => b.sortOrder));
217
+ expect(uniqueOrders.size).toBe(4);
218
+ });
219
+ test('역전된 sortOrder → 정규화', () => {
220
+ const boards = [
221
+ { id: 'A', sortOrder: 10 },
222
+ { id: 'B', sortOrder: 5 },
223
+ { id: 'C', sortOrder: 1 }
224
+ ];
225
+ simulateReorderBoard(boards, 'A', 'C', 'B');
226
+ // 정규화 후 정상 순서
227
+ const sorted = boards.sort((a, b) => a.sortOrder - b.sortOrder);
228
+ expect(sorted.every((b, i) => i === 0 || sorted[i - 1].sortOrder <= b.sortOrder)).toBe(true);
229
+ });
230
+ // --- NULL sortOrder ---
231
+ test('NULL sortOrder 보드가 포함된 목록', () => {
232
+ const boards = [
233
+ { id: 'A', sortOrder: 1 },
234
+ { id: 'B', sortOrder: null },
235
+ { id: 'C', sortOrder: 2 }
236
+ ];
237
+ // A를 C 뒤로 이동 (nextId=B는 null)
238
+ // nextOrder = null → prevOrder + 2 = 4 → 정상 간격
239
+ simulateReorderBoard(boards, 'A', 'C', undefined);
240
+ expect(boards.find(b => b.id === 'A').sortOrder).toBeGreaterThan(2);
241
+ });
242
+ test('모두 NULL → 정규화 트리거', () => {
243
+ const boards = [
244
+ { id: 'A', sortOrder: null, name: 'Alpha' },
245
+ { id: 'B', sortOrder: null, name: 'Beta' },
246
+ { id: 'C', sortOrder: null, name: 'Gamma' }
247
+ ];
248
+ // prevId=A(null), nextId=B(null) → prevOrder=0, nextOrder=2 (기본값)
249
+ // 간격 2 > 0.001 → 정규화 안 됨, 그냥 중간값 계산
250
+ simulateReorderBoard(boards, 'C', 'A', 'B');
251
+ // C에 sortOrder가 부여됨
252
+ expect(boards.find(b => b.id === 'C').sortOrder).not.toBeNull();
253
+ });
254
+ test('NULL과 정상값 혼재 — 정규화 시 NULL은 맨 뒤', () => {
255
+ const boards = [
256
+ { id: 'A', sortOrder: 5 },
257
+ { id: 'B', sortOrder: 5 },
258
+ { id: 'C', sortOrder: null },
259
+ { id: 'D', sortOrder: null }
260
+ ];
261
+ // A(5)와 B(5) 사이: 간격 0 → 정규화 트리거
262
+ simulateReorderBoard(boards, 'D', 'A', 'B');
263
+ // 정규화 후: A, B는 앞쪽, C/D(null→맨뒤)는 뒤쪽에 배치된 후 정규화
264
+ const sorted = boards.sort((a, b) => (a.sortOrder ?? Number.MAX_SAFE_INTEGER) - (b.sortOrder ?? Number.MAX_SAFE_INTEGER));
265
+ // 모든 보드에 sortOrder가 부여됨 (정규화로 null 제거)
266
+ const nonNull = sorted.filter(b => b.sortOrder != null);
267
+ expect(nonNull.length).toBeGreaterThanOrEqual(3); // 최소 정규화된 보드 + moved
268
+ });
269
+ // --- 연속 재정렬 ---
270
+ test('5회 연속 재정렬 후 순서 유지', () => {
271
+ const boards = [
272
+ { id: 'A', sortOrder: 1 },
273
+ { id: 'B', sortOrder: 2 },
274
+ { id: 'C', sortOrder: 3 },
275
+ { id: 'D', sortOrder: 4 },
276
+ { id: 'E', sortOrder: 5 }
277
+ ];
278
+ simulateReorderBoard(boards, 'D', 'A', 'B'); // A, D, B, C, E
279
+ simulateReorderBoard(boards, 'E', 'A', 'D'); // A, E, D, B, C
280
+ const order = boards.map(b => b.id);
281
+ expect(order.indexOf('A')).toBeLessThan(order.indexOf('E'));
282
+ expect(order.indexOf('E')).toBeLessThan(order.indexOf('D'));
283
+ expect(order.indexOf('D')).toBeLessThan(order.indexOf('B'));
284
+ });
285
+ test('같은 위치에 반복 삽입', () => {
286
+ const boards = [
287
+ { id: 'A', sortOrder: 0 },
288
+ { id: 'B', sortOrder: 10 },
289
+ { id: 'C', sortOrder: 20 },
290
+ { id: 'D', sortOrder: 30 },
291
+ { id: 'E', sortOrder: 40 }
292
+ ];
293
+ // A와 B 사이에 C, D, E를 차례로 삽입
294
+ simulateReorderBoard(boards, 'C', 'A', 'B');
295
+ simulateReorderBoard(boards, 'D', 'A', 'C');
296
+ simulateReorderBoard(boards, 'E', 'A', 'D');
297
+ const order = boards.map(b => b.id);
298
+ expect(order.indexOf('A')).toBeLessThan(order.indexOf('E'));
299
+ expect(order.indexOf('E')).toBeLessThan(order.indexOf('D'));
300
+ expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
301
+ expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
302
+ });
303
+ test('왕복 이동 (A를 맨뒤로, 다시 맨앞으로)', () => {
304
+ const boards = [
305
+ { id: 'A', sortOrder: 1 },
306
+ { id: 'B', sortOrder: 2 },
307
+ { id: 'C', sortOrder: 3 }
308
+ ];
309
+ simulateReorderBoard(boards, 'A', 'C', undefined);
310
+ expect(boards[boards.length - 1].id).toBe('A');
311
+ simulateReorderBoard(boards, 'A', undefined, 'B');
312
+ expect(boards[0].id).toBe('A');
313
+ });
314
+ // --- 보드 2개만 있는 경우 ---
315
+ test('보드 2개 — 순서 교체', () => {
316
+ const boards = [
317
+ { id: 'A', sortOrder: 1 },
318
+ { id: 'B', sortOrder: 2 }
319
+ ];
320
+ simulateReorderBoard(boards, 'B', undefined, 'A');
321
+ expect(boards[0].id).toBe('B');
322
+ expect(boards[1].id).toBe('A');
323
+ });
324
+ test('보드 1개 — 이동 무의미하지만 에러 없음', () => {
325
+ const boards = [
326
+ { id: 'A', sortOrder: 1 }
327
+ ];
328
+ simulateReorderBoard(boards, 'A', undefined, undefined);
329
+ expect(boards[0].id).toBe('A');
330
+ });
331
+ // --- 큰 목록 ---
332
+ test('100개 보드에서 재정렬', () => {
333
+ const boards = Array.from({ length: 100 }, (_, i) => ({
334
+ id: `B${i}`,
335
+ sortOrder: i
336
+ }));
337
+ // B99를 B0과 B1 사이로
338
+ simulateReorderBoard(boards, 'B99', 'B0', 'B1');
339
+ const sorted = boards.sort((a, b) => a.sortOrder - b.sortOrder);
340
+ const b99Index = sorted.findIndex(b => b.id === 'B99');
341
+ const b0Index = sorted.findIndex(b => b.id === 'B0');
342
+ const b1Index = sorted.findIndex(b => b.id === 'B1');
343
+ expect(b99Index).toBe(b0Index + 1);
344
+ expect(b1Index).toBe(b99Index + 1);
345
+ });
346
+ });
347
+ //# sourceMappingURL=board-reorder.test.js.map