@dssp/supervision 1.0.20 → 1.0.21

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.
@@ -133,6 +133,7 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
133
133
  private _runThicknessMeasure;
134
134
  private _applyMeasurement;
135
135
  private _dataUrlToFile;
136
+ private _saveJudgments;
136
137
  private _registerInspection;
137
138
  }
138
139
  export {};
@@ -37,6 +37,22 @@ const SET_FILES = ['video.mov', 'poses.jsonl', 'meta.json'];
37
37
  function appCaptureLink(tech) {
38
38
  return `gamri://${tech}?server=${encodeURIComponent(MEASURE_BASE_URL)}`;
39
39
  }
40
+ /* 한 번의 /inspect 에 보낼 검사항목 수. 엔진이 8개까지만 병렬 처리하고, 그 앞단
41
+ Cloudflare 가 100초에서 끊기 때문에(524) 항목이 많으면 나눠 보내야 한다. */
42
+ const INSPECT_BATCH = 8;
43
+ /* 나눠 호출한 /inspect 결과 합치기 — 항목 배열만 이어 붙이고 요약은 다시 센다 */
44
+ function _mergeInspectResults(a, b) {
45
+ const items = [...(a.items || []), ...(b.items || [])];
46
+ const itemsSorted = a.items_sorted || b.items_sorted ? [...(a.items_sorted || a.items || []), ...(b.items_sorted || b.items || [])] : undefined;
47
+ const summary = { pass: 0, fail: 0, hold: 0, pending: 0, error: 0, measure: 0 };
48
+ for (const it of items) {
49
+ summary[it.verdict] = (summary[it.verdict] || 0) + 1;
50
+ if (it.verdict === 'pending' && it.pending_reason === 'measure')
51
+ summary.measure += 1;
52
+ }
53
+ summary.total = items.length;
54
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, a), b), { items }), (itemsSorted ? { items_sorted: itemsSorted } : {})), { summary });
55
+ }
40
56
  /* KCS 기준은 매칭 결과 JSON(kcs_matches[0])으로 저장돼 있다 → 엔진에 보낼 조문 텍스트로 편다.
41
57
  문서/조항 라벨을 앞에 붙여야 모델이 어느 기준을 인용하는지 reason 에 쓸 수 있다. */
42
58
  function _kcsCriteriaText(raw) {
@@ -1149,17 +1165,31 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1149
1165
  });
1150
1166
  }
1151
1167
  }
1152
- const input = { action: 'inspect', checklist, input_kind: 'video_frames' };
1153
1168
  // 촬영 배정 되돌리기 — 이걸 안 보내면 모든 항목이 모든 사진을 보게 되어
1154
1169
  // "타설 후 항목이 배근 사진으로 판정되는" 오판이 난다 (엔진 실증에서 실제 발생).
1155
1170
  // 배정된 촬영이 안 올라온 항목은 엔진이 hold 로 둔다 — 남의 사진으로 판정하지 않게.
1156
1171
  const shotItems = this._shotItemsMap();
1157
- if (!isRecapture && shotItems && imageShots.length === files.length) {
1158
- input.image_shots = imageShots.join(',');
1159
- input.shot_items = JSON.stringify(shotItems);
1172
+ const scoping = !isRecapture && shotItems && imageShots.length === files.length;
1173
+ // 항목을 나눠 여러 번 호출한다. 엔진이 한 번에 8개까지만 병렬 처리하는데,
1174
+ // 그 앞단 Cloudflare 가 100초에서 연결을 끊어 항목이 많으면 524 가 난다.
1175
+ const batches = [];
1176
+ for (let i = 0; i < checklist.length; i += INSPECT_BATCH) {
1177
+ batches.push(checklist.slice(i, i + INSPECT_BATCH));
1178
+ }
1179
+ let result = null;
1180
+ for (let b = 0; b < batches.length; b++) {
1181
+ this.loadingText =
1182
+ batches.length > 1
1183
+ ? `AI 자동 판정 중... (${b + 1}/${batches.length} 묶음, 항목 ${batches[b].length}개)`
1184
+ : 'AI 자동 판정 중... (최대 1~2분 소요)';
1185
+ const input = { action: 'inspect', checklist: batches[b], input_kind: 'video_frames' };
1186
+ if (scoping) {
1187
+ input.image_shots = imageShots.join(',');
1188
+ input.shot_items = JSON.stringify(shotItems);
1189
+ }
1190
+ const part = await this._callEngine(input, files);
1191
+ result = result ? _mergeInspectResults(result, part) : part;
1160
1192
  }
1161
- this.loadingText = 'AI 자동 판정 중... (최대 1~2분 소요)';
1162
- const result = await this._callEngine(input, files);
1163
1193
  if (isRecapture) {
1164
1194
  this.photos = [...this.photos, ...newPhotos];
1165
1195
  this._mergeItems((result === null || result === void 0 ? void 0 : result.items_sorted) || (result === null || result === void 0 ? void 0 : result.items) || [], this.recaptureBase);
@@ -1441,6 +1471,9 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1441
1471
  });
1442
1472
  const judged = Object.assign(Object.assign({}, item), result.item);
1443
1473
  this._mergeItems([judged]);
1474
+ // 확정 즉시 체크리스트에 기록한다. 「검사 등록」까지 미루면 그 사이에 화면을 벗어나거나
1475
+ // 재판정이 일어났을 때 측정 판정만 유실된다 (VLM 항목은 inspect 결과에 남아 있어 티가 안 남).
1476
+ await this._saveJudgments([judged]);
1444
1477
  this._resetMeasureState();
1445
1478
  this.panel = 'none';
1446
1479
  this.selectedItem = null;
@@ -1475,9 +1508,33 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1475
1508
  return null;
1476
1509
  }
1477
1510
  }
1511
+ /* 적합/부적합으로 판정된 항목을 체크리스트의 감리자 확인란에 기록한다.
1512
+ 보류·미지원·오류는 미검사로 남긴다(자동으로 부적합 처리하지 않는다). */
1513
+ async _saveJudgments(items) {
1514
+ var _a;
1515
+ const judged = items.filter(it => it.verdict === 'pass' || it.verdict === 'fail');
1516
+ if (!judged.length)
1517
+ return 0;
1518
+ const response = await client.mutate({
1519
+ mutation: gql `
1520
+ mutation UpdateChecklistItemsAiJudgment($items: [ChecklistItemAiJudgment!]!) {
1521
+ updateChecklistItemsAiJudgment(items: $items)
1522
+ }
1523
+ `,
1524
+ variables: {
1525
+ items: judged.map(it => ({
1526
+ id: it.item_id,
1527
+ supervisoryConfirmStatus: it.verdict === 'pass' ? 'T' : 'F'
1528
+ }))
1529
+ }
1530
+ });
1531
+ if (response.errors)
1532
+ throw new Error(((_a = response.errors[0]) === null || _a === void 0 ? void 0 : _a.message) || '검사 결과 저장에 실패했습니다.');
1533
+ return judged.length;
1534
+ }
1478
1535
  /* ─── 검사 등록: 감리자 판정 자동 반영 + AI판단 근거 첨부 ─── */
1479
1536
  async _registerInspection() {
1480
- var _a, _b;
1537
+ var _a;
1481
1538
  const items = this._sortedItems();
1482
1539
  if (!items.length) {
1483
1540
  notify({ message: '판정 결과가 없습니다.', level: 'warn' });
@@ -1489,23 +1546,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1489
1546
  this.loading = true;
1490
1547
  this.loadingText = '검사 결과 등록 중...';
1491
1548
  // 1) 감리자 적합/부적합 자동 채움 (적합/부적합 판정된 항목만, 없으면 건너뜀)
1492
- if (judged.length) {
1493
- const response = await client.mutate({
1494
- mutation: gql `
1495
- mutation UpdateChecklistItemsAiJudgment($items: [ChecklistItemAiJudgment!]!) {
1496
- updateChecklistItemsAiJudgment(items: $items)
1497
- }
1498
- `,
1499
- variables: {
1500
- items: judged.map(it => ({
1501
- id: it.item_id,
1502
- supervisoryConfirmStatus: it.verdict === 'pass' ? 'T' : 'F'
1503
- }))
1504
- }
1505
- });
1506
- if (response.errors)
1507
- throw new Error(((_a = response.errors[0]) === null || _a === void 0 ? void 0 : _a.message) || '검사 등록에 실패했습니다.');
1508
- }
1549
+ await this._saveJudgments(items);
1509
1550
  // 2) 항목별 첨부: 판단 근거(AI판단 텍스트) + 근거 프레임/측정 근거 이미지
1510
1551
  // ai_report는 pending/error에서 빈 문자열로 오므로 그 항목은 첨부하지 않는다
1511
1552
  const attachments = [];
@@ -1544,7 +1585,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1544
1585
  context: { hasUpload: true }
1545
1586
  });
1546
1587
  if (attachResponse.errors)
1547
- throw new Error(((_b = attachResponse.errors[0]) === null || _b === void 0 ? void 0 : _b.message) || 'AI판단 자료 첨부에 실패했습니다.');
1588
+ throw new Error(((_a = attachResponse.errors[0]) === null || _a === void 0 ? void 0 : _a.message) || 'AI판단 자료 첨부에 실패했습니다.');
1548
1589
  }
1549
1590
  notify({ message: `검사 결과를 등록하였습니다. (자동 판정 ${judged.length}건 반영)` });
1550
1591
  // 등록 후 체크리스트 화면으로 이동