@things-factory/board-service 10.1.4 → 10.1.6

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.
@@ -19,12 +19,18 @@ const pdfToImage = async ({ pdfPath, fileName, extension = 'png', quality = 2, d
19
19
  ]
20
20
  });
21
21
  const browser = await pool.acquire();
22
+ let page;
22
23
  try {
23
24
  const protocol = 'http';
24
25
  const host = 'localhost';
25
26
  const port = process.env.PORT;
26
27
  const pdfUrl = `${protocol}://${host}:${port}${pdfPath}`;
27
- const page = await browser.newPage();
28
+ page = await browser.newPage();
29
+ // 풀에서 꺼낸 브라우저는 launch 옵션(defaultViewport)을 호출별로 줄 수 없으므로 page 단위로 적용.
30
+ // (헤드리스풀 통합 전에는 puppeteer.launch({ defaultViewport }) 로 전달되던 값 — 미적용 시 800x600 고정)
31
+ if (defaultViewport) {
32
+ await page.setViewport(defaultViewport);
33
+ }
28
34
  const html = await ejs.render(getPdfHtmlTemplate(), {
29
35
  data: { pdfUrl, quality },
30
36
  nonce: crypto_1.default.randomBytes(16).toString('hex')
@@ -42,7 +48,6 @@ const pdfToImage = async ({ pdfPath, fileName, extension = 'png', quality = 2, d
42
48
  const stream = new Readable();
43
49
  stream.push(screenshot);
44
50
  stream.push(null);
45
- await pool.release(browser);
46
51
  // file upload 형태로 return
47
52
  return {
48
53
  filename: `${fileName}.${extension}`,
@@ -52,10 +57,14 @@ const pdfToImage = async ({ pdfPath, fileName, extension = 'png', quality = 2, d
52
57
  };
53
58
  }
54
59
  catch (e) {
55
- await pool.release(browser);
56
60
  console.error('Error creating thumbnail', e);
57
61
  throw new Error('Error creating thumbnail');
58
62
  }
63
+ finally {
64
+ // 브라우저는 풀에 반환되어 재사용되므로 page 를 닫지 않으면 호출마다 탭이 누적된다.
65
+ await page?.close().catch(() => { });
66
+ await pool.release(browser);
67
+ }
59
68
  };
60
69
  exports.pdfToImage = pdfToImage;
61
70
  function getPdfHtmlTemplate() {
@@ -1 +1 @@
1
- {"version":3,"file":"headless-pdf-to-image.js","sourceRoot":"","sources":["../../server/controllers/headless-pdf-to-image.ts"],"names":[],"mappings":";;;;AAAA,4DAA2B;AAC3B,iDAA+D;AAE/D,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AACtC,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;AAEnB,MAAM,UAAU,GAAG,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,GAAG,KAAK,EAAE,OAAO,GAAG,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,EAAE,EAAE;IAChH,uDAAuD;IACvD,MAAM,IAAI,GAAG,IAAA,+BAAuB,EAAC,WAAW,EAAE;QAChD,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,CAAC;QACN,IAAI,EAAE;YACJ,wBAAwB;YACxB,mCAAmC;YACnC,iCAAiC;YACjC,cAAc;SACf;KACF,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;IAEpC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,CAAA;QACvB,MAAM,IAAI,GAAG,WAAW,CAAA;QACxB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAA;QAC7B,MAAM,MAAM,GAAG,GAAG,QAAQ,MAAM,IAAI,IAAI,IAAI,GAAG,OAAO,EAAE,CAAA;QAExD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;QACpC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,EAAE;YAClD,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;YACzB,KAAK,EAAE,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;SAC9C,CAAC,CAAA;QAEF,yEAAyE;QACzE,kCAAkC;QAClC,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;QAClD,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAA;QAC/B,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACrB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC;YACvC,IAAI,EAAE,SAAS;YACf,cAAc,EAAE,IAAI;SACrB,CAAC,CAAA;QAEF,6CAA6C;QAC7C,MAAM,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAA;QAC7B,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACvB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAEjB,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAE3B,yBAAyB;QACzB,OAAO;YACL,QAAQ,EAAE,GAAG,QAAQ,IAAI,SAAS,EAAE;YACpC,QAAQ,EAAE,SAAS,SAAS,EAAE;YAC9B,QAAQ,EAAE,MAAM;YAChB,gBAAgB,EAAE,GAAG,EAAE,CAAC,MAAM;SAC/B,CAAA;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAC3B,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,CAAC,CAAC,CAAA;QAC5C,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAA;IAC7C,CAAC;AACH,CAAC,CAAA;AAxDY,QAAA,UAAU,cAwDtB;AAED,SAAS,kBAAkB;IACzB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CN,CAAA;AACH,CAAC","sourcesContent":["import crypto from 'crypto'\nimport { getOrCreateHeadlessPool } from '@things-factory/shell'\n\nconst { Readable } = require('stream')\nconst ejs = require('ejs')\n\nexport const pdfToImage = async ({ pdfPath, fileName, extension = 'png', quality = 2, defaultViewport = null }) => {\n // Use headless pool instead of direct puppeteer.launch\n const pool = getOrCreateHeadlessPool('board-pdf', {\n min: 1,\n max: 3,\n args: [\n '--disable-web-security',\n '--disable-features=IsolateOrigins', \n '--disable-site-isolation-trials',\n '--no-sandbox'\n ]\n })\n\n const browser = await pool.acquire()\n\n try {\n const protocol = 'http'\n const host = 'localhost'\n const port = process.env.PORT\n const pdfUrl = `${protocol}://${host}:${port}${pdfPath}`\n\n const page = await browser.newPage()\n const html = await ejs.render(getPdfHtmlTemplate(), {\n data: { pdfUrl, quality },\n nonce: crypto.randomBytes(16).toString('hex')\n })\n\n // setContent 의 waitUntil 은 'load'/'domcontentloaded' 만 지원 → API/fetch 응답\n // 정착은 waitForNetworkIdle 로 별도 대기.\n await page.setContent(html, { waitUntil: 'load' })\n await page.waitForNetworkIdle()\n await page.$('#page')\n const screenshot = await page.screenshot({\n type: extension,\n omitBackground: true\n })\n\n // graphql fileupload형태로 return을 위해 stream 생성\n const stream = new Readable()\n stream.push(screenshot)\n stream.push(null)\n\n await pool.release(browser)\n\n // file upload 형태로 return\n return {\n filename: `${fileName}.${extension}`,\n mimetype: `image/${extension}`,\n encoding: '7bit',\n createReadStream: () => stream\n }\n } catch (e) {\n await pool.release(browser)\n console.error('Error creating thumbnail', e)\n throw new Error('Error creating thumbnail')\n }\n}\n\nfunction getPdfHtmlTemplate() {\n return `\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" />\n\n <style nonce=\"<%= nonce %>\">\n body {\n width: 100vw;\n height: 100vh;\n margin: 0;\n }\n #page {\n display: flex;\n width: 100%;\n height: 100%;\n }\n </style>\n\n </head>\n <body>\n <canvas id=\"page\"></canvas>\n <script src=\"https://unpkg.com/pdfjs-dist@2.0.489/build/pdf.min.js\"></script>\n <script nonce=\"<%= nonce %>\">\n ;(async () => {\n const pdf = await pdfjsLib.getDocument('<%= data.pdfUrl %>')\n const page = await pdf.getPage(1)\n const viewport = page.getViewport('<%= data.quality %>')\n const canvas = document.getElementById('page')\n const context = canvas.getContext('2d')\n\n canvas.height = viewport.height\n canvas.width = viewport.width\n\n const renderContext = {\n canvasContext: context,\n viewport: viewport\n }\n\n page.render(renderContext)\n })()\n </script>\n </body>\n </html>\n `\n}\n"]}
1
+ {"version":3,"file":"headless-pdf-to-image.js","sourceRoot":"","sources":["../../server/controllers/headless-pdf-to-image.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from 'crypto'\nimport { getOrCreateHeadlessPool } from '@things-factory/shell'\n\nconst { Readable } = require('stream')\nconst ejs = require('ejs')\n\nexport const pdfToImage = async ({ pdfPath, fileName, extension = 'png', quality = 2, defaultViewport = null }) => {\n // Use headless pool instead of direct puppeteer.launch\n const pool = getOrCreateHeadlessPool('board-pdf', {\n min: 1,\n max: 3,\n args: [\n '--disable-web-security',\n '--disable-features=IsolateOrigins', \n '--disable-site-isolation-trials',\n '--no-sandbox'\n ]\n })\n\n const browser = await pool.acquire()\n let page\n\n try {\n const protocol = 'http'\n const host = 'localhost'\n const port = process.env.PORT\n const pdfUrl = `${protocol}://${host}:${port}${pdfPath}`\n\n page = await browser.newPage()\n // 풀에서 꺼낸 브라우저는 launch 옵션(defaultViewport)을 호출별로 줄 수 없으므로 page 단위로 적용.\n // (헤드리스풀 통합 전에는 puppeteer.launch({ defaultViewport }) 로 전달되던 값 — 미적용 시 800x600 고정)\n if (defaultViewport) {\n await page.setViewport(defaultViewport)\n }\n const html = await ejs.render(getPdfHtmlTemplate(), {\n data: { pdfUrl, quality },\n nonce: crypto.randomBytes(16).toString('hex')\n })\n\n // setContent 의 waitUntil 은 'load'/'domcontentloaded' 만 지원 → API/fetch 응답\n // 정착은 waitForNetworkIdle 로 별도 대기.\n await page.setContent(html, { waitUntil: 'load' })\n await page.waitForNetworkIdle()\n await page.$('#page')\n const screenshot = await page.screenshot({\n type: extension,\n omitBackground: true\n })\n\n // graphql fileupload형태로 return을 위해 stream 생성\n const stream = new Readable()\n stream.push(screenshot)\n stream.push(null)\n\n // file upload 형태로 return\n return {\n filename: `${fileName}.${extension}`,\n mimetype: `image/${extension}`,\n encoding: '7bit',\n createReadStream: () => stream\n }\n } catch (e) {\n console.error('Error creating thumbnail', e)\n throw new Error('Error creating thumbnail')\n } finally {\n // 브라우저는 풀에 반환되어 재사용되므로 page 를 닫지 않으면 호출마다 탭이 누적된다.\n await page?.close().catch(() => {})\n await pool.release(browser)\n }\n}\n\nfunction getPdfHtmlTemplate() {\n return `\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" />\n\n <style nonce=\"<%= nonce %>\">\n body {\n width: 100vw;\n height: 100vh;\n margin: 0;\n }\n #page {\n display: flex;\n width: 100%;\n height: 100%;\n }\n </style>\n\n </head>\n <body>\n <canvas id=\"page\"></canvas>\n <script src=\"https://unpkg.com/pdfjs-dist@2.0.489/build/pdf.min.js\"></script>\n <script nonce=\"<%= nonce %>\">\n ;(async () => {\n const pdf = await pdfjsLib.getDocument('<%= data.pdfUrl %>')\n const page = await pdf.getPage(1)\n const viewport = page.getViewport('<%= data.quality %>')\n const canvas = document.getElementById('page')\n const context = canvas.getContext('2d')\n\n canvas.height = viewport.height\n canvas.width = viewport.width\n\n const renderContext = {\n canvasContext: context,\n viewport: viewport\n }\n\n page.render(renderContext)\n })()\n </script>\n </body>\n </html>\n `\n}\n"]}
@@ -14,16 +14,30 @@ describe('board-version-diff (pure)', () => {
14
14
  ]
15
15
  };
16
16
  const flat = (0, board_version_diff_1.flattenBoardComponents)(model);
17
- expect(flat.map(f => f.refid)).toEqual([1, 2, 3]);
17
+ /*
18
+ * 맨 앞은 **씬 자체**(`__root__`)다. 배경색·조명·scale 만 바뀐 릴리즈가 「차이 없음」으로
19
+ * 나오던 버그를 고치면서 들어갔고(§`flattenBoardComponents`), 이 시험이 안 따라와 있었다
20
+ * (2026-09-10 에 맞췄다). 루트를 빼면 그 버그가 조용히 돌아온다.
21
+ */
22
+ expect(flat[0].key).toBe('__root__');
23
+ expect(flat.map(f => f.refid)).toEqual([null, 1, 2, 3]);
18
24
  });
19
25
  it('parses a JSON string model', () => {
20
26
  const model = JSON.stringify({ components: [{ refid: 1, type: 'rect' }] });
21
- expect((0, board_version_diff_1.flattenBoardComponents)(model).map(f => f.refid)).toEqual([1]);
27
+ expect((0, board_version_diff_1.flattenBoardComponents)(model).map(f => f.refid)).toEqual([null, 1]);
22
28
  });
23
29
  it('returns [] for null/invalid model', () => {
24
30
  expect((0, board_version_diff_1.flattenBoardComponents)(null)).toEqual([]);
25
31
  expect((0, board_version_diff_1.flattenBoardComponents)('not-json')).toEqual([]);
26
- expect((0, board_version_diff_1.flattenBoardComponents)({})).toEqual([]);
32
+ });
33
+ it('an empty object is an empty scene, not an invalid model', () => {
34
+ /*
35
+ * `{}` 가 위 묶음에 함께 있었다. 무효한 입력이 아니라 **컴포넌트가 없는 씬**이고, 그것이
36
+ * 갈려야 배경색만 바뀐 릴리즈가 차이로 잡힌다 — `__root__` 항목이 있는 이유가 그것이다.
37
+ * 여기서 `[]` 를 기대하면 그 버그가 시험으로 고정된다.
38
+ */
39
+ const flat = (0, board_version_diff_1.flattenBoardComponents)({});
40
+ expect(flat.map(f => f.key)).toEqual(['__root__']);
27
41
  });
28
42
  });
29
43
  describe('diffBoardModels', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"board-version-diff.test.js","sourceRoot":"","sources":["../../../server/service/board/board-version-diff.test.ts"],"names":[],"mappings":";;AAAA;;GAEG;AACH,6DAA8E;AAE9E,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACtC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,KAAK,GAAG;gBACZ,UAAU,EAAE;oBACV,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;oBAC1B,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE;iBACtE;aACF,CAAA;YACD,MAAM,IAAI,GAAG,IAAA,2CAAsB,EAAC,KAAK,CAAC,CAAA;YAC1C,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;YAC1E,MAAM,CAAC,IAAA,2CAAsB,EAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACtE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC3C,MAAM,CAAC,IAAA,2CAAsB,EAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YAChD,MAAM,CAAC,IAAA,2CAAsB,EAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACtD,MAAM,CAAC,IAAA,2CAAsB,EAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAChD,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;YAClE,MAAM,IAAI,GAAG;gBACX,UAAU,EAAE;oBACV,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE;oBACnC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;iBAC1C;aACF,CAAA;YACD,MAAM,MAAM,GAAG;gBACb,UAAU,EAAE;oBACV,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,kBAAkB;oBACxD,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,QAAQ;oBACrC,kBAAkB;iBACnB;aACF,CAAA;YACD,MAAM,KAAK,GAAG,IAAA,oCAAe,EAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YAC3C,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAA;YAE5F,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACpC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACtC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAEvC,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAA;YAC/C,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;YAC/C,MAAM,KAAK,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,CAAA;YACpE,MAAM,CAAC,IAAA,oCAAe,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAC/E,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;YACzE,MAAM,IAAI,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;YACpG,0DAA0D;YAC1D,MAAM,MAAM,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,EAAE,CAAA;YAC5E,MAAM,KAAK,GAAG,IAAA,oCAAe,EAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YAC3C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;YACtD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACpE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;YAC5D,MAAM,IAAI,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;YACxD,MAAM,MAAM,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,CAAA;YAC3D,MAAM,KAAK,GAAG,IAAA,oCAAe,EAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YAC3C,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;YAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;YAC5C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QACjD,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["/**\n * board-version-diff — 보드 model(things-scene 컴포넌트 트리) diff 순수 로직 단위 검증.\n */\nimport { diffBoardModels, flattenBoardComponents } from './board-version-diff'\n\ndescribe('board-version-diff (pure)', () => {\n describe('flattenBoardComponents', () => {\n it('flattens nested components recursively', () => {\n const model = {\n components: [\n { refid: 1, type: 'rect' },\n { refid: 2, type: 'group', components: [{ refid: 3, type: 'text' }] }\n ]\n }\n const flat = flattenBoardComponents(model)\n expect(flat.map(f => f.refid)).toEqual([1, 2, 3])\n })\n\n it('parses a JSON string model', () => {\n const model = JSON.stringify({ components: [{ refid: 1, type: 'rect' }] })\n expect(flattenBoardComponents(model).map(f => f.refid)).toEqual([1])\n })\n\n it('returns [] for null/invalid model', () => {\n expect(flattenBoardComponents(null)).toEqual([])\n expect(flattenBoardComponents('not-json')).toEqual([])\n expect(flattenBoardComponents({})).toEqual([])\n })\n })\n\n describe('diffBoardModels', () => {\n it('detects added, removed, and modified components by refid', () => {\n const base = {\n components: [\n { refid: 1, type: 'rect', left: 0 },\n { refid: 2, type: 'text', text: 'hello' }\n ]\n }\n const target = {\n components: [\n { refid: 1, type: 'rect', left: 50 }, // modified (left)\n { refid: 3, type: 'circle' } // added\n // refid 2 removed\n ]\n }\n const diffs = diffBoardModels(base, target)\n const byType = (t: string) => diffs.filter(d => d.changeType === t).map(d => d.refid).sort()\n\n expect(byType('added')).toEqual([3])\n expect(byType('removed')).toEqual([2])\n expect(byType('modified')).toEqual([1])\n\n const modified = diffs.find(d => d.refid === 1)\n expect(modified?.changedProps).toContain('left')\n })\n\n it('reports no diffs for identical models', () => {\n const model = { components: [{ refid: 1, type: 'rect', left: 10 }] }\n expect(diffBoardModels(model, JSON.parse(JSON.stringify(model)))).toEqual([])\n })\n\n it('ignores children array when diffing a container component props', () => {\n const base = { components: [{ refid: 1, type: 'group', components: [{ refid: 2, type: 'text' }] }] }\n // 자식만 바뀌고 컨테이너 자체 속성은 동일 → 컨테이너는 modified 아님, 자식만 removed\n const target = { components: [{ refid: 1, type: 'group', components: [] }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs.find(d => d.refid === 1)).toBeUndefined()\n expect(diffs.find(d => d.refid === 2)?.changeType).toBe('removed')\n })\n\n it('falls back to positional key when refid is missing', () => {\n const base = { components: [{ type: 'rect', left: 0 }] }\n const target = { components: [{ type: 'rect', left: 99 }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs).toHaveLength(1)\n expect(diffs[0].changeType).toBe('modified')\n expect(diffs[0].changedProps).toContain('left')\n })\n })\n})\n"]}
1
+ {"version":3,"file":"board-version-diff.test.js","sourceRoot":"","sources":["../../../server/service/board/board-version-diff.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-version-diff — 보드 model(things-scene 컴포넌트 트리) diff 순수 로직 단위 검증.\n */\nimport { diffBoardModels, flattenBoardComponents } from './board-version-diff'\n\ndescribe('board-version-diff (pure)', () => {\n describe('flattenBoardComponents', () => {\n it('flattens nested components recursively', () => {\n const model = {\n components: [\n { refid: 1, type: 'rect' },\n { refid: 2, type: 'group', components: [{ refid: 3, type: 'text' }] }\n ]\n }\n const flat = flattenBoardComponents(model)\n /*\n * 맨 앞은 **씬 자체**(`__root__`)다. 배경색·조명·scale 만 바뀐 릴리즈가 「차이 없음」으로\n * 나오던 버그를 고치면서 들어갔고(§`flattenBoardComponents`), 이 시험이 안 따라와 있었다\n * (2026-09-10 에 맞췄다). 루트를 빼면 그 버그가 조용히 돌아온다.\n */\n expect(flat[0].key).toBe('__root__')\n expect(flat.map(f => f.refid)).toEqual([null, 1, 2, 3])\n })\n\n it('parses a JSON string model', () => {\n const model = JSON.stringify({ components: [{ refid: 1, type: 'rect' }] })\n expect(flattenBoardComponents(model).map(f => f.refid)).toEqual([null, 1])\n })\n\n it('returns [] for null/invalid model', () => {\n expect(flattenBoardComponents(null)).toEqual([])\n expect(flattenBoardComponents('not-json')).toEqual([])\n })\n\n it('an empty object is an empty scene, not an invalid model', () => {\n /*\n * `{}` 가 위 묶음에 함께 있었다. 무효한 입력이 아니라 **컴포넌트가 없는 씬**이고, 그것이\n * 갈려야 배경색만 바뀐 릴리즈가 차이로 잡힌다 — `__root__` 항목이 있는 이유가 그것이다.\n * 여기서 `[]` 를 기대하면 그 버그가 시험으로 고정된다.\n */\n const flat = flattenBoardComponents({})\n expect(flat.map(f => f.key)).toEqual(['__root__'])\n })\n })\n\n describe('diffBoardModels', () => {\n it('detects added, removed, and modified components by refid', () => {\n const base = {\n components: [\n { refid: 1, type: 'rect', left: 0 },\n { refid: 2, type: 'text', text: 'hello' }\n ]\n }\n const target = {\n components: [\n { refid: 1, type: 'rect', left: 50 }, // modified (left)\n { refid: 3, type: 'circle' } // added\n // refid 2 removed\n ]\n }\n const diffs = diffBoardModels(base, target)\n const byType = (t: string) => diffs.filter(d => d.changeType === t).map(d => d.refid).sort()\n\n expect(byType('added')).toEqual([3])\n expect(byType('removed')).toEqual([2])\n expect(byType('modified')).toEqual([1])\n\n const modified = diffs.find(d => d.refid === 1)\n expect(modified?.changedProps).toContain('left')\n })\n\n it('reports no diffs for identical models', () => {\n const model = { components: [{ refid: 1, type: 'rect', left: 10 }] }\n expect(diffBoardModels(model, JSON.parse(JSON.stringify(model)))).toEqual([])\n })\n\n it('ignores children array when diffing a container component props', () => {\n const base = { components: [{ refid: 1, type: 'group', components: [{ refid: 2, type: 'text' }] }] }\n // 자식만 바뀌고 컨테이너 자체 속성은 동일 → 컨테이너는 modified 아님, 자식만 removed\n const target = { components: [{ refid: 1, type: 'group', components: [] }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs.find(d => d.refid === 1)).toBeUndefined()\n expect(diffs.find(d => d.refid === 2)?.changeType).toBe('removed')\n })\n\n it('falls back to positional key when refid is missing', () => {\n const base = { components: [{ type: 'rect', left: 0 }] }\n const target = { components: [{ type: 'rect', left: 99 }] }\n const diffs = diffBoardModels(base, target)\n expect(diffs).toHaveLength(1)\n expect(diffs[0].changeType).toBe('modified')\n expect(diffs[0].changedProps).toContain('left')\n })\n })\n})\n"]}