@dssp/supervision 1.0.19 → 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.
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.d.ts +5 -0
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js +156 -28
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
|
|
|
77
77
|
item_id: any;
|
|
78
78
|
category: string;
|
|
79
79
|
text: any;
|
|
80
|
+
criteria_kcs: string;
|
|
81
|
+
criteria_site: any;
|
|
80
82
|
}[];
|
|
81
83
|
get uploadedSets(): SetInfo[];
|
|
82
84
|
render(): import("lit-html").TemplateResult<1>;
|
|
@@ -99,6 +101,8 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
|
|
|
99
101
|
private _callMeasure;
|
|
100
102
|
private _fetchMeasureImage;
|
|
101
103
|
private _loadShotGuide;
|
|
104
|
+
private _shotItemsMap;
|
|
105
|
+
private _itemsAwaitingShot;
|
|
102
106
|
private _shotTag;
|
|
103
107
|
private _shotTechnique;
|
|
104
108
|
private _shotAppLink;
|
|
@@ -129,6 +133,7 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
|
|
|
129
133
|
private _runThicknessMeasure;
|
|
130
134
|
private _applyMeasurement;
|
|
131
135
|
private _dataUrlToFile;
|
|
136
|
+
private _saveJudgments;
|
|
132
137
|
private _registerInspection;
|
|
133
138
|
}
|
|
134
139
|
export {};
|
|
@@ -37,6 +37,37 @@ 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
|
+
}
|
|
56
|
+
/* KCS 기준은 매칭 결과 JSON(kcs_matches[0])으로 저장돼 있다 → 엔진에 보낼 조문 텍스트로 편다.
|
|
57
|
+
문서/조항 라벨을 앞에 붙여야 모델이 어느 기준을 인용하는지 reason 에 쓸 수 있다. */
|
|
58
|
+
function _kcsCriteriaText(raw) {
|
|
59
|
+
if (!raw)
|
|
60
|
+
return '';
|
|
61
|
+
try {
|
|
62
|
+
const kcs = JSON.parse(raw);
|
|
63
|
+
const label = [kcs.file_label, kcs.clause_no, kcs.clause_title].filter(Boolean).join(' ');
|
|
64
|
+
const body = (kcs.matched_item || kcs.text || '').trim();
|
|
65
|
+
return body ? `${label}\n${body}`.trim() : label;
|
|
66
|
+
}
|
|
67
|
+
catch (_a) {
|
|
68
|
+
return raw.trim(); // 구형 데이터(문자열 그대로 저장된 경우)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
40
71
|
/* set_id(YYYYmmdd-HHMMSS_xxxxxx) → "9/11 14:30" 표기 */
|
|
41
72
|
function _setTimeLabel(setId) {
|
|
42
73
|
const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/.exec(setId || '');
|
|
@@ -158,7 +189,11 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
158
189
|
.map(item => ({
|
|
159
190
|
item_id: item.id,
|
|
160
191
|
category: [item.mainType, item.detailType].filter(Boolean).join(' · '),
|
|
161
|
-
text: item.name || ''
|
|
192
|
+
text: item.name || '',
|
|
193
|
+
// 검사 기준은 조문 '번호'가 아니라 본문을 보내야 판정 근거가 실제 기준에 붙는다.
|
|
194
|
+
// (조문 번호만 주면 모델이 그 내용을 모르는 상태로 판단한다 — 엔진 안내)
|
|
195
|
+
criteria_kcs: _kcsCriteriaText(item.kcsCriteria),
|
|
196
|
+
criteria_site: (item.siteSpecCriteria || '').trim()
|
|
162
197
|
}));
|
|
163
198
|
}
|
|
164
199
|
get uploadedSets() {
|
|
@@ -747,6 +782,8 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
747
782
|
mainType
|
|
748
783
|
detailType
|
|
749
784
|
name
|
|
785
|
+
kcsCriteria
|
|
786
|
+
siteSpecCriteria
|
|
750
787
|
}
|
|
751
788
|
}
|
|
752
789
|
buildingLevel {
|
|
@@ -833,7 +870,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
833
870
|
}
|
|
834
871
|
/* [1단계] 촬영 가이드 생성 (plan-shots) */
|
|
835
872
|
async _loadShotGuide() {
|
|
836
|
-
var _a;
|
|
873
|
+
var _a, _b;
|
|
837
874
|
const checklist = this.engineChecklist;
|
|
838
875
|
if (!checklist.length) {
|
|
839
876
|
notify({ message: '체크리스트 항목이 없습니다.', level: 'warn' });
|
|
@@ -844,6 +881,14 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
844
881
|
this.loadingText = 'AI 촬영 가이드 생성 중...';
|
|
845
882
|
this.guide = await this._callEngine({ action: 'plan-shots', checklist });
|
|
846
883
|
this.sets = new Array((((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || []).length).fill(null);
|
|
884
|
+
// 어느 촬영에도 배정되지 않은 항목이 있으면 그 항목은 판정 근거가 없다 → 재생성 권고
|
|
885
|
+
const unassigned = ((_b = this.guide) === null || _b === void 0 ? void 0 : _b.unassigned_items) || [];
|
|
886
|
+
if (unassigned.length) {
|
|
887
|
+
notify({
|
|
888
|
+
message: `촬영에 배정되지 않은 검사항목이 ${unassigned.length}건 있습니다. 촬영 가이드를 다시 생성해보세요.`,
|
|
889
|
+
level: 'warn'
|
|
890
|
+
});
|
|
891
|
+
}
|
|
847
892
|
// 자동 등록 기준 시각 — 이 시점 이후 올라온 세트만 자동으로 붙인다.
|
|
848
893
|
// (예전 테스트 세트가 태그만 같다고 채워지면 "찍지도 않았는데 완료"로 보인다)
|
|
849
894
|
this.autoAttachSince = Date.now() / 1000;
|
|
@@ -856,6 +901,33 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
856
901
|
this.loading = false;
|
|
857
902
|
}
|
|
858
903
|
}
|
|
904
|
+
/* plan-shots가 준 촬영별 항목 배정 → {촬영번호: [항목ID]} (엔진 inspect 의 shot_items).
|
|
905
|
+
배정 정보가 없는 구버전 응답이면 null 을 돌려 종전대로(전 항목이 전 사진) 동작시킨다. */
|
|
906
|
+
_shotItemsMap() {
|
|
907
|
+
var _a, _b, _c;
|
|
908
|
+
const shots = ((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || [];
|
|
909
|
+
const map = {};
|
|
910
|
+
for (let i = 0; i < shots.length; i++) {
|
|
911
|
+
const items = (_b = shots[i]) === null || _b === void 0 ? void 0 : _b.items;
|
|
912
|
+
if (!Array.isArray(items) || !items.length)
|
|
913
|
+
continue;
|
|
914
|
+
map[String(((_c = shots[i]) === null || _c === void 0 ? void 0 : _c.shot_no) || i + 1)] = items;
|
|
915
|
+
}
|
|
916
|
+
return Object.keys(map).length ? map : null;
|
|
917
|
+
}
|
|
918
|
+
/* 세트가 아직 안 올라온 촬영에 배정된 항목 ID 집합 (그 항목은 판정할 사진이 없다) */
|
|
919
|
+
_itemsAwaitingShot() {
|
|
920
|
+
var _a, _b;
|
|
921
|
+
const pending = new Set();
|
|
922
|
+
const shots = ((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || [];
|
|
923
|
+
for (let i = 0; i < shots.length; i++) {
|
|
924
|
+
if (this.sets[i])
|
|
925
|
+
continue;
|
|
926
|
+
for (const id of ((_b = shots[i]) === null || _b === void 0 ? void 0 : _b.items) || [])
|
|
927
|
+
pending.add(id);
|
|
928
|
+
}
|
|
929
|
+
return pending;
|
|
930
|
+
}
|
|
859
931
|
/* 촬영 칸마다 고유 태그 — 앱에 부재 ID로 넘겨 두면, 올라온 세트의 member_id로
|
|
860
932
|
"어느 촬영 칸의 것인지" 되찾을 수 있다 (앱↔플랫폼 왕복에 식별자가 없어서 쓰는 방법) */
|
|
861
933
|
_shotTag(shotNo) {
|
|
@@ -1044,11 +1116,13 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
1044
1116
|
* - 최초 판정: 업로드된 모든 세트의 대표 3프레임 + 전체 체크리스트 (input_kind=video_frames)
|
|
1045
1117
|
* - 재검사(재촬영 패널): 새 세트의 3프레임만 + 해당 항목만 재판정 후 결과 병합 */
|
|
1046
1118
|
async _runInspection() {
|
|
1119
|
+
var _a, _b, _c;
|
|
1047
1120
|
const isRecapture = this.panel === 'recapture' && !!this.selectedItem;
|
|
1048
1121
|
try {
|
|
1049
1122
|
this.loading = true;
|
|
1050
1123
|
this.loadingText = '동영상 프레임 추출 중...';
|
|
1051
1124
|
let newPhotos = [];
|
|
1125
|
+
const imageShots = []; // 사진마다 어느 촬영(shot_no)의 것인지
|
|
1052
1126
|
if (isRecapture) {
|
|
1053
1127
|
if (!this.recaptureSet) {
|
|
1054
1128
|
notify({ message: '재촬영 세트를 먼저 업로드해주세요.', level: 'warn' });
|
|
@@ -1058,21 +1132,64 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
1058
1132
|
newPhotos = await this._fetchVlmFrames(this.recaptureSet, this.sets.length);
|
|
1059
1133
|
}
|
|
1060
1134
|
else {
|
|
1061
|
-
|
|
1062
|
-
if (!sets.length) {
|
|
1135
|
+
if (!this.uploadedSets.length) {
|
|
1063
1136
|
notify({ message: '업로드된 촬영 세트가 없습니다.', level: 'warn' });
|
|
1064
1137
|
return;
|
|
1065
1138
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1139
|
+
// 촬영 칸 순서대로 — 비어 있는 칸은 건너뛰되 촬영번호는 가이드 것을 그대로 쓴다
|
|
1140
|
+
for (let i = 0; i < this.sets.length; i++) {
|
|
1141
|
+
const set = this.sets[i];
|
|
1142
|
+
if (!set)
|
|
1143
|
+
continue;
|
|
1144
|
+
const frames = await this._fetchVlmFrames(set, i);
|
|
1145
|
+
const shotNo = ((_c = (_b = (_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) === null || _b === void 0 ? void 0 : _b[i]) === null || _c === void 0 ? void 0 : _c.shot_no) || i + 1;
|
|
1146
|
+
for (const _ of frames)
|
|
1147
|
+
imageShots.push(shotNo);
|
|
1148
|
+
newPhotos = [...newPhotos, ...frames];
|
|
1068
1149
|
}
|
|
1069
1150
|
}
|
|
1070
1151
|
const files = newPhotos.map(p => p.file);
|
|
1071
|
-
|
|
1152
|
+
let checklist = isRecapture
|
|
1072
1153
|
? this.engineChecklist.filter(c => c.item_id === this.selectedItem.item_id)
|
|
1073
1154
|
: this.engineChecklist;
|
|
1074
|
-
|
|
1075
|
-
|
|
1155
|
+
// 아직 안 올라온 촬영에 배정된 항목은 이번 판정에서 뺀다.
|
|
1156
|
+
// 엔진은 그런 항목의 사진 범위를 비우는 대신 '전체 사진'을 주므로, 그냥 두면
|
|
1157
|
+
// 타설 후 항목이 배근 사진으로 판정된다 (배근·타설은 며칠 간격으로 찍는다).
|
|
1158
|
+
if (!isRecapture) {
|
|
1159
|
+
const pending = this._itemsAwaitingShot();
|
|
1160
|
+
if (pending.size) {
|
|
1161
|
+
checklist = checklist.filter(c => !pending.has(c.item_id));
|
|
1162
|
+
notify({
|
|
1163
|
+
message: `아직 촬영되지 않은 항목 ${pending.size}건은 이번 판정에서 제외했습니다. 해당 촬영을 올린 뒤 다시 실행하세요.`,
|
|
1164
|
+
level: 'warn'
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// 촬영 배정 되돌리기 — 이걸 안 보내면 모든 항목이 모든 사진을 보게 되어
|
|
1169
|
+
// "타설 후 항목이 배근 사진으로 판정되는" 오판이 난다 (엔진 실증에서 실제 발생).
|
|
1170
|
+
// 배정된 촬영이 안 올라온 항목은 엔진이 hold 로 둔다 — 남의 사진으로 판정하지 않게.
|
|
1171
|
+
const shotItems = this._shotItemsMap();
|
|
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;
|
|
1192
|
+
}
|
|
1076
1193
|
if (isRecapture) {
|
|
1077
1194
|
this.photos = [...this.photos, ...newPhotos];
|
|
1078
1195
|
this._mergeItems((result === null || result === void 0 ? void 0 : result.items_sorted) || (result === null || result === void 0 ? void 0 : result.items) || [], this.recaptureBase);
|
|
@@ -1354,6 +1471,9 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
1354
1471
|
});
|
|
1355
1472
|
const judged = Object.assign(Object.assign({}, item), result.item);
|
|
1356
1473
|
this._mergeItems([judged]);
|
|
1474
|
+
// 확정 즉시 체크리스트에 기록한다. 「검사 등록」까지 미루면 그 사이에 화면을 벗어나거나
|
|
1475
|
+
// 재판정이 일어났을 때 측정 판정만 유실된다 (VLM 항목은 inspect 결과에 남아 있어 티가 안 남).
|
|
1476
|
+
await this._saveJudgments([judged]);
|
|
1357
1477
|
this._resetMeasureState();
|
|
1358
1478
|
this.panel = 'none';
|
|
1359
1479
|
this.selectedItem = null;
|
|
@@ -1388,9 +1508,33 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
1388
1508
|
return null;
|
|
1389
1509
|
}
|
|
1390
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
|
+
}
|
|
1391
1535
|
/* ─── 검사 등록: 감리자 판정 자동 반영 + AI판단 근거 첨부 ─── */
|
|
1392
1536
|
async _registerInspection() {
|
|
1393
|
-
var _a
|
|
1537
|
+
var _a;
|
|
1394
1538
|
const items = this._sortedItems();
|
|
1395
1539
|
if (!items.length) {
|
|
1396
1540
|
notify({ message: '판정 결과가 없습니다.', level: 'warn' });
|
|
@@ -1402,23 +1546,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
1402
1546
|
this.loading = true;
|
|
1403
1547
|
this.loadingText = '검사 결과 등록 중...';
|
|
1404
1548
|
// 1) 감리자 적합/부적합 자동 채움 (적합/부적합 판정된 항목만, 없으면 건너뜀)
|
|
1405
|
-
|
|
1406
|
-
const response = await client.mutate({
|
|
1407
|
-
mutation: gql `
|
|
1408
|
-
mutation UpdateChecklistItemsAiJudgment($items: [ChecklistItemAiJudgment!]!) {
|
|
1409
|
-
updateChecklistItemsAiJudgment(items: $items)
|
|
1410
|
-
}
|
|
1411
|
-
`,
|
|
1412
|
-
variables: {
|
|
1413
|
-
items: judged.map(it => ({
|
|
1414
|
-
id: it.item_id,
|
|
1415
|
-
supervisoryConfirmStatus: it.verdict === 'pass' ? 'T' : 'F'
|
|
1416
|
-
}))
|
|
1417
|
-
}
|
|
1418
|
-
});
|
|
1419
|
-
if (response.errors)
|
|
1420
|
-
throw new Error(((_a = response.errors[0]) === null || _a === void 0 ? void 0 : _a.message) || '검사 등록에 실패했습니다.');
|
|
1421
|
-
}
|
|
1549
|
+
await this._saveJudgments(items);
|
|
1422
1550
|
// 2) 항목별 첨부: 판단 근거(AI판단 텍스트) + 근거 프레임/측정 근거 이미지
|
|
1423
1551
|
// ai_report는 pending/error에서 빈 문자열로 오므로 그 항목은 첨부하지 않는다
|
|
1424
1552
|
const attachments = [];
|
|
@@ -1457,7 +1585,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
1457
1585
|
context: { hasUpload: true }
|
|
1458
1586
|
});
|
|
1459
1587
|
if (attachResponse.errors)
|
|
1460
|
-
throw new Error(((
|
|
1588
|
+
throw new Error(((_a = attachResponse.errors[0]) === null || _a === void 0 ? void 0 : _a.message) || 'AI판단 자료 첨부에 실패했습니다.');
|
|
1461
1589
|
}
|
|
1462
1590
|
notify({ message: `검사 결과를 등록하였습니다. (자동 판정 ${judged.length}건 반영)` });
|
|
1463
1591
|
// 등록 후 체크리스트 화면으로 이동
|