@dssp/supervision 1.0.6 → 1.0.10
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/assets/ai-measurement/capture_guide.png +0 -0
- package/dist-client/assets/ai-measurement/picto-common-sweep.svg +11 -0
- package/dist-client/assets/ai-measurement/picto-member-corner.svg +11 -0
- package/dist-client/assets/ai-measurement/picto-rebar-parallel.svg +14 -0
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.d.ts +59 -13
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js +1003 -224
- package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js.map +1 -1
- package/dist-client/pages/checklist/checklist-view.js +3 -4
- package/dist-client/pages/checklist/checklist-view.js.map +1 -1
- package/dist-client/pages/project-checklist/project-checklist-management.js +5 -3
- package/dist-client/pages/project-checklist/project-checklist-management.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/project-checklist/spec-matching-service.d.ts +1 -1
- package/dist-server/service/project-checklist/spec-matching-service.js +23 -3
- package/dist-server/service/project-checklist/spec-matching-service.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
/**
|
|
3
|
-
* 검측 관리 상세 - AI 자동 검측
|
|
3
|
+
* 검측 관리 상세 - AI 자동 검측 (동영상 세트 + 측정 모델 2종)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1) 촬영 가이드(plan-shots
|
|
7
|
-
* 2)
|
|
8
|
-
* 3)
|
|
9
|
-
* 4)
|
|
5
|
+
* 체크리스트 항목을 AI 엔진(inspection-ai)과 측정 서버(measure-engine)에 연동해:
|
|
6
|
+
* 1) 촬영 가이드(plan-shots, 동영상 최대 3건)를 표시하고
|
|
7
|
+
* 2) LidarInspector 앱이 만든 세트(video.mov/poses.jsonl/meta.json)를 업로드하면
|
|
8
|
+
* 3) 세트별 대표 3프레임으로 자동 판정(inspect, input_kind=video_frames)을 실행한 뒤
|
|
9
|
+
* 4) 「추가 작업 필요」(재촬영/기준값 입력/AI 미지원)를 분류해 측정 UI(철근 배근 길이·기둥벽체 두께)로 처리하고
|
|
10
|
+
* 5) 측정 결과는 apply-measurement로 판정 확정 → 검사 등록 시 감리자 적합/부적합 자동 반영 + 근거 첨부한다.
|
|
10
11
|
*/
|
|
11
12
|
import '@material/web/icon/icon.js';
|
|
12
13
|
import '@material/web/progress/circular-progress.js';
|
|
@@ -22,20 +23,36 @@ import './component/building-inspection-detail-header';
|
|
|
22
23
|
const ENGINE_BASE_URL = 'https://hatiolab-korea-uni.nruing.com';
|
|
23
24
|
const ENGINE_RUN_URL = `${ENGINE_BASE_URL}/api/run/inspection-ai`;
|
|
24
25
|
const ENGINE_AUTH = `Basic ${btoa('admin:admin1234')}`;
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/* 측정 서버(measure-engine/server.py). 배포 시 실제 주소로 수정 — https 페이지에서는 https(리버스 프록시) 필요 */
|
|
27
|
+
const MEASURE_BASE_URL = 'https://hatiolab-korea-uni.nruing.com/measure';
|
|
28
|
+
const MEASURE_AUTH = ENGINE_AUTH;
|
|
29
|
+
/* 촬영 안내 이미지 (엔진 전달물 고정 그림 + 픽토그램) */
|
|
30
|
+
const captureGuideImg = new URL('../../assets/ai-measurement/capture_guide.png', import.meta.url).href;
|
|
31
|
+
const pictoRebarImg = new URL('../../assets/ai-measurement/picto-rebar-parallel.svg', import.meta.url).href;
|
|
32
|
+
const pictoMemberImg = new URL('../../assets/ai-measurement/picto-member-corner.svg', import.meta.url).href;
|
|
33
|
+
/* 세트 폴더 필수 파일 (LidarInspector 앱이 촬영 1회당 생성) */
|
|
34
|
+
const SET_FILES = ['video.mov', 'poses.jsonl', 'meta.json'];
|
|
35
|
+
/* 엔진 verdict(기계값) → 화면 라벨. pass/fail만 합/불로 자동 체크, 나머지는 감리자 판단 */
|
|
27
36
|
const VERDICT_LABEL = {
|
|
28
37
|
pass: '적합',
|
|
29
38
|
fail: '부적합',
|
|
30
|
-
hold: '
|
|
31
|
-
pending: '미지원',
|
|
32
|
-
error: '오류'
|
|
39
|
+
hold: '재촬영 필요',
|
|
40
|
+
pending: 'AI 미지원',
|
|
41
|
+
error: '분석 오류'
|
|
33
42
|
};
|
|
34
|
-
/* 엔진 routing(기계값) → 화면
|
|
43
|
+
/* 엔진 routing(기계값) → 화면 라벨. cv 계열은 CV팀 명세 고정 문구 "3D 계측" */
|
|
35
44
|
const ROUTING_LABEL = {
|
|
36
45
|
vlm: 'AI 사진판독',
|
|
37
|
-
cv: '
|
|
38
|
-
cv_missing: '
|
|
46
|
+
cv: '3D 계측',
|
|
47
|
+
cv_missing: '3D 계측'
|
|
48
|
+
};
|
|
49
|
+
/* operator_inputs 필드 키 → 한글 라벨 (모르는 키는 키 그대로 노출) */
|
|
50
|
+
const FIELD_LABEL = {
|
|
51
|
+
name: '부재/변 이름',
|
|
52
|
+
member_dim_mm: '설계 단면 치수 (mm)',
|
|
53
|
+
cover_spec_mm: '기준(최소) 피복두께 (mm)',
|
|
54
|
+
design_mm: '설계 두께 (mm)',
|
|
55
|
+
tol_mm: '허용 공차 ± (mm)'
|
|
39
56
|
};
|
|
40
57
|
let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement extends PageView {
|
|
41
58
|
constructor() {
|
|
@@ -44,23 +61,33 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
44
61
|
this.buildingInspection = {};
|
|
45
62
|
this.buildingInspectionId = '';
|
|
46
63
|
this.guide = null; // plan-shots 응답 {shots, total_shots, markdown}
|
|
47
|
-
this.
|
|
64
|
+
this.sets = []; // 촬영 건별 업로드된 세트 (shots 순서)
|
|
65
|
+
this.photos = []; // inspect에 보낸 대표 프레임(표시/첨부용)
|
|
48
66
|
this.result = null; // inspect 응답
|
|
49
67
|
this.mainPhotoIndex = 0;
|
|
50
68
|
this.panel = 'none';
|
|
51
69
|
this.selectedItem = null;
|
|
52
|
-
this.recaptureBase = 0; //
|
|
70
|
+
this.recaptureBase = 0; // 재검사 진입 시점의 프레임 수 (used_images 보정용)
|
|
71
|
+
this.recaptureSet = null; // 재촬영으로 새로 올린 세트
|
|
53
72
|
this.expandedReportId = null; // 종합 리포트에서 펼친 항목(아코디언)
|
|
73
|
+
/* 측정 패널 상태 */
|
|
74
|
+
this.measureSetIndex = 0;
|
|
75
|
+
this.measureInputs = {};
|
|
76
|
+
this.measureRunning = false;
|
|
77
|
+
this.measureMsg = '';
|
|
78
|
+
this.measureResult = null; // 측정 서버 원본 응답 — apply-measurement에 가공 없이 전달
|
|
79
|
+
this.measureImageUrl = ''; // 근거 이미지(오버레이/결과사진)
|
|
80
|
+
this.thicknessFrames = [];
|
|
81
|
+
this.thicknessSlot = null;
|
|
82
|
+
this.thicknessFrameUrl = '';
|
|
83
|
+
this.thicknessClick = null;
|
|
54
84
|
this.loading = false;
|
|
55
85
|
this.loadingText = '';
|
|
56
86
|
/** 뮤테이션 가능 여부('dcsp guest' 권한 보유 시 false) — 게스트에게는 생성/수정/삭제 버튼을 숨긴다 */
|
|
57
87
|
this.canMutate = false;
|
|
58
|
-
/*
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
this.photos.forEach(p => URL.revokeObjectURL(p.url));
|
|
62
|
-
this.photos = [];
|
|
63
|
-
};
|
|
88
|
+
/* 세트 업로드 대상: 촬영 건 인덱스(가이드 화면) 또는 -1(재촬영 패널) */
|
|
89
|
+
this.pendingSetIndex = null;
|
|
90
|
+
this.objectUrls = []; // 페이지 이탈 시 정리할 blob URL들
|
|
64
91
|
this._openReport = () => {
|
|
65
92
|
this.panel = 'report';
|
|
66
93
|
this.selectedItem = null;
|
|
@@ -70,6 +97,16 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
70
97
|
this.panel = 'none';
|
|
71
98
|
this.selectedItem = null;
|
|
72
99
|
};
|
|
100
|
+
/* 클릭 좌표를 표시 크기로 나눠 0~1 정규화 — 원본 해상도는 알 필요 없다 */
|
|
101
|
+
this._onThicknessClick = (e) => {
|
|
102
|
+
if (this.measureRunning)
|
|
103
|
+
return;
|
|
104
|
+
const rect = e.target.getBoundingClientRect();
|
|
105
|
+
this.thicknessClick = {
|
|
106
|
+
x: (e.clientX - rect.left) / rect.width,
|
|
107
|
+
y: (e.clientY - rect.top) / rect.height
|
|
108
|
+
};
|
|
109
|
+
};
|
|
73
110
|
}
|
|
74
111
|
async connectedCallback() {
|
|
75
112
|
super.connectedCallback();
|
|
@@ -86,6 +123,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
86
123
|
var _a, _b;
|
|
87
124
|
const items = ((_b = (_a = this.buildingInspection) === null || _a === void 0 ? void 0 : _a.checklist) === null || _b === void 0 ? void 0 : _b.checklistItems) || [];
|
|
88
125
|
return [...items]
|
|
126
|
+
.filter(item => (item.name || '').trim()) // 이름 없는 항목은 검측 불가 — 엔진 검증 실패 방지
|
|
89
127
|
.sort((a, b) => (a.sequence || 0) - (b.sequence || 0))
|
|
90
128
|
.map(item => ({
|
|
91
129
|
item_id: item.id,
|
|
@@ -93,6 +131,9 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
93
131
|
text: item.name || ''
|
|
94
132
|
}));
|
|
95
133
|
}
|
|
134
|
+
get uploadedSets() {
|
|
135
|
+
return this.sets.filter((s) => !!s);
|
|
136
|
+
}
|
|
96
137
|
render() {
|
|
97
138
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
98
139
|
return html `
|
|
@@ -114,8 +155,14 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
114
155
|
${this.result ? this._renderResult() : this._renderGuide()}
|
|
115
156
|
</div>
|
|
116
157
|
|
|
117
|
-
<!--
|
|
118
|
-
<input
|
|
158
|
+
<!-- 세트 파일 선택: video.mov/poses.jsonl/meta.json 3개 멀티선택 또는 zip 1개 -->
|
|
159
|
+
<input
|
|
160
|
+
id="setInput"
|
|
161
|
+
type="file"
|
|
162
|
+
accept=".mov,.jsonl,.json,.zip,video/quicktime"
|
|
163
|
+
multiple
|
|
164
|
+
@change=${this._onSetFilesSelected}
|
|
165
|
+
/>
|
|
119
166
|
|
|
120
167
|
${this.loading
|
|
121
168
|
? html `
|
|
@@ -129,66 +176,107 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
129
176
|
: ''}
|
|
130
177
|
`;
|
|
131
178
|
}
|
|
132
|
-
/* ─── 1단계: 촬영 가이드 화면 ─── */
|
|
179
|
+
/* ─── 1단계: 촬영 가이드 + 세트 업로드 화면 ─── */
|
|
133
180
|
_renderGuide() {
|
|
134
181
|
if (!this.guide) {
|
|
135
182
|
return html `<div class="empty-state">촬영 가이드를 준비 중입니다...</div>`;
|
|
136
183
|
}
|
|
137
184
|
const shots = this.guide.shots || [];
|
|
138
185
|
const total = this.guide.total_shots || shots.length;
|
|
139
|
-
const
|
|
140
|
-
const current = shots[Math.min(
|
|
141
|
-
const done =
|
|
186
|
+
const uploaded = this.uploadedSets.length;
|
|
187
|
+
const current = shots[Math.min(uploaded, shots.length - 1)];
|
|
188
|
+
const done = uploaded >= total;
|
|
142
189
|
return html `
|
|
143
190
|
<div class="guide-container">
|
|
144
|
-
<div class="step-badge"
|
|
191
|
+
<div class="step-badge">동영상 ${Math.min(uploaded + 1, total)}/${total} · ${(current === null || current === void 0 ? void 0 : current.target) || '촬영'}</div>
|
|
145
192
|
<div class="guide-title">${(current === null || current === void 0 ? void 0 : current.target) || ''}${done ? ' 촬영 완료' : ''}</div>
|
|
146
|
-
<div class="guide-desc"
|
|
193
|
+
<div class="guide-desc">
|
|
194
|
+
전용 촬영 앱(LidarInspector)으로 동영상을 촬영한 뒤, 앱이 만든 파일 3개(video.mov·poses.jsonl·meta.json)를 촬영 건마다
|
|
195
|
+
업로드해주세요. 일반 카메라 동영상은 사용할 수 없습니다.
|
|
196
|
+
</div>
|
|
147
197
|
|
|
148
198
|
<div class="card">
|
|
149
|
-
<h4>촬영
|
|
199
|
+
<h4>촬영 가이드 (동영상 ${total}건)</h4>
|
|
150
200
|
<ul>
|
|
151
|
-
${shots.map(s => html `<li
|
|
201
|
+
${shots.map(s => html `<li>촬영 ${s.shot_no || ''} · ${s.target} — ${s.how}</li>`)}
|
|
152
202
|
</ul>
|
|
153
203
|
</div>
|
|
154
204
|
|
|
205
|
+
<div class="card">
|
|
206
|
+
<h4>공통 촬영법 — 옆으로 이동 · 대상 정면 유지 · 대상 전체 포함</h4>
|
|
207
|
+
<img class="capture-guide-img" src=${captureGuideImg} alt="촬영 안내" />
|
|
208
|
+
<div class="picto-strip">
|
|
209
|
+
<img src=${pictoRebarImg} alt="철근: 재려는 변과 나란히" />
|
|
210
|
+
<img src=${pictoMemberImg} alt="기둥·벽: 모서리가 보이게" />
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
155
214
|
<div class="card" style="display: flex; flex-direction: column; gap: 10px;">
|
|
156
215
|
<div class="progress-row">
|
|
157
|
-
<span
|
|
158
|
-
<span>${
|
|
216
|
+
<span>촬영 세트 업로드</span>
|
|
217
|
+
<span>${uploaded} / ${total}</span>
|
|
159
218
|
</div>
|
|
160
|
-
<div class="progress-bar"><div style="width: ${total ? (
|
|
161
|
-
${
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
219
|
+
<div class="progress-bar"><div style="width: ${total ? (uploaded / total) * 100 : 0}%"></div></div>
|
|
220
|
+
${shots.map((s, i) => {
|
|
221
|
+
const set = this.sets[i];
|
|
222
|
+
return html `
|
|
223
|
+
<div class="set-card" ?uploaded=${!!set} @click=${() => !set && this._openSetPicker(i)}>
|
|
224
|
+
<md-icon>${set ? 'check_circle' : 'video_file'}</md-icon>
|
|
225
|
+
<div class="set-main">
|
|
226
|
+
<b>촬영 ${s.shot_no || i + 1} · ${s.target}</b>
|
|
227
|
+
${set
|
|
228
|
+
? html `세트 업로드 완료${set.technique ? ` (${set.technique})` : ''}
|
|
229
|
+
${set.warnings.map(w => html `<div class="set-warn">⚠ ${w}</div>`)}`
|
|
230
|
+
: html `탭해서 파일 3개(video.mov·poses.jsonl·meta.json)를 한 번에 선택`}
|
|
168
231
|
</div>
|
|
169
|
-
|
|
170
|
-
|
|
232
|
+
${set
|
|
233
|
+
? html `<button
|
|
234
|
+
class="text-button"
|
|
235
|
+
@click=${(e) => {
|
|
236
|
+
e.stopPropagation();
|
|
237
|
+
this._openSetPicker(i);
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
교체
|
|
241
|
+
</button>`
|
|
242
|
+
: ''}
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
})}
|
|
171
246
|
</div>
|
|
172
247
|
|
|
173
|
-
<button class="primary-button" ?disabled=${
|
|
174
|
-
|
|
248
|
+
<button class="primary-button" ?disabled=${uploaded === 0} @click=${() => this._runInspection()}>
|
|
249
|
+
AI 자동 판정 실행 ${uploaded ? `(세트 ${uploaded}/${total})` : ''}
|
|
175
250
|
</button>
|
|
176
|
-
${taken > 0
|
|
177
|
-
? html `
|
|
178
|
-
<div style="display: flex; justify-content: flex-end;">
|
|
179
|
-
<button class="text-button" @click=${this._resetPhotos}>사진 초기화</button>
|
|
180
|
-
</div>
|
|
181
|
-
`
|
|
182
|
-
: ''}
|
|
183
251
|
</div>
|
|
184
252
|
`;
|
|
185
253
|
}
|
|
186
254
|
/* ─── 2단계: 판정 결과 화면 ─── */
|
|
255
|
+
/* 항목 그룹: auto(판정완료) / recapture(재촬영) / measure(기준값 입력) / manual(AI 미지원·오류) */
|
|
256
|
+
_groupOf(item) {
|
|
257
|
+
if (item.verdict === 'pass' || item.verdict === 'fail')
|
|
258
|
+
return 'auto';
|
|
259
|
+
if (item.verdict === 'hold')
|
|
260
|
+
return 'recapture';
|
|
261
|
+
if (item.verdict === 'pending' && item.pending_reason === 'measure')
|
|
262
|
+
return 'measure';
|
|
263
|
+
return 'manual';
|
|
264
|
+
}
|
|
265
|
+
/* verdict → 화면 라벨 (pending은 pending_reason으로 세분) */
|
|
266
|
+
_verdictLabel(item) {
|
|
267
|
+
if (item.verdict === 'pending') {
|
|
268
|
+
return item.pending_reason === 'measure' ? '기준값 입력 필요' : 'AI 미지원';
|
|
269
|
+
}
|
|
270
|
+
return VERDICT_LABEL[item.verdict] || item.verdict;
|
|
271
|
+
}
|
|
187
272
|
_renderResult() {
|
|
188
273
|
const items = this._sortedItems();
|
|
189
|
-
const autoItems = items.filter(it =>
|
|
190
|
-
const
|
|
191
|
-
|
|
274
|
+
const autoItems = items.filter(it => this._groupOf(it) === 'auto');
|
|
275
|
+
const recaptureItems = items.filter(it => this._groupOf(it) === 'recapture');
|
|
276
|
+
const measureItems = items.filter(it => this._groupOf(it) === 'measure');
|
|
277
|
+
const manualItems = items.filter(it => this._groupOf(it) === 'manual');
|
|
278
|
+
const moreCount = recaptureItems.length + measureItems.length + manualItems.length;
|
|
279
|
+
// 클릭한 항목의 used_images만 표시 (선택 없으면 전체 항목 합집합). used_images는 전체 프레임 기준 1-based
|
|
192
280
|
const focusItems = this.selectedItem ? [this.selectedItem] : items;
|
|
193
281
|
const usedNos = new Set();
|
|
194
282
|
for (const it of focusItems)
|
|
@@ -196,16 +284,17 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
196
284
|
usedNos.add(n);
|
|
197
285
|
const usedPhotos = this.photos.map((photo, i) => ({ photo, no: i + 1 })).filter(x => usedNos.has(x.no));
|
|
198
286
|
const mainPhoto = usedPhotos[this.mainPhotoIndex];
|
|
287
|
+
let no = 0;
|
|
199
288
|
return html `
|
|
200
289
|
<div class="result-body">
|
|
201
290
|
<div class="photo-pane">
|
|
202
|
-
<div class="caption">판정에 사용된
|
|
203
|
-
<div class="photo-main">${mainPhoto ? html `<img src=${mainPhoto.photo.url} alt="현장
|
|
291
|
+
<div class="caption">판정에 사용된 프레임 (동영상에서 추출)</div>
|
|
292
|
+
<div class="photo-main">${mainPhoto ? html `<img src=${mainPhoto.photo.url} alt="현장 프레임" />` : ''}</div>
|
|
204
293
|
<div class="photo-thumbs">
|
|
205
294
|
${usedPhotos.map((x, i) => html `
|
|
206
295
|
<img
|
|
207
296
|
src=${x.photo.url}
|
|
208
|
-
alt="
|
|
297
|
+
alt="프레임 ${x.no}"
|
|
209
298
|
?selected=${i === this.mainPhotoIndex}
|
|
210
299
|
@click=${() => (this.mainPhotoIndex = i)}
|
|
211
300
|
/>
|
|
@@ -215,19 +304,36 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
215
304
|
|
|
216
305
|
<div class="items-pane">
|
|
217
306
|
<div class="summary-banner">
|
|
218
|
-
적용 항목 ${items.length}개 · 자동 판정 ${autoItems.length}개 · 추가
|
|
307
|
+
적용 항목 ${items.length}개 · 자동 판정 ${autoItems.length}개 · 추가 작업 ${moreCount}개
|
|
219
308
|
</div>
|
|
220
309
|
|
|
221
310
|
${autoItems.length
|
|
222
311
|
? html `
|
|
223
312
|
<div class="group-title">자동 판정 완료 (${autoItems.length})</div>
|
|
224
|
-
${autoItems.map(
|
|
313
|
+
${autoItems.map(it => this._renderItemCard(it, ++no))}
|
|
225
314
|
`
|
|
226
315
|
: ''}
|
|
227
|
-
${
|
|
316
|
+
${moreCount
|
|
228
317
|
? html `
|
|
229
|
-
<div class="group-title">추가
|
|
230
|
-
${
|
|
318
|
+
<div class="group-title">추가 작업 필요 (${moreCount} 남음)</div>
|
|
319
|
+
${recaptureItems.length
|
|
320
|
+
? html `
|
|
321
|
+
<div class="group-subtitle">재촬영 필요 (${recaptureItems.length})</div>
|
|
322
|
+
${recaptureItems.map(it => this._renderItemCard(it, ++no))}
|
|
323
|
+
`
|
|
324
|
+
: ''}
|
|
325
|
+
${measureItems.length
|
|
326
|
+
? html `
|
|
327
|
+
<div class="group-subtitle">기준값 입력 필요 — 3D 계측 (${measureItems.length})</div>
|
|
328
|
+
${measureItems.map(it => this._renderItemCard(it, ++no))}
|
|
329
|
+
`
|
|
330
|
+
: ''}
|
|
331
|
+
${manualItems.length
|
|
332
|
+
? html `
|
|
333
|
+
<div class="group-subtitle">AI 미지원 — 감리자 수동 확인 (${manualItems.length})</div>
|
|
334
|
+
${manualItems.map(it => this._renderItemCard(it, ++no))}
|
|
335
|
+
`
|
|
336
|
+
: ''}
|
|
231
337
|
`
|
|
232
338
|
: ''}
|
|
233
339
|
|
|
@@ -239,26 +345,38 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
239
345
|
`;
|
|
240
346
|
}
|
|
241
347
|
_renderItemCard(item, no) {
|
|
242
|
-
var _a;
|
|
243
|
-
const
|
|
348
|
+
var _a, _b, _c;
|
|
349
|
+
const group = this._groupOf(item);
|
|
350
|
+
// 측정 완료 항목은 summary(측정 한 줄 설명)를 자르지 않고 그대로, VLM 항목은 관찰/사유
|
|
351
|
+
const desc = item.measurements && item.reason
|
|
352
|
+
? item.reason
|
|
353
|
+
: item.observation
|
|
354
|
+
? `관찰: ${item.observation}`
|
|
355
|
+
: item.reason || ((_a = item.measure) === null || _a === void 0 ? void 0 : _a.question) || '';
|
|
244
356
|
return html `
|
|
245
|
-
<div class="item-card" ?selected=${((
|
|
357
|
+
<div class="item-card" ?selected=${((_b = this.selectedItem) === null || _b === void 0 ? void 0 : _b.item_id) === item.item_id} @click=${() => this._openDetail(item)}>
|
|
246
358
|
<div class="item-no">${no}</div>
|
|
247
359
|
<div class="item-main">
|
|
248
360
|
<div class="item-name">${item.text}</div>
|
|
249
361
|
<div>
|
|
250
|
-
|
|
251
|
-
|
|
362
|
+
${group === 'auto'
|
|
363
|
+
? html `<span class="chip">${ROUTING_LABEL[item.routing] || item.routing}</span>
|
|
364
|
+
<span class="chip-verdict">${this._verdictLabel(item)}</span>`
|
|
365
|
+
: group === 'recapture'
|
|
366
|
+
? html `<span class="chip">추가 촬영 필요</span>`
|
|
367
|
+
: group === 'measure'
|
|
368
|
+
? html `<span class="chip">AI 측정 모델</span> <span class="chip-verdict">기준값 입력 필요</span>`
|
|
369
|
+
: html `<span class="chip">미지원</span>`}
|
|
252
370
|
</div>
|
|
253
|
-
<div class="item-desc">${
|
|
371
|
+
<div class="item-desc">${desc}</div>
|
|
254
372
|
</div>
|
|
255
|
-
${auto
|
|
373
|
+
${group === 'auto'
|
|
256
374
|
? html `
|
|
257
375
|
<div class="badge" ?pass=${item.verdict === 'pass'} ?fail=${item.verdict === 'fail'}>
|
|
258
376
|
${VERDICT_LABEL[item.verdict]}
|
|
259
377
|
</div>
|
|
260
378
|
`
|
|
261
|
-
:
|
|
379
|
+
: group === 'recapture'
|
|
262
380
|
? html `
|
|
263
381
|
<button
|
|
264
382
|
class="capture-button"
|
|
@@ -270,16 +388,30 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
270
388
|
촬영하기
|
|
271
389
|
</button>
|
|
272
390
|
`
|
|
273
|
-
:
|
|
391
|
+
: group === 'measure'
|
|
392
|
+
? html `
|
|
393
|
+
<button
|
|
394
|
+
class="measure-button"
|
|
395
|
+
@click=${(e) => {
|
|
396
|
+
e.stopPropagation();
|
|
397
|
+
this._openMeasure(item);
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
${((_c = item.measure) === null || _c === void 0 ? void 0 : _c.next_action) === 'select_and_input' ? '측정 진행' : '기준값 입력'}
|
|
401
|
+
</button>
|
|
402
|
+
`
|
|
403
|
+
: html `<div class="badge" etc>${this._verdictLabel(item)}</div>`}
|
|
274
404
|
</div>
|
|
275
405
|
`;
|
|
276
406
|
}
|
|
277
|
-
/* ─── 우측 패널: 상세 / 재촬영 / 리포트 ─── */
|
|
407
|
+
/* ─── 우측 패널: 상세 / 재촬영 / 측정 / 리포트 ─── */
|
|
278
408
|
_renderSidePane() {
|
|
279
409
|
if (this.panel === 'detail' && this.selectedItem)
|
|
280
410
|
return this._renderDetailPane(this.selectedItem);
|
|
281
411
|
if (this.panel === 'recapture' && this.selectedItem)
|
|
282
412
|
return this._renderRecapturePane(this.selectedItem);
|
|
413
|
+
if (this.panel === 'measure' && this.selectedItem)
|
|
414
|
+
return this._renderMeasurePane(this.selectedItem);
|
|
283
415
|
if (this.panel === 'report')
|
|
284
416
|
return this._renderReportPane();
|
|
285
417
|
return '';
|
|
@@ -287,7 +419,8 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
287
419
|
_renderDetailPane(item) {
|
|
288
420
|
const auto = item.verdict === 'pass' || item.verdict === 'fail';
|
|
289
421
|
const routingLabel = ROUTING_LABEL[item.routing] || item.routing;
|
|
290
|
-
const verdictLabel =
|
|
422
|
+
const verdictLabel = this._verdictLabel(item);
|
|
423
|
+
const measured = !!item.measurements; // 측정 모델로 판정된 항목 — 신뢰도는 확률이 없어 비운다
|
|
291
424
|
return html `
|
|
292
425
|
<div class="side-pane">
|
|
293
426
|
<div class="detail-header">
|
|
@@ -304,7 +437,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
304
437
|
${verdictLabel || '-'}
|
|
305
438
|
</span>
|
|
306
439
|
</div>
|
|
307
|
-
${item.confidence != null
|
|
440
|
+
${!measured && item.confidence != null
|
|
308
441
|
? html `
|
|
309
442
|
<div class="kv">
|
|
310
443
|
<label>신뢰도</label>
|
|
@@ -314,7 +447,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
314
447
|
: ''}
|
|
315
448
|
<div class="kv">
|
|
316
449
|
<label>처리</label>
|
|
317
|
-
<span>${
|
|
450
|
+
<span>${routingLabel || '-'}</span>
|
|
318
451
|
</div>
|
|
319
452
|
</div>
|
|
320
453
|
|
|
@@ -325,54 +458,187 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
325
458
|
</div>
|
|
326
459
|
</div>
|
|
327
460
|
|
|
461
|
+
${item.image_data ? html `<img class="evidence-img" src=${item.image_data} alt="측정 근거 이미지" />` : ''}
|
|
462
|
+
|
|
328
463
|
<button class="text-button" @click=${this._closePanel}>닫기</button>
|
|
329
464
|
</div>
|
|
330
465
|
`;
|
|
331
466
|
}
|
|
332
467
|
_renderRecapturePane(item) {
|
|
333
468
|
var _a;
|
|
334
|
-
const
|
|
335
|
-
? item.recapture_shots
|
|
336
|
-
: [{ target: '추가 사진', how: '항목이 잘 보이도록 촬영해주세요.' }];
|
|
337
|
-
const taken = this.photos.length - this.recaptureBase;
|
|
338
|
-
const total = shots.length;
|
|
469
|
+
const shot = (_a = item.recapture_shots) === null || _a === void 0 ? void 0 : _a[0];
|
|
339
470
|
return html `
|
|
340
471
|
<div class="side-pane">
|
|
341
472
|
<h3>${item.text}</h3>
|
|
342
473
|
<div class="why-box">
|
|
343
|
-
<b>왜
|
|
344
|
-
${item.reason || '판정을 위해 추가
|
|
345
|
-
</div>
|
|
346
|
-
<div>
|
|
347
|
-
<h4 style="margin: 0 0 8px 0; font-size: 13px;">촬영 가이드</h4>
|
|
348
|
-
<ul
|
|
349
|
-
style="margin: 0; padding-left: 18px; font-size: 12px; color: #555; display: flex; flex-direction: column; gap: 6px;"
|
|
350
|
-
>
|
|
351
|
-
${shots.map(s => html `<li>${s.target} — ${s.how}</li>`)}
|
|
352
|
-
</ul>
|
|
474
|
+
<b>왜 재촬영이 필요한가요?</b><br />
|
|
475
|
+
${item.reason || '판정을 위해 추가 촬영이 필요합니다.'}
|
|
353
476
|
</div>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
477
|
+
${shot
|
|
478
|
+
? html `
|
|
479
|
+
<div>
|
|
480
|
+
<h4 style="margin: 0 0 8px 0; font-size: 13px;">촬영 가이드</h4>
|
|
481
|
+
<div style="font-size: 12px; color: #555;">${shot.target} — ${shot.how}</div>
|
|
482
|
+
</div>
|
|
483
|
+
`
|
|
484
|
+
: ''}
|
|
485
|
+
<div class="set-card" ?uploaded=${!!this.recaptureSet} @click=${() => this._openSetPicker(-1)}>
|
|
486
|
+
<md-icon>${this.recaptureSet ? 'check_circle' : 'video_file'}</md-icon>
|
|
487
|
+
<div class="set-main">
|
|
488
|
+
<b>재촬영 세트</b>
|
|
489
|
+
${this.recaptureSet
|
|
490
|
+
? html `세트 업로드 완료 ${this.recaptureSet.warnings.map(w => html `<div class="set-warn">⚠ ${w}</div>`)}`
|
|
491
|
+
: html `앱으로 재촬영 후 파일 3개를 한 번에 선택`}
|
|
492
|
+
</div>
|
|
357
493
|
</div>
|
|
358
|
-
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
494
|
+
${this.recaptureSet ? html `<button class="primary-button" @click=${() => this._runInspection()}>재검사 실행</button>` : ''}
|
|
495
|
+
<button class="text-button" @click=${this._closePanel}>닫기</button>
|
|
496
|
+
</div>
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
/* ─── 측정 패널 (④ 철근 배근 길이 / ⑤ 기둥·벽체 두께) ─── */
|
|
500
|
+
/* operator_inputs 정의 기반 숫자/텍스트 입력 렌더 (slot·click·n_frames는 별도 UI/고정) */
|
|
501
|
+
_renderMeasureFields(item) {
|
|
502
|
+
var _a;
|
|
503
|
+
const defs = ((_a = item.measure) === null || _a === void 0 ? void 0 : _a.operator_inputs) || [];
|
|
504
|
+
const fields = [];
|
|
505
|
+
for (const d of defs) {
|
|
506
|
+
if (d.key === 'n_frames' || d.key === 'slot' || d.key === 'click')
|
|
507
|
+
continue;
|
|
508
|
+
if (d.fields) {
|
|
509
|
+
// 복합 입력(targets 등): 하위 필드를 각각 입력으로 펼친다
|
|
510
|
+
for (const [k, desc] of Object.entries(d.fields)) {
|
|
511
|
+
if (k === 'name')
|
|
512
|
+
continue; // 부재명은 검사 항목명으로 자동 지정
|
|
513
|
+
fields.push({ key: k, label: FIELD_LABEL[k] || k, desc: String(desc) });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
fields.push({ key: d.key, label: FIELD_LABEL[d.key] || d.key, desc: d.desc, def: d.default });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return fields.map(f => {
|
|
521
|
+
var _a;
|
|
362
522
|
return html `
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
523
|
+
<div class="measure-field">
|
|
524
|
+
<label title=${f.desc || ''}>${f.label}${f.desc ? html ` <span style="color:#aaa">— ${f.desc}</span>` : ''}</label>
|
|
525
|
+
<input
|
|
526
|
+
type="number"
|
|
527
|
+
inputmode="decimal"
|
|
528
|
+
.value=${(_a = this.measureInputs[f.key]) !== null && _a !== void 0 ? _a : (f.def != null ? String(f.def) : '')}
|
|
529
|
+
placeholder="도면값"
|
|
530
|
+
@input=${(e) => {
|
|
531
|
+
this.measureInputs = Object.assign(Object.assign({}, this.measureInputs), { [f.key]: e.target.value });
|
|
532
|
+
}}
|
|
533
|
+
/>
|
|
534
|
+
</div>
|
|
535
|
+
`;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
_renderMeasurePane(item) {
|
|
539
|
+
const measure = item.measure || {};
|
|
540
|
+
const isThickness = measure.next_action === 'select_and_input';
|
|
541
|
+
const setOptions = this.uploadedSets;
|
|
542
|
+
const done = !!this.measureResult;
|
|
543
|
+
return html `
|
|
544
|
+
<div class="side-pane">
|
|
545
|
+
<h3>${item.text}</h3>
|
|
546
|
+
<div class="detail-header">
|
|
547
|
+
<span class="chip">3D 계측</span>
|
|
548
|
+
<span style="font-size: 12px; color: #777;">${measure.name || ''} (${measure.status || ''})</span>
|
|
368
549
|
</div>
|
|
369
|
-
${
|
|
370
|
-
|
|
550
|
+
${measure.capture_guide ? html `<div class="why-box">📷 ${measure.capture_guide}</div>` : ''}
|
|
551
|
+
<img
|
|
552
|
+
class="capture-guide-img"
|
|
553
|
+
style="background: #fff; border: 1px solid #eee; padding: 6px;"
|
|
554
|
+
src=${isThickness ? pictoMemberImg : pictoRebarImg}
|
|
555
|
+
alt="촬영 안내"
|
|
556
|
+
/>
|
|
557
|
+
|
|
558
|
+
${setOptions.length > 1
|
|
559
|
+
? html `
|
|
560
|
+
<div class="measure-field">
|
|
561
|
+
<label>측정할 촬영 세트</label>
|
|
562
|
+
<select
|
|
563
|
+
.value=${String(this.measureSetIndex)}
|
|
564
|
+
@change=${(e) => this._onMeasureSetChange(parseInt(e.target.value))}
|
|
565
|
+
>
|
|
566
|
+
${setOptions.map((s, i) => html `<option value=${i} ?selected=${i === this.measureSetIndex}>
|
|
567
|
+
세트 ${i + 1}${s.technique ? ` (${s.technique})` : ''}
|
|
568
|
+
</option>`)}
|
|
569
|
+
</select>
|
|
570
|
+
</div>
|
|
571
|
+
`
|
|
572
|
+
: ''}
|
|
573
|
+
${isThickness ? this._renderThicknessPicker() : ''} ${this._renderMeasureFields(item)}
|
|
574
|
+
${this.measureRunning
|
|
575
|
+
? html `
|
|
576
|
+
<div class="measure-msg">
|
|
577
|
+
<md-circular-progress indeterminate></md-circular-progress>
|
|
578
|
+
<span>${this.measureMsg || '측정 중...'}</span>
|
|
579
|
+
</div>
|
|
580
|
+
`
|
|
581
|
+
: html `
|
|
582
|
+
<button
|
|
583
|
+
class="primary-button"
|
|
584
|
+
?disabled=${isThickness && (this.thicknessSlot === null || !this.thicknessClick)}
|
|
585
|
+
@click=${() => (isThickness ? this._runThicknessMeasure(item) : this._runRebarMeasure(item))}
|
|
586
|
+
>
|
|
587
|
+
${done ? '다시 측정' : isThickness ? '측정 실행 (10~20초)' : '측정 시작 (약 45초)'}
|
|
588
|
+
</button>
|
|
589
|
+
`}
|
|
590
|
+
${done
|
|
591
|
+
? html `
|
|
592
|
+
${this.measureResult.summary || this.measureResult.summary_line || this.measureResult.headline
|
|
593
|
+
? html `<div class="measure-summary">
|
|
594
|
+
${this.measureResult.summary || this.measureResult.summary_line || this.measureResult.headline}
|
|
595
|
+
</div>`
|
|
596
|
+
: ''}
|
|
597
|
+
${this.measureImageUrl ? html `<img class="evidence-img" src=${this.measureImageUrl} alt="측정 근거" />` : ''}
|
|
598
|
+
${this.canMutate
|
|
599
|
+
? html `<button class="primary-button" @click=${() => this._applyMeasurement(item)}>판정 확정</button>`
|
|
600
|
+
: ''}
|
|
601
|
+
`
|
|
371
602
|
: ''}
|
|
603
|
+
|
|
372
604
|
<button class="text-button" @click=${this._closePanel}>닫기</button>
|
|
373
605
|
</div>
|
|
374
606
|
`;
|
|
375
607
|
}
|
|
608
|
+
/* ⑤ 두께: 프레임 선택 + 측정면 클릭 (자동 면 선택은 정확도가 낮아 클릭 단계 생략 불가) */
|
|
609
|
+
_renderThicknessPicker() {
|
|
610
|
+
return html `
|
|
611
|
+
<div class="measure-field">
|
|
612
|
+
<label>① 모서리가 잘 보이는 프레임을 고르세요</label>
|
|
613
|
+
<div class="frame-strip">
|
|
614
|
+
${this.thicknessFrames.map(f => html `
|
|
615
|
+
<img
|
|
616
|
+
src=${f.thumbUrl || ''}
|
|
617
|
+
alt="프레임 ${f.index}"
|
|
618
|
+
?selected=${this.thicknessSlot === f.slot}
|
|
619
|
+
@click=${() => this._selectThicknessFrame(f.slot)}
|
|
620
|
+
/>
|
|
621
|
+
`)}
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
${this.thicknessFrameUrl
|
|
625
|
+
? html `
|
|
626
|
+
<div class="measure-field">
|
|
627
|
+
<label>② 잴 면의 가운데를 한 번 탭하세요 (모서리·철근 위 말고 <b>면 위</b>)</label>
|
|
628
|
+
<div class="click-stage">
|
|
629
|
+
<img src=${this.thicknessFrameUrl} alt="선택 프레임" @click=${this._onThicknessClick} />
|
|
630
|
+
${this.thicknessClick
|
|
631
|
+
? html `<div
|
|
632
|
+
class="click-dot"
|
|
633
|
+
style="left: ${this.thicknessClick.x * 100}%; top: ${this.thicknessClick.y * 100}%"
|
|
634
|
+
></div>`
|
|
635
|
+
: ''}
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
`
|
|
639
|
+
: ''}
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
376
642
|
_renderReportPane() {
|
|
377
643
|
var _a;
|
|
378
644
|
const items = this._sortedItems();
|
|
@@ -400,7 +666,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
400
666
|
?fail=${it.verdict === 'fail'}
|
|
401
667
|
?etc=${it.verdict !== 'pass' && it.verdict !== 'fail'}
|
|
402
668
|
>
|
|
403
|
-
${
|
|
669
|
+
${this._verdictLabel(it)}
|
|
404
670
|
</span>
|
|
405
671
|
<md-icon class="report-caret" ?open=${expanded}>expand_more</md-icon>
|
|
406
672
|
</div>
|
|
@@ -411,7 +677,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
411
677
|
${it.observation ? html `<div>관찰: ${it.observation}</div>` : ''}
|
|
412
678
|
${it.reason ? html `<div>판단: ${it.reason}</div>` : ''}
|
|
413
679
|
${((_a = it.used_images) === null || _a === void 0 ? void 0 : _a.length)
|
|
414
|
-
? html `<div class="report-used">근거
|
|
680
|
+
? html `<div class="report-used">근거 프레임: ${it.used_images.map(n => `#${n}`).join(', ')}</div>`
|
|
415
681
|
: ''}
|
|
416
682
|
</div>
|
|
417
683
|
`
|
|
@@ -420,9 +686,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
420
686
|
`;
|
|
421
687
|
})}
|
|
422
688
|
</div>
|
|
423
|
-
${this.canMutate
|
|
424
|
-
? html `<button class="primary-button" @click=${this._registerInspection}>검사 등록</button>`
|
|
425
|
-
: ''}
|
|
689
|
+
${this.canMutate ? html `<button class="primary-button" @click=${this._registerInspection}>검사 등록</button>` : ''}
|
|
426
690
|
<button class="text-button" @click=${this._closePanel}>닫기</button>
|
|
427
691
|
</div>
|
|
428
692
|
`;
|
|
@@ -493,8 +757,8 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
493
757
|
return;
|
|
494
758
|
this.project = response.data.project;
|
|
495
759
|
}
|
|
496
|
-
/* ───
|
|
497
|
-
/* 엔진 실행 공통 호출: input(JSON) +
|
|
760
|
+
/* ─── 서버 호출 공통 ─── */
|
|
761
|
+
/* 엔진 실행 공통 호출: input(JSON) + 파일들을 multipart로 전송 */
|
|
498
762
|
async _callEngine(input, files = []) {
|
|
499
763
|
const form = new FormData();
|
|
500
764
|
form.append('input', JSON.stringify(input));
|
|
@@ -512,8 +776,29 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
512
776
|
}
|
|
513
777
|
return data.result;
|
|
514
778
|
}
|
|
779
|
+
/* 측정 서버 호출 (JSON 응답) */
|
|
780
|
+
async _callMeasure(path, init = {}) {
|
|
781
|
+
var _a;
|
|
782
|
+
const response = await fetch(`${MEASURE_BASE_URL}${path}`, Object.assign(Object.assign({}, init), { headers: Object.assign(Object.assign({}, (init.headers || {})), { Authorization: MEASURE_AUTH }) }));
|
|
783
|
+
const data = await response.json().catch(() => ({}));
|
|
784
|
+
if (!response.ok) {
|
|
785
|
+
const msg = ((_a = data === null || data === void 0 ? void 0 : data.errors) === null || _a === void 0 ? void 0 : _a.join('\n')) || (data === null || data === void 0 ? void 0 : data.detail) || (data === null || data === void 0 ? void 0 : data.error) || `측정 서버 오류 (${response.status})`;
|
|
786
|
+
throw new Error(msg);
|
|
787
|
+
}
|
|
788
|
+
return data;
|
|
789
|
+
}
|
|
790
|
+
/* 측정 서버 이미지 → blob URL (img src는 인증 헤더를 못 실어서 fetch로 받는다) */
|
|
791
|
+
async _fetchMeasureImage(path) {
|
|
792
|
+
const response = await fetch(`${MEASURE_BASE_URL}${path}`, { headers: { Authorization: MEASURE_AUTH } });
|
|
793
|
+
if (!response.ok)
|
|
794
|
+
throw new Error(`이미지 수신 실패 (${response.status})`);
|
|
795
|
+
const url = URL.createObjectURL(await response.blob());
|
|
796
|
+
this.objectUrls.push(url);
|
|
797
|
+
return url;
|
|
798
|
+
}
|
|
515
799
|
/* [1단계] 촬영 가이드 생성 (plan-shots) */
|
|
516
800
|
async _loadShotGuide() {
|
|
801
|
+
var _a;
|
|
517
802
|
const checklist = this.engineChecklist;
|
|
518
803
|
if (!checklist.length) {
|
|
519
804
|
notify({ message: '체크리스트 항목이 없습니다.', level: 'warn' });
|
|
@@ -523,6 +808,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
523
808
|
this.loading = true;
|
|
524
809
|
this.loadingText = 'AI 촬영 가이드 생성 중...';
|
|
525
810
|
this.guide = await this._callEngine({ action: 'plan-shots', checklist });
|
|
811
|
+
this.sets = new Array((((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || []).length).fill(null);
|
|
526
812
|
}
|
|
527
813
|
catch (error) {
|
|
528
814
|
console.error('촬영 가이드 생성 실패:', error);
|
|
@@ -532,28 +818,122 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
532
818
|
this.loading = false;
|
|
533
819
|
}
|
|
534
820
|
}
|
|
821
|
+
/* ─── 세트 업로드 ─── */
|
|
822
|
+
_openSetPicker(index) {
|
|
823
|
+
var _a;
|
|
824
|
+
this.pendingSetIndex = index;
|
|
825
|
+
(_a = this.setInputEl) === null || _a === void 0 ? void 0 : _a.click();
|
|
826
|
+
}
|
|
827
|
+
async _onSetFilesSelected(event) {
|
|
828
|
+
const input = event.target;
|
|
829
|
+
const files = Array.from((input === null || input === void 0 ? void 0 : input.files) || []);
|
|
830
|
+
input.value = '';
|
|
831
|
+
const index = this.pendingSetIndex;
|
|
832
|
+
this.pendingSetIndex = null;
|
|
833
|
+
if (!files.length || index === null)
|
|
834
|
+
return;
|
|
835
|
+
// zip 1개 또는 필수 3파일 검증 (한 번의 선택으로 3개 멀티선택)
|
|
836
|
+
const isZip = files.length === 1 && /\.zip$/i.test(files[0].name);
|
|
837
|
+
if (!isZip) {
|
|
838
|
+
const names = files.map(f => f.name.toLowerCase());
|
|
839
|
+
const missing = SET_FILES.filter(n => !names.includes(n));
|
|
840
|
+
if (missing.length) {
|
|
841
|
+
notify({
|
|
842
|
+
message: `세트 파일이 부족합니다. 누락: ${missing.join(', ')} — 촬영 앱이 만든 3개 파일을 한 번에 선택해주세요.`,
|
|
843
|
+
level: 'warn'
|
|
844
|
+
});
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
this.loading = true;
|
|
850
|
+
this.loadingText = '촬영 세트 업로드 중...';
|
|
851
|
+
const form = new FormData();
|
|
852
|
+
for (const f of files)
|
|
853
|
+
if (isZip || SET_FILES.includes(f.name.toLowerCase()))
|
|
854
|
+
form.append('files', f, f.name);
|
|
855
|
+
const data = await this._callMeasure('/api/sets', { method: 'POST', body: form });
|
|
856
|
+
const set = {
|
|
857
|
+
setId: data.set_id,
|
|
858
|
+
technique: data.technique,
|
|
859
|
+
label: `세트 ${data.set_id}`,
|
|
860
|
+
warnings: data.warnings || []
|
|
861
|
+
};
|
|
862
|
+
if (index === -1) {
|
|
863
|
+
this.recaptureSet = set;
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
const next = [...this.sets];
|
|
867
|
+
next[index] = set;
|
|
868
|
+
this.sets = next;
|
|
869
|
+
}
|
|
870
|
+
for (const w of set.warnings)
|
|
871
|
+
notify({ message: `업로드 경고: ${w}`, level: 'warn' });
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
console.error('세트 업로드 실패:', error);
|
|
875
|
+
notify({ message: error.message, level: 'error' });
|
|
876
|
+
}
|
|
877
|
+
finally {
|
|
878
|
+
this.loading = false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/* 세트의 대표 3프레임(vlm)을 받아 File 목록으로 (시간순 유지) */
|
|
882
|
+
async _fetchVlmFrames(set, setIndex) {
|
|
883
|
+
const data = await this._callMeasure(`/api/sets/${set.setId}/vlm-frames`);
|
|
884
|
+
const out = [];
|
|
885
|
+
for (const f of data.frames || []) {
|
|
886
|
+
const response = await fetch(`${MEASURE_BASE_URL}${f.url}`, { headers: { Authorization: MEASURE_AUTH } });
|
|
887
|
+
if (!response.ok)
|
|
888
|
+
throw new Error(`프레임 수신 실패 (${response.status})`);
|
|
889
|
+
const blob = await response.blob();
|
|
890
|
+
const file = new File([blob], `set${setIndex + 1}_${f.name}`, { type: 'image/jpeg' });
|
|
891
|
+
const url = URL.createObjectURL(file);
|
|
892
|
+
this.objectUrls.push(url);
|
|
893
|
+
out.push({ file, url, setIndex });
|
|
894
|
+
}
|
|
895
|
+
return out;
|
|
896
|
+
}
|
|
535
897
|
/* [2단계] 자동 판정 실행 (inspect)
|
|
536
|
-
* - 최초 판정:
|
|
537
|
-
* - 재검사(재촬영 패널):
|
|
898
|
+
* - 최초 판정: 업로드된 모든 세트의 대표 3프레임 + 전체 체크리스트 (input_kind=video_frames)
|
|
899
|
+
* - 재검사(재촬영 패널): 새 세트의 3프레임만 + 해당 항목만 재판정 후 결과 병합 */
|
|
538
900
|
async _runInspection() {
|
|
539
901
|
const isRecapture = this.panel === 'recapture' && !!this.selectedItem;
|
|
540
|
-
// 재검사는 recaptureBase 이후에 찍은 사진만 전송
|
|
541
|
-
const files = (isRecapture ? this.photos.slice(this.recaptureBase) : this.photos).map(p => p.file);
|
|
542
|
-
if (!files.length) {
|
|
543
|
-
notify({ message: '촬영된 사진이 없습니다.', level: 'warn' });
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
const checklist = isRecapture
|
|
547
|
-
? this.engineChecklist.filter(c => c.item_id === this.selectedItem.item_id)
|
|
548
|
-
: this.engineChecklist;
|
|
549
902
|
try {
|
|
550
903
|
this.loading = true;
|
|
904
|
+
this.loadingText = '동영상 프레임 추출 중...';
|
|
905
|
+
let newPhotos = [];
|
|
906
|
+
if (isRecapture) {
|
|
907
|
+
if (!this.recaptureSet) {
|
|
908
|
+
notify({ message: '재촬영 세트를 먼저 업로드해주세요.', level: 'warn' });
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.recaptureBase = this.photos.length;
|
|
912
|
+
newPhotos = await this._fetchVlmFrames(this.recaptureSet, this.sets.length);
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const sets = this.uploadedSets;
|
|
916
|
+
if (!sets.length) {
|
|
917
|
+
notify({ message: '업로드된 촬영 세트가 없습니다.', level: 'warn' });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
for (let i = 0; i < sets.length; i++) {
|
|
921
|
+
newPhotos = [...newPhotos, ...(await this._fetchVlmFrames(sets[i], i))];
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const files = newPhotos.map(p => p.file);
|
|
925
|
+
const checklist = isRecapture
|
|
926
|
+
? this.engineChecklist.filter(c => c.item_id === this.selectedItem.item_id)
|
|
927
|
+
: this.engineChecklist;
|
|
551
928
|
this.loadingText = 'AI 자동 판정 중... (최대 1~2분 소요)';
|
|
552
|
-
const result = await this._callEngine({ action: 'inspect', checklist }, files);
|
|
929
|
+
const result = await this._callEngine({ action: 'inspect', checklist, input_kind: 'video_frames' }, files);
|
|
553
930
|
if (isRecapture) {
|
|
554
|
-
this.
|
|
931
|
+
this.photos = [...this.photos, ...newPhotos];
|
|
932
|
+
this._mergeItems((result === null || result === void 0 ? void 0 : result.items_sorted) || (result === null || result === void 0 ? void 0 : result.items) || [], this.recaptureBase);
|
|
933
|
+
this.recaptureSet = null;
|
|
555
934
|
}
|
|
556
935
|
else {
|
|
936
|
+
this.photos = newPhotos;
|
|
557
937
|
this.result = result;
|
|
558
938
|
}
|
|
559
939
|
this.panel = 'none';
|
|
@@ -568,83 +948,96 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
568
948
|
this.loading = false;
|
|
569
949
|
}
|
|
570
950
|
}
|
|
571
|
-
/*
|
|
572
|
-
* - used_images
|
|
573
|
-
|
|
574
|
-
_mergeRecaptureResult(result) {
|
|
951
|
+
/* 부분 재판정/측정 확정 결과를 기존 전체 결과에 병합하고 summary 재계산
|
|
952
|
+
* - usedImageBase: used_images(부분집합 1-based) → 전체 프레임 인덱스 보정값 */
|
|
953
|
+
_mergeItems(updatedItems, usedImageBase = 0) {
|
|
575
954
|
var _a, _b;
|
|
576
|
-
const
|
|
577
|
-
const updated = ((result === null || result === void 0 ? void 0 : result.items_sorted) || (result === null || result === void 0 ? void 0 : result.items) || []).map((it) => (Object.assign(Object.assign({}, it), { used_images: (it.used_images || []).map((n) => n + base) })));
|
|
955
|
+
const updated = updatedItems.map((it) => (Object.assign(Object.assign({}, it), { used_images: (it.used_images || []).map((n) => n + usedImageBase) })));
|
|
578
956
|
const byId = new Map(updated.map((it) => [it.item_id, it]));
|
|
579
957
|
const replace = (arr) => (arr || []).map(it => byId.get(it.item_id) || it);
|
|
580
958
|
const items = replace((_a = this.result) === null || _a === void 0 ? void 0 : _a.items);
|
|
581
959
|
const itemsSorted = ((_b = this.result) === null || _b === void 0 ? void 0 : _b.items_sorted) ? replace(this.result.items_sorted) : undefined;
|
|
582
|
-
const summary = { pass: 0, fail: 0, hold: 0, pending: 0, error: 0 };
|
|
583
|
-
for (const it of itemsSorted || items)
|
|
960
|
+
const summary = { pass: 0, fail: 0, hold: 0, pending: 0, error: 0, measure: 0 };
|
|
961
|
+
for (const it of itemsSorted || items) {
|
|
584
962
|
summary[it.verdict] = (summary[it.verdict] || 0) + 1;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
/* ─── 사진 선택 ─── */
|
|
588
|
-
/* 슬롯 클릭 → 사진 선택기 열기 (모바일: 촬영/앨범 선택 팝업, 채워진 슬롯이면 교체) */
|
|
589
|
-
_openPhotoPicker(slotIndex = null) {
|
|
590
|
-
var _a;
|
|
591
|
-
this.pendingSlotIndex = slotIndex;
|
|
592
|
-
(_a = this.photoInputEl) === null || _a === void 0 ? void 0 : _a.click();
|
|
593
|
-
}
|
|
594
|
-
_onPhotosSelected(event) {
|
|
595
|
-
const input = event.target;
|
|
596
|
-
const files = Array.from((input === null || input === void 0 ? void 0 : input.files) || []).filter(f => f.type.startsWith('image/') || /\.heic$/i.test(f.name));
|
|
597
|
-
input.value = '';
|
|
598
|
-
const slotIndex = this.pendingSlotIndex;
|
|
599
|
-
this.pendingSlotIndex = null;
|
|
600
|
-
if (!files.length)
|
|
601
|
-
return;
|
|
602
|
-
// 채워진 슬롯을 클릭한 경우 → 해당 사진 교체 (첫 파일만 사용)
|
|
603
|
-
if (slotIndex !== null && slotIndex < this.photos.length) {
|
|
604
|
-
URL.revokeObjectURL(this.photos[slotIndex].url);
|
|
605
|
-
const next = [...this.photos];
|
|
606
|
-
next[slotIndex] = { file: files[0], url: URL.createObjectURL(files[0]) };
|
|
607
|
-
this.photos = next;
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
// 빈 슬롯/추가 → 순서대로 뒤에 추가
|
|
611
|
-
// 엔진 제한은 요청(호출)당 MAX_IMAGES장. 재검사는 새로 찍은 사진만 별도 전송되므로
|
|
612
|
-
// 누적 합계가 아니라 이번 회차(최초=전체, 재검사=recaptureBase 이후) 기준으로만 제한한다.
|
|
613
|
-
const batchBase = this.panel === 'recapture' ? this.recaptureBase : 0;
|
|
614
|
-
const cap = batchBase + MAX_IMAGES;
|
|
615
|
-
if (this.photos.length + files.length > cap) {
|
|
616
|
-
notify({ message: `사진은 회차당 최대 ${MAX_IMAGES}장까지 사용할 수 있습니다.`, level: 'warn' });
|
|
617
|
-
files.length = Math.max(0, cap - this.photos.length);
|
|
618
|
-
if (!files.length)
|
|
619
|
-
return;
|
|
963
|
+
if (it.verdict === 'pending' && it.pending_reason === 'measure')
|
|
964
|
+
summary.measure += 1;
|
|
620
965
|
}
|
|
621
|
-
|
|
966
|
+
summary.total = (itemsSorted || items).length;
|
|
967
|
+
this.result = Object.assign(Object.assign(Object.assign(Object.assign({}, this.result), { items }), (itemsSorted ? { items_sorted: itemsSorted } : {})), { summary });
|
|
622
968
|
}
|
|
623
969
|
_resetAll() {
|
|
624
|
-
this.
|
|
970
|
+
this.objectUrls.forEach(u => URL.revokeObjectURL(u));
|
|
971
|
+
this.objectUrls = [];
|
|
625
972
|
this.guide = null;
|
|
973
|
+
this.sets = [];
|
|
974
|
+
this.photos = [];
|
|
626
975
|
this.result = null;
|
|
627
976
|
this.panel = 'none';
|
|
628
977
|
this.selectedItem = null;
|
|
629
978
|
this.mainPhotoIndex = 0;
|
|
630
979
|
this.recaptureBase = 0;
|
|
980
|
+
this.recaptureSet = null;
|
|
981
|
+
this._resetMeasureState();
|
|
982
|
+
}
|
|
983
|
+
_resetMeasureState() {
|
|
984
|
+
this.measureInputs = {};
|
|
985
|
+
this.measureRunning = false;
|
|
986
|
+
this.measureMsg = '';
|
|
987
|
+
this.measureResult = null;
|
|
988
|
+
this.measureImageUrl = '';
|
|
989
|
+
this.thicknessFrames = [];
|
|
990
|
+
this.thicknessSlot = null;
|
|
991
|
+
this.thicknessFrameUrl = '';
|
|
992
|
+
this.thicknessClick = null;
|
|
631
993
|
}
|
|
632
994
|
/* ─── 패널 열기/닫기 ─── */
|
|
633
995
|
_openDetail(item) {
|
|
634
|
-
|
|
635
|
-
|
|
996
|
+
const group = this._groupOf(item);
|
|
997
|
+
// 재촬영/측정 항목은 상세 대신 해당 작업 패널을 바로 표시
|
|
998
|
+
if (group === 'recapture') {
|
|
636
999
|
this._openRecapture(item);
|
|
637
1000
|
return;
|
|
638
1001
|
}
|
|
1002
|
+
if (group === 'measure') {
|
|
1003
|
+
this._openMeasure(item);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
639
1006
|
this.selectedItem = item;
|
|
640
|
-
this.mainPhotoIndex = 0; // 선택 항목의 첫 사용
|
|
1007
|
+
this.mainPhotoIndex = 0; // 선택 항목의 첫 사용 프레임부터 표시
|
|
641
1008
|
this.panel = 'detail';
|
|
642
1009
|
}
|
|
643
1010
|
_openRecapture(item) {
|
|
644
1011
|
this.selectedItem = item;
|
|
645
|
-
this.
|
|
1012
|
+
this.recaptureSet = null;
|
|
646
1013
|
this.panel = 'recapture';
|
|
647
1014
|
}
|
|
1015
|
+
async _openMeasure(item) {
|
|
1016
|
+
var _a, _b, _c;
|
|
1017
|
+
this.selectedItem = item;
|
|
1018
|
+
this._resetMeasureState();
|
|
1019
|
+
this.panel = 'measure';
|
|
1020
|
+
// 촬영 기술이 맞는 세트를 기본 선택 (④=04_cover, ⑤=05_thickness)
|
|
1021
|
+
const wantTech = ((_b = (_a = item.measure) === null || _a === void 0 ? void 0 : _a.model_id) === null || _b === void 0 ? void 0 : _b.startsWith('rebar')) ? '04_cover' : '05_thickness';
|
|
1022
|
+
const sets = this.uploadedSets;
|
|
1023
|
+
const found = sets.findIndex(s => s.technique === wantTech);
|
|
1024
|
+
this.measureSetIndex = found >= 0 ? found : 0;
|
|
1025
|
+
if (((_c = item.measure) === null || _c === void 0 ? void 0 : _c.next_action) === 'select_and_input') {
|
|
1026
|
+
await this._loadThicknessFrames();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async _onMeasureSetChange(index) {
|
|
1030
|
+
var _a, _b;
|
|
1031
|
+
this.measureSetIndex = index;
|
|
1032
|
+
this.measureResult = null;
|
|
1033
|
+
this.measureImageUrl = '';
|
|
1034
|
+
if (((_b = (_a = this.selectedItem) === null || _a === void 0 ? void 0 : _a.measure) === null || _b === void 0 ? void 0 : _b.next_action) === 'select_and_input') {
|
|
1035
|
+
this.thicknessSlot = null;
|
|
1036
|
+
this.thicknessFrameUrl = '';
|
|
1037
|
+
this.thicknessClick = null;
|
|
1038
|
+
await this._loadThicknessFrames();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
648
1041
|
/* 종합 리포트 항목 펼치기/접기 (한 번에 하나만 펼침) */
|
|
649
1042
|
_toggleReportItem(id) {
|
|
650
1043
|
this.expandedReportId = this.expandedReportId === id ? null : id;
|
|
@@ -653,7 +1046,203 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
653
1046
|
var _a, _b;
|
|
654
1047
|
return ((_a = this.result) === null || _a === void 0 ? void 0 : _a.items_sorted) || ((_b = this.result) === null || _b === void 0 ? void 0 : _b.items) || [];
|
|
655
1048
|
}
|
|
656
|
-
/* ───
|
|
1049
|
+
/* ─── ④ 철근 배근 길이 측정 (비동기 job + 진행률 폴링) ─── */
|
|
1050
|
+
async _runRebarMeasure(item) {
|
|
1051
|
+
const set = this.uploadedSets[this.measureSetIndex];
|
|
1052
|
+
if (!set) {
|
|
1053
|
+
notify({ message: '측정할 촬영 세트가 없습니다.', level: 'warn' });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const memberDim = parseFloat(this.measureInputs.member_dim_mm);
|
|
1057
|
+
const coverSpec = parseFloat(this.measureInputs.cover_spec_mm);
|
|
1058
|
+
if (!memberDim || !coverSpec) {
|
|
1059
|
+
notify({ message: '설계 단면 치수와 기준 피복두께를 입력해주세요. (도면값)', level: 'warn' });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
this.measureRunning = true;
|
|
1064
|
+
this.measureMsg = '측정 시작...';
|
|
1065
|
+
this.measureResult = null;
|
|
1066
|
+
this.measureImageUrl = '';
|
|
1067
|
+
const { job } = await this._callMeasure('/api/rebar/measure', {
|
|
1068
|
+
method: 'POST',
|
|
1069
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1070
|
+
body: JSON.stringify({
|
|
1071
|
+
set_id: set.setId,
|
|
1072
|
+
targets: [{ name: item.text || '측정 대상', member_dim_mm: memberDim, cover_spec_mm: coverSpec }]
|
|
1073
|
+
})
|
|
1074
|
+
});
|
|
1075
|
+
// 진행률 폴링 — msg는 서버가 주는 한국어 문구 그대로 표시
|
|
1076
|
+
let status;
|
|
1077
|
+
do {
|
|
1078
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
1079
|
+
status = await this._callMeasure(`/api/job/${job}`);
|
|
1080
|
+
this.measureMsg = status.msg || '측정 중...';
|
|
1081
|
+
} while (status.state !== 'done' && this.panel === 'measure');
|
|
1082
|
+
if (this.panel !== 'measure')
|
|
1083
|
+
return; // 패널을 닫았으면 결과 무시
|
|
1084
|
+
const r = status.result || {};
|
|
1085
|
+
if (!r.ok) {
|
|
1086
|
+
notify({ message: r.why || '측정에 실패했습니다.', level: 'error' });
|
|
1087
|
+
// 실패 결과도 확정 가능(엔진이 hold로 변환) — 결과는 보관하되 이미지 없음
|
|
1088
|
+
this.measureResult = r;
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
this.measureResult = r;
|
|
1092
|
+
// 측정에 쓴 프레임 중 가운데 프레임에 측정선 오버레이
|
|
1093
|
+
const frames = r.frames_used || [];
|
|
1094
|
+
const overlayIdx = frames.length ? frames[Math.floor(frames.length / 2)] : 0;
|
|
1095
|
+
try {
|
|
1096
|
+
this.measureImageUrl = await this._fetchMeasureImage(`/api/rebar/overlay/${job}/${overlayIdx}?w=1280`);
|
|
1097
|
+
}
|
|
1098
|
+
catch (e) {
|
|
1099
|
+
console.warn('오버레이 수신 실패:', e);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
console.error('철근 측정 실패:', error);
|
|
1104
|
+
notify({ message: error.message, level: 'error' });
|
|
1105
|
+
}
|
|
1106
|
+
finally {
|
|
1107
|
+
this.measureRunning = false;
|
|
1108
|
+
this.measureMsg = '';
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/* ─── ⑤ 기둥·벽체 두께 측정 (프레임 선택 + 면 클릭, 동기) ─── */
|
|
1112
|
+
async _loadThicknessFrames() {
|
|
1113
|
+
const set = this.uploadedSets[this.measureSetIndex];
|
|
1114
|
+
if (!set)
|
|
1115
|
+
return;
|
|
1116
|
+
try {
|
|
1117
|
+
this.measureRunning = true;
|
|
1118
|
+
this.measureMsg = '스윕에서 프레임 뽑는 중... (첫 호출은 10초쯤)';
|
|
1119
|
+
const data = await this._callMeasure(`/api/thickness/frames/${set.setId}`);
|
|
1120
|
+
const frames = (data.frames || []);
|
|
1121
|
+
// 썸네일은 인증 때문에 fetch로 받아 blob URL로
|
|
1122
|
+
const withThumbs = [];
|
|
1123
|
+
for (const f of frames) {
|
|
1124
|
+
try {
|
|
1125
|
+
const thumbUrl = await this._fetchMeasureImage(`/api/thickness/frame/${set.setId}/${f.slot}?thumb=1`);
|
|
1126
|
+
withThumbs.push(Object.assign(Object.assign({}, f), { thumbUrl }));
|
|
1127
|
+
}
|
|
1128
|
+
catch (_a) {
|
|
1129
|
+
withThumbs.push(f);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
this.thicknessFrames = withThumbs;
|
|
1133
|
+
if (withThumbs.length)
|
|
1134
|
+
await this._selectThicknessFrame(withThumbs[Math.floor(withThumbs.length / 2)].slot);
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
console.error('프레임 목록 실패:', error);
|
|
1138
|
+
notify({ message: error.message, level: 'error' });
|
|
1139
|
+
}
|
|
1140
|
+
finally {
|
|
1141
|
+
this.measureRunning = false;
|
|
1142
|
+
this.measureMsg = '';
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async _selectThicknessFrame(slot) {
|
|
1146
|
+
const set = this.uploadedSets[this.measureSetIndex];
|
|
1147
|
+
if (!set)
|
|
1148
|
+
return;
|
|
1149
|
+
this.thicknessSlot = slot;
|
|
1150
|
+
this.thicknessClick = null;
|
|
1151
|
+
try {
|
|
1152
|
+
this.thicknessFrameUrl = await this._fetchMeasureImage(`/api/thickness/frame/${set.setId}/${slot}`);
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
notify({ message: error.message, level: 'error' });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async _runThicknessMeasure(item) {
|
|
1159
|
+
const set = this.uploadedSets[this.measureSetIndex];
|
|
1160
|
+
if (!set || this.thicknessSlot === null || !this.thicknessClick) {
|
|
1161
|
+
notify({ message: '프레임을 고르고 잴 면을 탭해주세요.', level: 'warn' });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
try {
|
|
1165
|
+
this.measureRunning = true;
|
|
1166
|
+
this.measureMsg = '측정 중... (이 프레임 첫 측정이면 10~20초)';
|
|
1167
|
+
this.measureResult = null;
|
|
1168
|
+
this.measureImageUrl = '';
|
|
1169
|
+
const form = new FormData();
|
|
1170
|
+
form.append('set_id', set.setId);
|
|
1171
|
+
form.append('slot', String(this.thicknessSlot));
|
|
1172
|
+
form.append('x', String(this.thicknessClick.x));
|
|
1173
|
+
form.append('y', String(this.thicknessClick.y));
|
|
1174
|
+
if (this.measureInputs.design_mm)
|
|
1175
|
+
form.append('design_mm', this.measureInputs.design_mm);
|
|
1176
|
+
if (this.measureInputs.tol_mm)
|
|
1177
|
+
form.append('tol_mm', this.measureInputs.tol_mm);
|
|
1178
|
+
const r = await this._callMeasure('/api/thickness/measure', { method: 'POST', body: form });
|
|
1179
|
+
this.measureResult = r;
|
|
1180
|
+
if (r.image_data)
|
|
1181
|
+
this.measureImageUrl = r.image_data;
|
|
1182
|
+
if (!r.ok)
|
|
1183
|
+
notify({ message: r.summary || r.headline || '측정에 실패했습니다.', level: 'warn' });
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
console.error('두께 측정 실패:', error);
|
|
1187
|
+
notify({ message: error.message, level: 'error' });
|
|
1188
|
+
}
|
|
1189
|
+
finally {
|
|
1190
|
+
this.measureRunning = false;
|
|
1191
|
+
this.measureMsg = '';
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/* ─── 측정 결과 → 판정 확정 (apply-measurement, OpenAI 미호출·즉시 응답) ─── */
|
|
1195
|
+
async _applyMeasurement(item) {
|
|
1196
|
+
var _a;
|
|
1197
|
+
if (!this.measureResult)
|
|
1198
|
+
return;
|
|
1199
|
+
try {
|
|
1200
|
+
this.loading = true;
|
|
1201
|
+
this.loadingText = '판정 확정 중...';
|
|
1202
|
+
// 측정 서버 응답을 가공 없이 그대로 전달 — 품질 게이트/hold 승격은 엔진이 처리한다
|
|
1203
|
+
const result = await this._callEngine({
|
|
1204
|
+
action: 'apply-measurement',
|
|
1205
|
+
item: { item_id: item.item_id, text: item.text, category: item.category },
|
|
1206
|
+
model_id: ((_a = item.measure) === null || _a === void 0 ? void 0 : _a.model_id) || this.measureResult.model_id,
|
|
1207
|
+
result: this.measureResult
|
|
1208
|
+
});
|
|
1209
|
+
const judged = Object.assign(Object.assign({}, item), result.item);
|
|
1210
|
+
this._mergeItems([judged]);
|
|
1211
|
+
this._resetMeasureState();
|
|
1212
|
+
this.panel = 'none';
|
|
1213
|
+
this.selectedItem = null;
|
|
1214
|
+
if (judged.verdict === 'hold') {
|
|
1215
|
+
notify({ message: '측정 품질이 기준에 못 미쳐 보류되었습니다. 재촬영을 권장합니다.', level: 'warn' });
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
notify({ message: `판정이 확정되었습니다: ${this._verdictLabel(judged)}` });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
catch (error) {
|
|
1222
|
+
console.error('판정 확정 실패:', error);
|
|
1223
|
+
notify({ message: error.message, level: 'error' });
|
|
1224
|
+
}
|
|
1225
|
+
finally {
|
|
1226
|
+
this.loading = false;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/* base64 data URL → File (측정 근거 이미지 첨부용) */
|
|
1230
|
+
_dataUrlToFile(dataUrl, name) {
|
|
1231
|
+
var _a;
|
|
1232
|
+
try {
|
|
1233
|
+
const [head, body] = dataUrl.split(',');
|
|
1234
|
+
const mime = ((_a = head.match(/data:(.*?);/)) === null || _a === void 0 ? void 0 : _a[1]) || 'image/jpeg';
|
|
1235
|
+
const bin = atob(body);
|
|
1236
|
+
const bytes = new Uint8Array(bin.length);
|
|
1237
|
+
for (let i = 0; i < bin.length; i++)
|
|
1238
|
+
bytes[i] = bin.charCodeAt(i);
|
|
1239
|
+
return new File([bytes], name, { type: mime });
|
|
1240
|
+
}
|
|
1241
|
+
catch (_b) {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/* ─── 검사 등록: 감리자 판정 자동 반영 + AI판단 근거 첨부 ─── */
|
|
657
1246
|
async _registerInspection() {
|
|
658
1247
|
var _a, _b;
|
|
659
1248
|
const items = this._sortedItems();
|
|
@@ -684,7 +1273,8 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
684
1273
|
if (response.errors)
|
|
685
1274
|
throw new Error(((_a = response.errors[0]) === null || _a === void 0 ? void 0 : _a.message) || '검사 등록에 실패했습니다.');
|
|
686
1275
|
}
|
|
687
|
-
// 2) 항목별 첨부: 판단 근거(AI판단 텍스트) + 근거
|
|
1276
|
+
// 2) 항목별 첨부: 판단 근거(AI판단 텍스트) + 근거 프레임/측정 근거 이미지
|
|
1277
|
+
// ai_report는 pending/error에서 빈 문자열로 오므로 그 항목은 첨부하지 않는다
|
|
688
1278
|
const attachments = [];
|
|
689
1279
|
for (const it of items) {
|
|
690
1280
|
if (it.ai_report) {
|
|
@@ -701,6 +1291,12 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
|
|
|
701
1291
|
attachments.push({ file: photo.file, refBy: it.item_id, refType: 'ChecklistItem', description: 'AI판단' });
|
|
702
1292
|
}
|
|
703
1293
|
}
|
|
1294
|
+
// 측정 항목의 근거 이미지 (apply-measurement 응답의 base64)
|
|
1295
|
+
if (it.image_data) {
|
|
1296
|
+
const file = this._dataUrlToFile(it.image_data, `AI측정근거_${(it.text || it.item_id).slice(0, 30)}.jpg`);
|
|
1297
|
+
if (file)
|
|
1298
|
+
attachments.push({ file, refBy: it.item_id, refType: 'ChecklistItem', description: 'AI판단' });
|
|
1299
|
+
}
|
|
704
1300
|
}
|
|
705
1301
|
if (attachments.length) {
|
|
706
1302
|
const attachResponse = await client.mutate({
|
|
@@ -839,68 +1435,95 @@ BuildingInspectionAiMeasurement.styles = [
|
|
|
839
1435
|
margin-top: 6px;
|
|
840
1436
|
}
|
|
841
1437
|
|
|
842
|
-
|
|
1438
|
+
/* 촬영 안내 그림 — 픽토그램은 배경 투명·짙은 글자라 밝은 카드 위에만 얹는다 */
|
|
1439
|
+
.capture-guide-img {
|
|
1440
|
+
width: 100%;
|
|
1441
|
+
border-radius: 8px;
|
|
1442
|
+
display: block;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
.picto-strip {
|
|
1446
|
+
display: flex;
|
|
1447
|
+
gap: 8px;
|
|
1448
|
+
margin-top: 10px;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.picto-strip img {
|
|
1452
|
+
flex: 1;
|
|
1453
|
+
min-width: 0;
|
|
1454
|
+
background: #fff;
|
|
1455
|
+
border: 1px solid #eee;
|
|
1456
|
+
border-radius: 8px;
|
|
1457
|
+
padding: 6px;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/* 세트 업로드 카드 */
|
|
1461
|
+
.set-card {
|
|
843
1462
|
display: flex;
|
|
844
|
-
justify-content: space-between;
|
|
845
1463
|
align-items: center;
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1464
|
+
gap: 10px;
|
|
1465
|
+
border: 1px dashed #ccc;
|
|
1466
|
+
border-radius: 8px;
|
|
1467
|
+
background: #fafafa;
|
|
1468
|
+
padding: 12px;
|
|
1469
|
+
cursor: pointer;
|
|
849
1470
|
}
|
|
850
1471
|
|
|
851
|
-
.
|
|
852
|
-
|
|
853
|
-
border-
|
|
854
|
-
background: #
|
|
855
|
-
|
|
1472
|
+
.set-card[uploaded] {
|
|
1473
|
+
border-style: solid;
|
|
1474
|
+
border-color: #bfd8bf;
|
|
1475
|
+
background: #f2f8f2;
|
|
1476
|
+
cursor: default;
|
|
856
1477
|
}
|
|
857
1478
|
|
|
858
|
-
.
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1479
|
+
.set-card md-icon {
|
|
1480
|
+
--md-icon-size: 28px;
|
|
1481
|
+
color: #b5b5b5;
|
|
1482
|
+
flex-shrink: 0;
|
|
862
1483
|
}
|
|
863
1484
|
|
|
864
|
-
.
|
|
1485
|
+
.set-card[uploaded] md-icon {
|
|
1486
|
+
color: #2e7d32;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.set-card .set-main {
|
|
1490
|
+
flex: 1;
|
|
1491
|
+
min-width: 0;
|
|
865
1492
|
font-size: 13px;
|
|
866
|
-
|
|
1493
|
+
color: #555;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.set-card .set-main b {
|
|
1497
|
+
display: block;
|
|
867
1498
|
color: #333;
|
|
1499
|
+
margin-bottom: 2px;
|
|
868
1500
|
}
|
|
869
1501
|
|
|
870
|
-
.
|
|
871
|
-
|
|
872
|
-
|
|
1502
|
+
.set-card .set-warn {
|
|
1503
|
+
font-size: 11px;
|
|
1504
|
+
color: #b7791f;
|
|
873
1505
|
}
|
|
874
1506
|
|
|
875
|
-
.
|
|
876
|
-
flex: 1;
|
|
877
|
-
aspect-ratio: 1;
|
|
878
|
-
border: 1px dashed #ccc;
|
|
879
|
-
border-radius: 8px;
|
|
880
|
-
background: #fafafa;
|
|
1507
|
+
.progress-row {
|
|
881
1508
|
display: flex;
|
|
882
|
-
|
|
883
|
-
justify-content: center;
|
|
1509
|
+
justify-content: space-between;
|
|
884
1510
|
align-items: center;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
text-align: center;
|
|
889
|
-
padding: 4px;
|
|
890
|
-
overflow: hidden;
|
|
891
|
-
cursor: pointer;
|
|
1511
|
+
font-size: 13px;
|
|
1512
|
+
color: #555;
|
|
1513
|
+
font-weight: 600;
|
|
892
1514
|
}
|
|
893
1515
|
|
|
894
|
-
.
|
|
895
|
-
|
|
896
|
-
|
|
1516
|
+
.progress-bar {
|
|
1517
|
+
height: 6px;
|
|
1518
|
+
border-radius: 3px;
|
|
1519
|
+
background: #eee;
|
|
1520
|
+
overflow: hidden;
|
|
897
1521
|
}
|
|
898
1522
|
|
|
899
|
-
.
|
|
900
|
-
width: 100%;
|
|
1523
|
+
.progress-bar div {
|
|
901
1524
|
height: 100%;
|
|
902
|
-
|
|
903
|
-
|
|
1525
|
+
background: #7d2f2f;
|
|
1526
|
+
transition: width 0.3s;
|
|
904
1527
|
}
|
|
905
1528
|
|
|
906
1529
|
.primary-button {
|
|
@@ -1016,6 +1639,13 @@ BuildingInspectionAiMeasurement.styles = [
|
|
|
1016
1639
|
margin-top: 4px;
|
|
1017
1640
|
}
|
|
1018
1641
|
|
|
1642
|
+
.group-subtitle {
|
|
1643
|
+
font-size: 12px;
|
|
1644
|
+
font-weight: 700;
|
|
1645
|
+
color: #777;
|
|
1646
|
+
margin: 2px 0 0 4px;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1019
1649
|
.item-card {
|
|
1020
1650
|
display: flex;
|
|
1021
1651
|
gap: 10px;
|
|
@@ -1120,11 +1750,23 @@ BuildingInspectionAiMeasurement.styles = [
|
|
|
1120
1750
|
cursor: pointer;
|
|
1121
1751
|
}
|
|
1122
1752
|
|
|
1123
|
-
|
|
1753
|
+
.measure-button {
|
|
1754
|
+
flex-shrink: 0;
|
|
1755
|
+
border: none;
|
|
1756
|
+
border-radius: 14px;
|
|
1757
|
+
background: #e8eef7;
|
|
1758
|
+
color: #2f5d8a;
|
|
1759
|
+
font-size: 12px;
|
|
1760
|
+
font-weight: 700;
|
|
1761
|
+
padding: 6px 12px;
|
|
1762
|
+
cursor: pointer;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/* ─── 우측 패널 (상세/재촬영/측정/리포트) ─── */
|
|
1124
1766
|
.side-pane {
|
|
1125
1767
|
flex: 1;
|
|
1126
|
-
min-width:
|
|
1127
|
-
max-width:
|
|
1768
|
+
min-width: 300px;
|
|
1769
|
+
max-width: 400px;
|
|
1128
1770
|
background: #fff;
|
|
1129
1771
|
border: 1px solid #e5e5e5;
|
|
1130
1772
|
border-radius: 10px;
|
|
@@ -1209,6 +1851,12 @@ BuildingInspectionAiMeasurement.styles = [
|
|
|
1209
1851
|
padding: 8px 10px;
|
|
1210
1852
|
}
|
|
1211
1853
|
|
|
1854
|
+
.evidence-img {
|
|
1855
|
+
width: 100%;
|
|
1856
|
+
border-radius: 8px;
|
|
1857
|
+
border: 1px solid #eee;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1212
1860
|
.why-box {
|
|
1213
1861
|
background: #fff8e1;
|
|
1214
1862
|
border: 1px solid #f4e4b5;
|
|
@@ -1219,6 +1867,87 @@ BuildingInspectionAiMeasurement.styles = [
|
|
|
1219
1867
|
line-height: 1.6;
|
|
1220
1868
|
}
|
|
1221
1869
|
|
|
1870
|
+
/* 측정 패널 */
|
|
1871
|
+
.measure-field {
|
|
1872
|
+
display: flex;
|
|
1873
|
+
flex-direction: column;
|
|
1874
|
+
gap: 4px;
|
|
1875
|
+
font-size: 12px;
|
|
1876
|
+
color: #555;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
.measure-field input,
|
|
1880
|
+
.measure-field select {
|
|
1881
|
+
padding: 9px 10px;
|
|
1882
|
+
border: 1px solid #ccc;
|
|
1883
|
+
border-radius: 6px;
|
|
1884
|
+
font-size: 14px;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.measure-msg {
|
|
1888
|
+
display: flex;
|
|
1889
|
+
align-items: center;
|
|
1890
|
+
gap: 8px;
|
|
1891
|
+
font-size: 13px;
|
|
1892
|
+
color: #7d2f2f;
|
|
1893
|
+
font-weight: 600;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
.measure-msg md-circular-progress {
|
|
1897
|
+
--md-circular-progress-size: 22px;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
.frame-strip {
|
|
1901
|
+
display: flex;
|
|
1902
|
+
gap: 6px;
|
|
1903
|
+
overflow-x: auto;
|
|
1904
|
+
padding-bottom: 4px;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
.frame-strip img {
|
|
1908
|
+
height: 60px;
|
|
1909
|
+
border-radius: 6px;
|
|
1910
|
+
border: 2px solid transparent;
|
|
1911
|
+
cursor: pointer;
|
|
1912
|
+
opacity: 0.7;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
.frame-strip img[selected] {
|
|
1916
|
+
border-color: #7d2f2f;
|
|
1917
|
+
opacity: 1;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
.click-stage {
|
|
1921
|
+
position: relative;
|
|
1922
|
+
line-height: 0;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
.click-stage img {
|
|
1926
|
+
width: 100%;
|
|
1927
|
+
border-radius: 8px;
|
|
1928
|
+
cursor: crosshair;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
.click-dot {
|
|
1932
|
+
position: absolute;
|
|
1933
|
+
width: 24px;
|
|
1934
|
+
height: 24px;
|
|
1935
|
+
margin: -12px 0 0 -12px;
|
|
1936
|
+
border: 3px solid #ffd400;
|
|
1937
|
+
border-radius: 50%;
|
|
1938
|
+
pointer-events: none;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
.measure-summary {
|
|
1942
|
+
background: #f7f7f7;
|
|
1943
|
+
border-radius: 8px;
|
|
1944
|
+
padding: 10px;
|
|
1945
|
+
font-size: 12px;
|
|
1946
|
+
color: #333;
|
|
1947
|
+
line-height: 1.7;
|
|
1948
|
+
white-space: pre-line;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1222
1951
|
.report-counts {
|
|
1223
1952
|
display: flex;
|
|
1224
1953
|
text-align: center;
|
|
@@ -1368,6 +2097,10 @@ __decorate([
|
|
|
1368
2097
|
state(),
|
|
1369
2098
|
__metadata("design:type", Object)
|
|
1370
2099
|
], BuildingInspectionAiMeasurement.prototype, "guide", void 0);
|
|
2100
|
+
__decorate([
|
|
2101
|
+
state(),
|
|
2102
|
+
__metadata("design:type", Array)
|
|
2103
|
+
], BuildingInspectionAiMeasurement.prototype, "sets", void 0);
|
|
1371
2104
|
__decorate([
|
|
1372
2105
|
state(),
|
|
1373
2106
|
__metadata("design:type", Array)
|
|
@@ -1392,10 +2125,54 @@ __decorate([
|
|
|
1392
2125
|
state(),
|
|
1393
2126
|
__metadata("design:type", Number)
|
|
1394
2127
|
], BuildingInspectionAiMeasurement.prototype, "recaptureBase", void 0);
|
|
2128
|
+
__decorate([
|
|
2129
|
+
state(),
|
|
2130
|
+
__metadata("design:type", Object)
|
|
2131
|
+
], BuildingInspectionAiMeasurement.prototype, "recaptureSet", void 0);
|
|
1395
2132
|
__decorate([
|
|
1396
2133
|
state(),
|
|
1397
2134
|
__metadata("design:type", Object)
|
|
1398
2135
|
], BuildingInspectionAiMeasurement.prototype, "expandedReportId", void 0);
|
|
2136
|
+
__decorate([
|
|
2137
|
+
state(),
|
|
2138
|
+
__metadata("design:type", Number)
|
|
2139
|
+
], BuildingInspectionAiMeasurement.prototype, "measureSetIndex", void 0);
|
|
2140
|
+
__decorate([
|
|
2141
|
+
state(),
|
|
2142
|
+
__metadata("design:type", Object)
|
|
2143
|
+
], BuildingInspectionAiMeasurement.prototype, "measureInputs", void 0);
|
|
2144
|
+
__decorate([
|
|
2145
|
+
state(),
|
|
2146
|
+
__metadata("design:type", Boolean)
|
|
2147
|
+
], BuildingInspectionAiMeasurement.prototype, "measureRunning", void 0);
|
|
2148
|
+
__decorate([
|
|
2149
|
+
state(),
|
|
2150
|
+
__metadata("design:type", String)
|
|
2151
|
+
], BuildingInspectionAiMeasurement.prototype, "measureMsg", void 0);
|
|
2152
|
+
__decorate([
|
|
2153
|
+
state(),
|
|
2154
|
+
__metadata("design:type", Object)
|
|
2155
|
+
], BuildingInspectionAiMeasurement.prototype, "measureResult", void 0);
|
|
2156
|
+
__decorate([
|
|
2157
|
+
state(),
|
|
2158
|
+
__metadata("design:type", String)
|
|
2159
|
+
], BuildingInspectionAiMeasurement.prototype, "measureImageUrl", void 0);
|
|
2160
|
+
__decorate([
|
|
2161
|
+
state(),
|
|
2162
|
+
__metadata("design:type", Array)
|
|
2163
|
+
], BuildingInspectionAiMeasurement.prototype, "thicknessFrames", void 0);
|
|
2164
|
+
__decorate([
|
|
2165
|
+
state(),
|
|
2166
|
+
__metadata("design:type", Object)
|
|
2167
|
+
], BuildingInspectionAiMeasurement.prototype, "thicknessSlot", void 0);
|
|
2168
|
+
__decorate([
|
|
2169
|
+
state(),
|
|
2170
|
+
__metadata("design:type", String)
|
|
2171
|
+
], BuildingInspectionAiMeasurement.prototype, "thicknessFrameUrl", void 0);
|
|
2172
|
+
__decorate([
|
|
2173
|
+
state(),
|
|
2174
|
+
__metadata("design:type", Object)
|
|
2175
|
+
], BuildingInspectionAiMeasurement.prototype, "thicknessClick", void 0);
|
|
1399
2176
|
__decorate([
|
|
1400
2177
|
state(),
|
|
1401
2178
|
__metadata("design:type", Boolean)
|
|
@@ -1409,9 +2186,11 @@ __decorate([
|
|
|
1409
2186
|
__metadata("design:type", Object)
|
|
1410
2187
|
], BuildingInspectionAiMeasurement.prototype, "canMutate", void 0);
|
|
1411
2188
|
__decorate([
|
|
1412
|
-
query('#
|
|
1413
|
-
__metadata("design:type", HTMLInputElement
|
|
1414
|
-
|
|
2189
|
+
query('#setInput'),
|
|
2190
|
+
__metadata("design:type", HTMLInputElement
|
|
2191
|
+
/* 세트 업로드 대상: 촬영 건 인덱스(가이드 화면) 또는 -1(재촬영 패널) */
|
|
2192
|
+
)
|
|
2193
|
+
], BuildingInspectionAiMeasurement.prototype, "setInputEl", void 0);
|
|
1415
2194
|
BuildingInspectionAiMeasurement = __decorate([
|
|
1416
2195
|
customElement('building-inspection-detail-ai-measurement')
|
|
1417
2196
|
], BuildingInspectionAiMeasurement);
|