@dssp/supervision 1.0.18 → 1.0.20

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.
@@ -9,6 +9,7 @@ import { customElement, state } from 'lit/decorators.js';
9
9
  import { PageView } from '@operato/shell';
10
10
  import { CommonGristStyles, ScrollbarStyles } from '@operato/styles';
11
11
  import { client } from '@operato/graphql';
12
+ import { notify } from '@operato/layout';
12
13
  import './component/building-inspection-detail-header';
13
14
  let BuildingInspectionAiDefect = class BuildingInspectionAiDefect extends PageView {
14
15
  constructor() {
@@ -90,6 +91,12 @@ let BuildingInspectionAiDefect = class BuildingInspectionAiDefect extends PageVi
90
91
  if (response.errors)
91
92
  return;
92
93
  this.buildingInspection = response.data.buildingInspection;
94
+ // 삭제(soft delete)됐거나 다른 projectType의 검측이면 서버가 null을 준다.
95
+ // 그대로 진행하면 뒤 쿼리에 undefined가 실려 GraphQL 에러가 난다.
96
+ if (!this.buildingInspection) {
97
+ notify({ message: '삭제되었거나 접근할 수 없는 검측입니다.', level: 'warn' });
98
+ return;
99
+ }
93
100
  await this._getProjectByBuildingComplexId((_d = (_c = (_b = (_a = this.buildingInspection) === null || _a === void 0 ? void 0 : _a.buildingLevel) === null || _b === void 0 ? void 0 : _b.building) === null || _c === void 0 ? void 0 : _c.buildingComplex) === null || _d === void 0 ? void 0 : _d.id);
94
101
  }
95
102
  async _getProjectByBuildingComplexId(buildingComplexId) {
@@ -1 +1 @@
1
- {"version":3,"file":"building-inspection-detail-ai-defect.js","sourceRoot":"","sources":["../../../client/pages/building-inspection/building-inspection-detail-ai-defect.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/input/ox-input-switch.js'\nimport '@operato/mini-map/ox-zoomable-image.js'\nimport '@material/web/progress/circular-progress.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, PropertyValues } from 'lit'\nimport { customElement, state, query } from 'lit/decorators.js'\n\nimport { PageView } from '@operato/shell'\nimport { CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { PageLifecycle } from '@operato/shell/dist/src/app/pages/page-view'\nimport { client } from '@operato/graphql'\n\nimport './component/building-inspection-detail-header'\n\n@customElement('building-inspection-detail-ai-defect')\nexport class BuildingInspectionAiDefect extends PageView {\n static styles = [\n ScrollbarStyles,\n CommonGristStyles,\n css`\n :host {\n display: grid;\n grid-template-rows: 75px 1fr;\n color: #4e5055;\n width: 100%;\n height: 100%;\n background-color: #f7f7f7;\n }\n\n div[body] {\n display: flex;\n justify-items: center;\n gap: var(--spacing-medium);\n margin: var(--spacing-medium);\n min-height: 0;\n }\n\n div[preview] {\n flex: 1;\n border: 2px solid #ddd;\n border-radius: 10px;\n\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 0;\n overflow: hidden;\n }\n\n div[preview] video {\n flex: 1;\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n }\n `\n ]\n\n @state() project: any = {}\n @state() buildingInspection: any = {}\n @state() buildingInspectionId: string = ''\n\n get context() {\n return {\n title: '검측 관리 상세 - AI 하자 분석'\n }\n }\n\n render() {\n return html`\n <building-inspection-detail-header\n .buildingInspectionId=${this.buildingInspection?.id}\n .buildingLevelId=${this.buildingInspection?.buildingLevel?.id}\n .projectName=${this.project.name}\n .buildingName=${this.buildingInspection?.buildingLevel?.building?.name}\n .buildingLevelFloor=${this.buildingInspection?.buildingLevel?.floor}\n .projectId=${this.project.id}\n ></building-inspection-detail-header>\n\n <div body>\n <div preview>\n <video src=\"/attachment/21e53688-5262-4fae-961a-591db7114b75.mp4\" controls playsinline></video>\n </div>\n </div>\n `\n }\n\n protected async updated(changes: PropertyValues): Promise<void> {}\n\n async pageUpdated(changes: any, lifecycle: PageLifecycle) {\n if (this.active) {\n this.buildingInspectionId = lifecycle.resourceId || ''\n await this.initBuildingInspection(this.buildingInspectionId)\n }\n }\n\n async initBuildingInspection(buildingInspectionId: string = '') {\n const response = await client.query({\n query: gql`\n query BuildingInspection($buildingInspectionId: String!) {\n buildingInspection(id: $buildingInspectionId) {\n id\n status\n requestDate\n drawingMarker\n checklist {\n id\n location\n inspectionDrawingType\n }\n buildingLevel {\n id\n floor\n mainDrawing {\n id\n name\n fullpath\n }\n mainDrawingImage\n building {\n id\n name\n buildingComplex {\n id\n }\n }\n }\n }\n }\n `,\n variables: {\n buildingInspectionId\n }\n })\n\n if (response.errors) return\n\n this.buildingInspection = response.data.buildingInspection\n\n await this._getProjectByBuildingComplexId(this.buildingInspection?.buildingLevel?.building?.buildingComplex?.id)\n }\n\n private async _getProjectByBuildingComplexId(buildingComplexId) {\n const response = await client.query({\n query: gql`\n query ProjectByBuildingComplexId($buildingComplexId: String!) {\n project: projectByBuildingComplexId(buildingComplexId: $buildingComplexId) {\n id\n name\n }\n }\n `,\n variables: {\n buildingComplexId\n }\n })\n\n if (response.errors) return\n\n this.project = response.data.project\n }\n}\n"]}
1
+ {"version":3,"file":"building-inspection-detail-ai-defect.js","sourceRoot":"","sources":["../../../client/pages/building-inspection/building-inspection-detail-ai-defect.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,mCAAmC,CAAA;AAC1C,OAAO,wCAAwC,CAAA;AAC/C,OAAO,6CAA6C,CAAA;AAEpD,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAkB,MAAM,KAAK,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,KAAK,EAAS,MAAM,mBAAmB,CAAA;AAE/D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEpE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,OAAO,+CAA+C,CAAA;AAG/C,IAAM,0BAA0B,GAAhC,MAAM,0BAA2B,SAAQ,QAAQ;IAAjD;;QA2CI,YAAO,GAAQ,EAAE,CAAA;QACjB,uBAAkB,GAAQ,EAAE,CAAA;QAC5B,yBAAoB,GAAW,EAAE,CAAA;IA4G5C,CAAC;IA1GC,IAAI,OAAO;QACT,OAAO;YACL,KAAK,EAAE,qBAAqB;SAC7B,CAAA;IACH,CAAC;IAED,MAAM;;QACJ,OAAO,IAAI,CAAA;;gCAEiB,MAAA,IAAI,CAAC,kBAAkB,0CAAE,EAAE;2BAChC,MAAA,MAAA,IAAI,CAAC,kBAAkB,0CAAE,aAAa,0CAAE,EAAE;uBAC9C,IAAI,CAAC,OAAO,CAAC,IAAI;wBAChB,MAAA,MAAA,MAAA,IAAI,CAAC,kBAAkB,0CAAE,aAAa,0CAAE,QAAQ,0CAAE,IAAI;8BAChD,MAAA,MAAA,IAAI,CAAC,kBAAkB,0CAAE,aAAa,0CAAE,KAAK;qBACtD,IAAI,CAAC,OAAO,CAAC,EAAE;;;;;;;;KAQ/B,CAAA;IACH,CAAC;IAES,KAAK,CAAC,OAAO,CAAC,OAAuB,IAAkB,CAAC;IAElE,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,SAAwB;QACtD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,oBAAoB,GAAG,SAAS,CAAC,UAAU,IAAI,EAAE,CAAA;YACtD,MAAM,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;QAC9D,CAAC;IACH,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,uBAA+B,EAAE;;QAC5D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BT;YACD,SAAS,EAAE;gBACT,oBAAoB;aACrB;SACF,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM;YAAE,OAAM;QAE3B,IAAI,CAAC,kBAAkB,GAAG,QAAQ,CAAC,IAAI,CAAC,kBAAkB,CAAA;QAE1D,wDAAwD;QACxD,+CAA+C;QAC/C,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC7B,MAAM,CAAC,EAAE,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;YAC5D,OAAM;QACR,CAAC;QAED,MAAM,IAAI,CAAC,8BAA8B,CAAC,MAAA,MAAA,MAAA,MAAA,IAAI,CAAC,kBAAkB,0CAAE,aAAa,0CAAE,QAAQ,0CAAE,eAAe,0CAAE,EAAE,CAAC,CAAA;IAClH,CAAC;IAEO,KAAK,CAAC,8BAA8B,CAAC,iBAAiB;QAC5D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;OAOT;YACD,SAAS,EAAE;gBACT,iBAAiB;aAClB;SACF,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM;YAAE,OAAM;QAE3B,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAA;IACtC,CAAC;;AAvJM,iCAAM,GAAG;IACd,eAAe;IACf,iBAAiB;IACjB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoCF;CACF,AAxCY,CAwCZ;AAEQ;IAAR,KAAK,EAAE;;2DAAkB;AACjB;IAAR,KAAK,EAAE;;sEAA6B;AAC5B;IAAR,KAAK,EAAE;;wEAAkC;AA7C/B,0BAA0B;IADtC,aAAa,CAAC,sCAAsC,CAAC;GACzC,0BAA0B,CAyJtC","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/input/ox-input-switch.js'\nimport '@operato/mini-map/ox-zoomable-image.js'\nimport '@material/web/progress/circular-progress.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, PropertyValues } from 'lit'\nimport { customElement, state, query } from 'lit/decorators.js'\n\nimport { PageView } from '@operato/shell'\nimport { CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { PageLifecycle } from '@operato/shell/dist/src/app/pages/page-view'\nimport { client } from '@operato/graphql'\nimport { notify } from '@operato/layout'\n\nimport './component/building-inspection-detail-header'\n\n@customElement('building-inspection-detail-ai-defect')\nexport class BuildingInspectionAiDefect extends PageView {\n static styles = [\n ScrollbarStyles,\n CommonGristStyles,\n css`\n :host {\n display: grid;\n grid-template-rows: 75px 1fr;\n color: #4e5055;\n width: 100%;\n height: 100%;\n background-color: #f7f7f7;\n }\n\n div[body] {\n display: flex;\n justify-items: center;\n gap: var(--spacing-medium);\n margin: var(--spacing-medium);\n min-height: 0;\n }\n\n div[preview] {\n flex: 1;\n border: 2px solid #ddd;\n border-radius: 10px;\n\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 0;\n overflow: hidden;\n }\n\n div[preview] video {\n flex: 1;\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n }\n `\n ]\n\n @state() project: any = {}\n @state() buildingInspection: any = {}\n @state() buildingInspectionId: string = ''\n\n get context() {\n return {\n title: '검측 관리 상세 - AI 하자 분석'\n }\n }\n\n render() {\n return html`\n <building-inspection-detail-header\n .buildingInspectionId=${this.buildingInspection?.id}\n .buildingLevelId=${this.buildingInspection?.buildingLevel?.id}\n .projectName=${this.project.name}\n .buildingName=${this.buildingInspection?.buildingLevel?.building?.name}\n .buildingLevelFloor=${this.buildingInspection?.buildingLevel?.floor}\n .projectId=${this.project.id}\n ></building-inspection-detail-header>\n\n <div body>\n <div preview>\n <video src=\"/attachment/21e53688-5262-4fae-961a-591db7114b75.mp4\" controls playsinline></video>\n </div>\n </div>\n `\n }\n\n protected async updated(changes: PropertyValues): Promise<void> {}\n\n async pageUpdated(changes: any, lifecycle: PageLifecycle) {\n if (this.active) {\n this.buildingInspectionId = lifecycle.resourceId || ''\n await this.initBuildingInspection(this.buildingInspectionId)\n }\n }\n\n async initBuildingInspection(buildingInspectionId: string = '') {\n const response = await client.query({\n query: gql`\n query BuildingInspection($buildingInspectionId: String!) {\n buildingInspection(id: $buildingInspectionId) {\n id\n status\n requestDate\n drawingMarker\n checklist {\n id\n location\n inspectionDrawingType\n }\n buildingLevel {\n id\n floor\n mainDrawing {\n id\n name\n fullpath\n }\n mainDrawingImage\n building {\n id\n name\n buildingComplex {\n id\n }\n }\n }\n }\n }\n `,\n variables: {\n buildingInspectionId\n }\n })\n\n if (response.errors) return\n\n this.buildingInspection = response.data.buildingInspection\n\n // 삭제(soft delete)됐거나 다른 projectType의 검측이면 서버가 null을 준다.\n // 그대로 진행하면 뒤 쿼리에 undefined가 실려 GraphQL 에러가 난다.\n if (!this.buildingInspection) {\n notify({ message: '삭제되었거나 접근할 수 없는 검측입니다.', level: 'warn' })\n return\n }\n\n await this._getProjectByBuildingComplexId(this.buildingInspection?.buildingLevel?.building?.buildingComplex?.id)\n }\n\n private async _getProjectByBuildingComplexId(buildingComplexId) {\n const response = await client.query({\n query: gql`\n query ProjectByBuildingComplexId($buildingComplexId: String!) {\n project: projectByBuildingComplexId(buildingComplexId: $buildingComplexId) {\n id\n name\n }\n }\n `,\n variables: {\n buildingComplexId\n }\n })\n\n if (response.errors) return\n\n this.project = response.data.project\n }\n}\n"]}
@@ -41,6 +41,7 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
41
41
  setPickerFor: number | null;
42
42
  recentSets: any[];
43
43
  recentLoading: boolean;
44
+ private autoAttachSince;
44
45
  expandedReportId: string | null;
45
46
  measureSetIndex: number;
46
47
  measureInputs: Record<string, string>;
@@ -76,6 +77,8 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
76
77
  item_id: any;
77
78
  category: string;
78
79
  text: any;
80
+ criteria_kcs: string;
81
+ criteria_site: any;
79
82
  }[];
80
83
  get uploadedSets(): SetInfo[];
81
84
  render(): import("lit-html").TemplateResult<1>;
@@ -98,6 +101,8 @@ export declare class BuildingInspectionAiMeasurement extends PageView {
98
101
  private _callMeasure;
99
102
  private _fetchMeasureImage;
100
103
  private _loadShotGuide;
104
+ private _shotItemsMap;
105
+ private _itemsAwaitingShot;
101
106
  private _shotTag;
102
107
  private _shotTechnique;
103
108
  private _shotAppLink;
@@ -37,6 +37,21 @@ const SET_FILES = ['video.mov', 'poses.jsonl', 'meta.json'];
37
37
  function appCaptureLink(tech) {
38
38
  return `gamri://${tech}?server=${encodeURIComponent(MEASURE_BASE_URL)}`;
39
39
  }
40
+ /* KCS 기준은 매칭 결과 JSON(kcs_matches[0])으로 저장돼 있다 → 엔진에 보낼 조문 텍스트로 편다.
41
+ 문서/조항 라벨을 앞에 붙여야 모델이 어느 기준을 인용하는지 reason 에 쓸 수 있다. */
42
+ function _kcsCriteriaText(raw) {
43
+ if (!raw)
44
+ return '';
45
+ try {
46
+ const kcs = JSON.parse(raw);
47
+ const label = [kcs.file_label, kcs.clause_no, kcs.clause_title].filter(Boolean).join(' ');
48
+ const body = (kcs.matched_item || kcs.text || '').trim();
49
+ return body ? `${label}\n${body}`.trim() : label;
50
+ }
51
+ catch (_a) {
52
+ return raw.trim(); // 구형 데이터(문자열 그대로 저장된 경우)
53
+ }
54
+ }
40
55
  /* set_id(YYYYmmdd-HHMMSS_xxxxxx) → "9/11 14:30" 표기 */
41
56
  function _setTimeLabel(setId) {
42
57
  const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/.exec(setId || '');
@@ -82,6 +97,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
82
97
  this.setPickerFor = null; // 세트 고르는 중인 촬영 번호 (-1 = 재촬영)
83
98
  this.recentSets = []; // 측정 서버에 올라온 최근 세트 (앱이 촬영 직후 자동 전송)
84
99
  this.recentLoading = false;
100
+ this.autoAttachSince = 0; // 이 시각(초) 이후 업로드된 세트만 자동 등록
85
101
  this.expandedReportId = null; // 종합 리포트에서 펼친 항목(아코디언)
86
102
  /* 측정 패널 상태 */
87
103
  this.measureSetIndex = 0;
@@ -157,7 +173,11 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
157
173
  .map(item => ({
158
174
  item_id: item.id,
159
175
  category: [item.mainType, item.detailType].filter(Boolean).join(' · '),
160
- text: item.name || ''
176
+ text: item.name || '',
177
+ // 검사 기준은 조문 '번호'가 아니라 본문을 보내야 판정 근거가 실제 기준에 붙는다.
178
+ // (조문 번호만 주면 모델이 그 내용을 모르는 상태로 판단한다 — 엔진 안내)
179
+ criteria_kcs: _kcsCriteriaText(item.kcsCriteria),
180
+ criteria_site: (item.siteSpecCriteria || '').trim()
161
181
  }));
162
182
  }
163
183
  get uploadedSets() {
@@ -184,14 +204,10 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
184
204
  ${this.result ? this._renderResult() : this._renderGuide()}
185
205
  </div>
186
206
 
187
- <!-- 세트 파일 선택: video.mov/poses.jsonl/meta.json 3개 멀티선택 또는 zip 1개 -->
188
- <input
189
- id="setInput"
190
- type="file"
191
- accept=".mov,.jsonl,.json,.zip,video/quicktime"
192
- multiple
193
- @change=${this._onSetFilesSelected}
194
- />
207
+ <!-- 세트 파일 선택: video.mov/poses.jsonl/meta.json 3개 멀티선택 또는 zip 1개.
208
+ accept을 두지 않는다 — iOS는 확장자를 UTI로 바꿔 거르는데 .jsonl은 매핑이 없어
209
+ 파일 앱에서 poses.jsonl이 비활성으로 보인다. 파일명 검증은 아래 핸들러가 한다. -->
210
+ <input id="setInput" type="file" multiple @change=${this._onSetFilesSelected} />
195
211
 
196
212
  ${this.loading
197
213
  ? html `
@@ -750,6 +766,8 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
750
766
  mainType
751
767
  detailType
752
768
  name
769
+ kcsCriteria
770
+ siteSpecCriteria
753
771
  }
754
772
  }
755
773
  buildingLevel {
@@ -771,6 +789,12 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
771
789
  if (response.errors)
772
790
  return;
773
791
  this.buildingInspection = response.data.buildingInspection;
792
+ // 삭제(soft delete)됐거나 다른 projectType의 검측이면 서버가 null을 준다.
793
+ // 그대로 진행하면 뒤 쿼리에 undefined가 실려 GraphQL 에러가 난다.
794
+ if (!this.buildingInspection) {
795
+ notify({ message: '삭제되었거나 접근할 수 없는 검측입니다.', level: 'warn' });
796
+ return;
797
+ }
774
798
  await this._getProjectByBuildingComplexId((_d = (_c = (_b = (_a = this.buildingInspection) === null || _a === void 0 ? void 0 : _a.buildingLevel) === null || _b === void 0 ? void 0 : _b.building) === null || _c === void 0 ? void 0 : _c.buildingComplex) === null || _d === void 0 ? void 0 : _d.id);
775
799
  }
776
800
  async _getProjectByBuildingComplexId(buildingComplexId) {
@@ -830,7 +854,7 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
830
854
  }
831
855
  /* [1단계] 촬영 가이드 생성 (plan-shots) */
832
856
  async _loadShotGuide() {
833
- var _a;
857
+ var _a, _b;
834
858
  const checklist = this.engineChecklist;
835
859
  if (!checklist.length) {
836
860
  notify({ message: '체크리스트 항목이 없습니다.', level: 'warn' });
@@ -841,6 +865,17 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
841
865
  this.loadingText = 'AI 촬영 가이드 생성 중...';
842
866
  this.guide = await this._callEngine({ action: 'plan-shots', checklist });
843
867
  this.sets = new Array((((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || []).length).fill(null);
868
+ // 어느 촬영에도 배정되지 않은 항목이 있으면 그 항목은 판정 근거가 없다 → 재생성 권고
869
+ const unassigned = ((_b = this.guide) === null || _b === void 0 ? void 0 : _b.unassigned_items) || [];
870
+ if (unassigned.length) {
871
+ notify({
872
+ message: `촬영에 배정되지 않은 검사항목이 ${unassigned.length}건 있습니다. 촬영 가이드를 다시 생성해보세요.`,
873
+ level: 'warn'
874
+ });
875
+ }
876
+ // 자동 등록 기준 시각 — 이 시점 이후 올라온 세트만 자동으로 붙인다.
877
+ // (예전 테스트 세트가 태그만 같다고 채워지면 "찍지도 않았는데 완료"로 보인다)
878
+ this.autoAttachSince = Date.now() / 1000;
844
879
  }
845
880
  catch (error) {
846
881
  console.error('촬영 가이드 생성 실패:', error);
@@ -850,6 +885,33 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
850
885
  this.loading = false;
851
886
  }
852
887
  }
888
+ /* plan-shots가 준 촬영별 항목 배정 → {촬영번호: [항목ID]} (엔진 inspect 의 shot_items).
889
+ 배정 정보가 없는 구버전 응답이면 null 을 돌려 종전대로(전 항목이 전 사진) 동작시킨다. */
890
+ _shotItemsMap() {
891
+ var _a, _b, _c;
892
+ const shots = ((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || [];
893
+ const map = {};
894
+ for (let i = 0; i < shots.length; i++) {
895
+ const items = (_b = shots[i]) === null || _b === void 0 ? void 0 : _b.items;
896
+ if (!Array.isArray(items) || !items.length)
897
+ continue;
898
+ map[String(((_c = shots[i]) === null || _c === void 0 ? void 0 : _c.shot_no) || i + 1)] = items;
899
+ }
900
+ return Object.keys(map).length ? map : null;
901
+ }
902
+ /* 세트가 아직 안 올라온 촬영에 배정된 항목 ID 집합 (그 항목은 판정할 사진이 없다) */
903
+ _itemsAwaitingShot() {
904
+ var _a, _b;
905
+ const pending = new Set();
906
+ const shots = ((_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) || [];
907
+ for (let i = 0; i < shots.length; i++) {
908
+ if (this.sets[i])
909
+ continue;
910
+ for (const id of ((_b = shots[i]) === null || _b === void 0 ? void 0 : _b.items) || [])
911
+ pending.add(id);
912
+ }
913
+ return pending;
914
+ }
853
915
  /* 촬영 칸마다 고유 태그 — 앱에 부재 ID로 넘겨 두면, 올라온 세트의 member_id로
854
916
  "어느 촬영 칸의 것인지" 되찾을 수 있다 (앱↔플랫폼 왕복에 식별자가 없어서 쓰는 방법) */
855
917
  _shotTag(shotNo) {
@@ -873,22 +935,23 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
873
935
  return;
874
936
  await this._loadRecentSets();
875
937
  const used = new Set(this.uploadedSets.map(s => s.setId));
876
- let changed = false;
938
+ const attached = [];
877
939
  const next = [...this.sets];
878
940
  this.guide.shots.forEach((shot, i) => {
879
941
  if (next[i])
880
942
  return;
881
943
  const tag = this._shotTag((shot === null || shot === void 0 ? void 0 : shot.shot_no) || i + 1);
882
- const hit = this.recentSets.find(s => s.member_id === tag && !used.has(s.set_id));
944
+ // 이 화면을 연 뒤에 올라온 것만 — 예전 세트는 목록에서 직접 고르게 둔다
945
+ const hit = this.recentSets.find(s => s.member_id === tag && !used.has(s.set_id) && (s.uploaded_at || 0) >= this.autoAttachSince);
883
946
  if (!hit)
884
947
  return;
885
948
  used.add(hit.set_id);
886
949
  next[i] = { setId: hit.set_id, technique: hit.technique, label: `세트 ${hit.set_id}`, warnings: [] };
887
- changed = true;
950
+ attached.push((shot === null || shot === void 0 ? void 0 : shot.shot_no) || i + 1);
888
951
  });
889
- if (changed) {
952
+ if (attached.length) {
890
953
  this.sets = next;
891
- notify({ message: '촬영한 세트를 자동으로 등록했습니다.' });
954
+ notify({ message: `촬영 ${attached.join('·')} 세트를 자동으로 등록했습니다.` });
892
955
  }
893
956
  }
894
957
  /* 세트 선택 목록 — 앱이 올린 최근 세트 + 파일 직접 선택 폴백 */
@@ -1037,11 +1100,13 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1037
1100
  * - 최초 판정: 업로드된 모든 세트의 대표 3프레임 + 전체 체크리스트 (input_kind=video_frames)
1038
1101
  * - 재검사(재촬영 패널): 새 세트의 3프레임만 + 해당 항목만 재판정 후 결과 병합 */
1039
1102
  async _runInspection() {
1103
+ var _a, _b, _c;
1040
1104
  const isRecapture = this.panel === 'recapture' && !!this.selectedItem;
1041
1105
  try {
1042
1106
  this.loading = true;
1043
1107
  this.loadingText = '동영상 프레임 추출 중...';
1044
1108
  let newPhotos = [];
1109
+ const imageShots = []; // 사진마다 어느 촬영(shot_no)의 것인지
1045
1110
  if (isRecapture) {
1046
1111
  if (!this.recaptureSet) {
1047
1112
  notify({ message: '재촬영 세트를 먼저 업로드해주세요.', level: 'warn' });
@@ -1051,21 +1116,50 @@ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement exte
1051
1116
  newPhotos = await this._fetchVlmFrames(this.recaptureSet, this.sets.length);
1052
1117
  }
1053
1118
  else {
1054
- const sets = this.uploadedSets;
1055
- if (!sets.length) {
1119
+ if (!this.uploadedSets.length) {
1056
1120
  notify({ message: '업로드된 촬영 세트가 없습니다.', level: 'warn' });
1057
1121
  return;
1058
1122
  }
1059
- for (let i = 0; i < sets.length; i++) {
1060
- newPhotos = [...newPhotos, ...(await this._fetchVlmFrames(sets[i], i))];
1123
+ // 촬영 칸 순서대로 — 비어 있는 칸은 건너뛰되 촬영번호는 가이드 것을 그대로 쓴다
1124
+ for (let i = 0; i < this.sets.length; i++) {
1125
+ const set = this.sets[i];
1126
+ if (!set)
1127
+ continue;
1128
+ const frames = await this._fetchVlmFrames(set, i);
1129
+ const shotNo = ((_c = (_b = (_a = this.guide) === null || _a === void 0 ? void 0 : _a.shots) === null || _b === void 0 ? void 0 : _b[i]) === null || _c === void 0 ? void 0 : _c.shot_no) || i + 1;
1130
+ for (const _ of frames)
1131
+ imageShots.push(shotNo);
1132
+ newPhotos = [...newPhotos, ...frames];
1061
1133
  }
1062
1134
  }
1063
1135
  const files = newPhotos.map(p => p.file);
1064
- const checklist = isRecapture
1136
+ let checklist = isRecapture
1065
1137
  ? this.engineChecklist.filter(c => c.item_id === this.selectedItem.item_id)
1066
1138
  : this.engineChecklist;
1139
+ // 아직 안 올라온 촬영에 배정된 항목은 이번 판정에서 뺀다.
1140
+ // 엔진은 그런 항목의 사진 범위를 비우는 대신 '전체 사진'을 주므로, 그냥 두면
1141
+ // 타설 후 항목이 배근 사진으로 판정된다 (배근·타설은 며칠 간격으로 찍는다).
1142
+ if (!isRecapture) {
1143
+ const pending = this._itemsAwaitingShot();
1144
+ if (pending.size) {
1145
+ checklist = checklist.filter(c => !pending.has(c.item_id));
1146
+ notify({
1147
+ message: `아직 촬영되지 않은 항목 ${pending.size}건은 이번 판정에서 제외했습니다. 해당 촬영을 올린 뒤 다시 실행하세요.`,
1148
+ level: 'warn'
1149
+ });
1150
+ }
1151
+ }
1152
+ const input = { action: 'inspect', checklist, input_kind: 'video_frames' };
1153
+ // 촬영 배정 되돌리기 — 이걸 안 보내면 모든 항목이 모든 사진을 보게 되어
1154
+ // "타설 후 항목이 배근 사진으로 판정되는" 오판이 난다 (엔진 실증에서 실제 발생).
1155
+ // 배정된 촬영이 안 올라온 항목은 엔진이 hold 로 둔다 — 남의 사진으로 판정하지 않게.
1156
+ const shotItems = this._shotItemsMap();
1157
+ if (!isRecapture && shotItems && imageShots.length === files.length) {
1158
+ input.image_shots = imageShots.join(',');
1159
+ input.shot_items = JSON.stringify(shotItems);
1160
+ }
1067
1161
  this.loadingText = 'AI 자동 판정 중... (최대 1~2분 소요)';
1068
- const result = await this._callEngine({ action: 'inspect', checklist, input_kind: 'video_frames' }, files);
1162
+ const result = await this._callEngine(input, files);
1069
1163
  if (isRecapture) {
1070
1164
  this.photos = [...this.photos, ...newPhotos];
1071
1165
  this._mergeItems((result === null || result === void 0 ? void 0 : result.items_sorted) || (result === null || result === void 0 ? void 0 : result.items) || [], this.recaptureBase);