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