@dssp/dkpi 1.0.0-alpha.80 → 1.0.0-alpha.81

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.
Files changed (63) hide show
  1. package/dist-client/bootstrap.d.ts +1 -0
  2. package/dist-client/bootstrap.js +11 -0
  3. package/dist-client/bootstrap.js.map +1 -1
  4. package/dist-client/components/kpi-single-boxplot-chart.d.ts +3 -2
  5. package/dist-client/components/kpi-single-boxplot-chart.js +30 -23
  6. package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
  7. package/dist-client/pages/component/project-update-header.d.ts +1 -0
  8. package/dist-client/pages/component/project-update-header.js +127 -0
  9. package/dist-client/pages/component/project-update-header.js.map +1 -0
  10. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -1
  11. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +1 -1
  12. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  13. package/dist-client/pages/kpi-value/kpi-value-list-page.js +1 -1
  14. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  15. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +16 -2
  16. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +138 -128
  17. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
  18. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +4 -2
  19. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +109 -44
  20. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
  21. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +3 -0
  22. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +32 -4
  23. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
  24. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +24 -0
  25. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +365 -157
  26. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -1
  27. package/dist-client/pages/sv-project-complete.d.ts +4 -1
  28. package/dist-client/pages/sv-project-complete.js +43 -12
  29. package/dist-client/pages/sv-project-complete.js.map +1 -1
  30. package/dist-client/pages/sv-project-detail.d.ts +11 -0
  31. package/dist-client/pages/sv-project-detail.js +184 -48
  32. package/dist-client/pages/sv-project-detail.js.map +1 -1
  33. package/dist-client/pages/sv-project-list.d.ts +9 -0
  34. package/dist-client/pages/sv-project-list.js +93 -3
  35. package/dist-client/pages/sv-project-list.js.map +1 -1
  36. package/dist-client/pages/sv-project-update.d.ts +86 -0
  37. package/dist-client/pages/sv-project-update.js +1331 -0
  38. package/dist-client/pages/sv-project-update.js.map +1 -0
  39. package/dist-client/route.d.ts +1 -1
  40. package/dist-client/route.js +3 -0
  41. package/dist-client/route.js.map +1 -1
  42. package/dist-client/shared/complete-api.d.ts +10 -9
  43. package/dist-client/shared/complete-api.js +44 -18
  44. package/dist-client/shared/complete-api.js.map +1 -1
  45. package/dist-client/tsconfig.tsbuildinfo +1 -1
  46. package/dist-client/viewparts/menu-tools.js +9 -18
  47. package/dist-client/viewparts/menu-tools.js.map +1 -1
  48. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +23 -0
  49. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +72 -28
  50. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  51. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +9 -2
  52. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
  53. package/dist-server/service/kpi-stat/kpi-stat-query.js +19 -18
  54. package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -1
  55. package/dist-server/service/kpi-value/kpi-value-query.js +2 -2
  56. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  57. package/dist-server/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +3 -3
  59. package/schema.graphql +13 -1
  60. package/things-factory.config.js +1 -0
  61. package/dist-client/shared/domain-context.d.ts +0 -7
  62. package/dist-client/shared/domain-context.js +0 -13
  63. package/dist-client/shared/domain-context.js.map +0 -1
@@ -7,6 +7,7 @@ import { getKpiMetricValues, getKpiMetrics, updateProjectCompleteStep1, collectP
7
7
  import { INTEGRATION_SOURCES } from '../../shared/integration-fetch';
8
8
  import moment from 'moment-timezone';
9
9
  import { notify } from '@operato/layout';
10
+ import { hasPrivilege } from '@things-factory/auth-base/dist-client';
10
11
  const KPI_METRIC_KEY_MAPPING = [
11
12
  { label: '공사기간', projectKey: 'constructionPeriod' },
12
13
  { label: '총 근로자수', projectKey: 'totalWorkerCount' },
@@ -24,12 +25,6 @@ const KPI_METRIC_KEY_MAPPING = [
24
25
  ];
25
26
  // 계획값이 없는 것들 (실제값으로 표시할 필드)
26
27
  const NO_PLAN_FIELDS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount', 'upperFloorCount', 'lowerFloorCount'];
27
- const EXCLUDE_FIELDS = [
28
- { id: 'b7a583c0-9de9-4c12-af49-dde65198dfce', name: '계획공사비' },
29
- { id: '9767747a-d2ec-4e36-96c4-4a78bba35b98', name: '실제공사비' },
30
- { id: '036d6e1c-193e-46bd-8d6e-93f248af8124', name: '계획공사기간' },
31
- { id: '5df77618-db44-4da3-a22d-43c067cb5e86', name: '실제공사기간' }
32
- ];
33
28
  let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCompleteTab1Plan extends LitElement {
34
29
  constructor() {
35
30
  super(...arguments);
@@ -50,8 +45,14 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
50
45
  /** 방금 채워진 셀 강조용 (metricId 또는 plan:projectKey) */
51
46
  this.justFilled = {};
52
47
  this.collecting = false;
48
+ /** kpi:input — Step1 계획값 입력/저장 권한 */
49
+ this.canSave = false;
50
+ /** kpi:auto-collect — 외부 시스템 자동 수집 권한 */
51
+ this.canAutoCollect = false;
53
52
  }
54
53
  render() {
54
+ // 전월 (last month) YYYY-MM. 월별 metric 의 "전월 데이터 입력" 검사 기준.
55
+ const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
55
56
  return html `
56
57
  <div class="title">
57
58
  <div>
@@ -73,12 +74,18 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
73
74
 
74
75
  ${this.kpiMetrics.map(metric => {
75
76
  var _a, _b, _c, _d, _e;
76
- // 제외 필드는 표시하지 않음
77
- if (EXCLUDE_FIELDS.some(item => item.id === metric.id))
78
- return null;
79
77
  // 상수로 정의된 매핑 정보에서 metric.name으로 projectKey를 찾음
80
78
  const projectKey = ((_a = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)) === null || _a === void 0 ? void 0 : _a.projectKey) || '';
81
- const kpiMetricValue = this.kpiMetricValues.find((item) => item.metricId === metric.id) || {};
79
+ const isMonthly = metric.periodType === 'MONTH';
80
+ // 현재값 lookup:
81
+ // MONTH → 전월 row (단일 진실원)
82
+ // ALLTIME → 단일 ALLTIME row
83
+ // 그 외 → metricId+periodType 매칭
84
+ const kpiMetricValue = isMonthly
85
+ ? this.kpiMetricValues.find((item) => item.metricId === metric.id &&
86
+ item.periodType === 'MONTH' &&
87
+ (item.valueDate || '').startsWith(lastMonth)) || {}
88
+ : this.kpiMetricValues.find((item) => item.metricId === metric.id && item.periodType === metric.periodType) || {};
82
89
  const isDisplayInput = projectKey === 'constructionPeriod' || projectKey === 'constructionCost'; // 유효한 계획 필드
83
90
  // planValue는 project에서 찾고 없으면 buildingComplex의 값에서 찾음
84
91
  const basePlanValue = this.project[projectKey] || ((_c = (_b = this.project) === null || _b === void 0 ? void 0 : _b.buildingComplex) === null || _c === void 0 ? void 0 : _c[projectKey]) || 0;
@@ -106,15 +113,22 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
106
113
  const effDiff = calcDiff(shownPlan, actualValue);
107
114
  const effDiffClass = effDiff === 0 ? '' : effDiff > 0 ? 'plus' : 'minus';
108
115
  const effDiffSign = effDiff === 0 ? '' : effDiff > 0 ? '+' : '-';
116
+ // periodType 무관하게 값 없으면 pending. 메시지는 MONTH 만 "전월 데이터 입력 필요".
117
+ const isPending = kpiMetricValue.value === undefined || kpiMetricValue.value === null;
109
118
  return html `<div class="row">
110
- <div class="label">• ${metric.name}</div>
119
+ <div class="label ${isPending ? 'pending' : ''}"
120
+ title=${isPending
121
+ ? isMonthly
122
+ ? '전월 데이터 입력 필요'
123
+ : '아직 입력되지 않은 항목'
124
+ : ''}>• ${metric.name}</div>
111
125
  <div class="cell ${planFilled ? 'just-filled' : ''}">
112
126
  ${isDisplayInput
113
127
  ? html `<input .value=${shownPlan} disabled /> ${unit}
114
128
  ${planSrc ? html `<span class="src-chip">🔗 ${planSrc}</span>` : ''}`
115
129
  : html `-`}
116
130
  </div>
117
- <div class="cell ${this.justFilled[metric.id] ? 'just-filled' : ''}">
131
+ <div class="cell ${this.justFilled[metric.id] ? 'just-filled' : ''} ${isPending ? 'pending' : ''}">
118
132
  <input numeric .value=${actualValue !== null && actualValue !== void 0 ? actualValue : 0} @input=${(e) => this._onInputChange(e, metric)} />
119
133
  ${unit}
120
134
  ${src ? html `<span class="src-chip">🔗 ${src}</span>` : ''}
@@ -128,7 +142,13 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
128
142
 
129
143
  <div class="button-line">
130
144
  <div class="ghost-btn" @click=${this._reset}>초기화</div>
131
- <div class="ghost-btn secondary" @click=${() => this._save()}>저장</div>
145
+ <div
146
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
147
+ title=${this.canSave ? '' : 'kpi:input 권한 필요'}
148
+ @click=${() => this.canSave && this._save()}
149
+ >
150
+ 저장
151
+ </div>
132
152
  </div>
133
153
  </div>
134
154
  `;
@@ -139,9 +159,10 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
139
159
  <div class="collect-head">
140
160
  <div class="ttl">시스템 연동 자동 수집 <small>외부 시스템에서 기본정보를 가져옵니다</small></div>
141
161
  <div
142
- class="collect-btn"
162
+ class="collect-btn ${this.canAutoCollect ? '' : 'disabled'}"
143
163
  ?disabled=${this.collecting}
144
- @click=${() => (this.collecting ? null : this._autoCollect())}
164
+ title=${this.canAutoCollect ? '' : 'kpi:auto-collect 권한 필요'}
165
+ @click=${() => this.canAutoCollect && !this.collecting ? this._autoCollect() : null}
145
166
  >
146
167
  ${this.collecting ? '수집 중…' : '⟳ 자동 수집'}
147
168
  </div>
@@ -226,9 +247,17 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
226
247
  // KPI_METRIC_KEY_MAPPING 매칭 키 → form 채움. 미매칭 키 (siteType,
227
248
  // structureType 등 프로젝트/BuildingComplex 기본 속성) → 카드 요약만.
228
249
  const summary = [];
250
+ // KPI_METRIC_KEY_MAPPING 에 없는 키들의 한글 라벨. BuildingComplex/Project 직접 속성용.
229
251
  const NON_KPI_LABEL = {
230
252
  siteType: '건축현장유형',
231
- structureType: '구조형태'
253
+ structureType: '구조형태',
254
+ coverageRatio: '건폐율',
255
+ householdCount: '세대수',
256
+ buildingCount: '동수',
257
+ startDate: '착공일',
258
+ endDate: '준공일',
259
+ permitDate: '건축허가일',
260
+ bldNm: '건물명'
232
261
  };
233
262
  for (const [projectKey, rawValue] of Object.entries(r.data || {})) {
234
263
  if (rawValue === null || rawValue === undefined || rawValue === '')
@@ -305,26 +334,20 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
305
334
  this.collecting = false;
306
335
  }
307
336
  }
308
- /** 메트릭의 실제값 셀을 set/add (사용자 입력과 동일 경로) */
337
+ /**
338
+ * 메트릭의 실제값 set/add — _onInputChange 와 동일하게 periodType 별 row upsert.
339
+ */
309
340
  _setMetricValue(metric, value) {
310
- const idx = this.kpiMetricValues.findIndex((item) => item.metricId === metric.id);
311
- if (idx !== -1) {
312
- this.kpiMetricValues = this.kpiMetricValues.map((item) => item.metricId === metric.id ? Object.assign(Object.assign({}, item), { value }) : item);
313
- }
314
- else {
315
- this.kpiMetricValues = [
316
- ...this.kpiMetricValues,
317
- {
318
- id: crypto.randomUUID(),
319
- value,
320
- metricId: metric.id,
321
- unit: metric.unit || '',
322
- org: this.project.id,
323
- periodType: metric.periodType,
324
- valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')
325
- }
326
- ];
327
- }
341
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, value);
342
+ }
343
+ async connectedCallback() {
344
+ super.connectedCallback();
345
+ const [canSave, canAutoCollect] = await Promise.all([
346
+ hasPrivilege({ category: 'kpi', privilege: 'input', domainOwnerGranted: true, superUserGranted: true }),
347
+ hasPrivilege({ category: 'kpi', privilege: 'auto-collect', domainOwnerGranted: true, superUserGranted: true })
348
+ ]);
349
+ this.canSave = canSave;
350
+ this.canAutoCollect = canAutoCollect;
328
351
  }
329
352
  willUpdate(changedProperties) {
330
353
  var _a;
@@ -335,9 +358,10 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
335
358
  }
336
359
  }
337
360
  async _getInitData() {
338
- const kpiMetrics = await getKpiMetrics();
361
+ var _a, _b;
362
+ const kpiMetrics = await getKpiMetrics((_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
339
363
  this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || []; // 평가 텍스트가 들어간건 제외
340
- this.kpiMetricValues = await getKpiMetricValues(this.project.id);
364
+ this.kpiMetricValues = await getKpiMetricValues(this.project.id, (_b = this.project) === null || _b === void 0 ? void 0 : _b.code);
341
365
  }
342
366
  // Input 요소의 값이 변경될 때 호출되는 콜백 함수
343
367
  _onInputChange(event, metric) {
@@ -347,105 +371,68 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
347
371
  if (target.hasAttribute('numeric')) {
348
372
  inputVal = Number(inputVal.replace(/[^\d.]/g, ''));
349
373
  }
350
- // 기존 배열에서 해당 id를 가진 항목을 찾음
351
- const existingItemIndex = this.kpiMetricValues.findIndex((item) => item.metricId === metric.id);
352
- if (existingItemIndex !== -1) {
353
- // 기존 항목이 있으면 업데이트
354
- this.kpiMetricValues = this.kpiMetricValues.map((item) => item.metricId === metric.id ? Object.assign(Object.assign({}, item), { value: inputVal }) : item);
355
- }
356
- else {
357
- // 기존 항목이 없으면 새로 추가
358
- this.kpiMetricValues = [
359
- ...this.kpiMetricValues,
360
- {
374
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, inputVal);
375
+ }
376
+ /**
377
+ * metric.periodType 따라 적절한 row 를 upsert.
378
+ * - MONTH : 전월 row (YYYY-MM-01). "오늘 입력 = 지난달 데이터" 운영 원칙.
379
+ * - ALLTIME : 단일 row (valueDate sentinel 무관).
380
+ * - 그 외 (DAY/WEEK/QUARTER/YEAR/RANGE) : metric.periodType 그대로, valueDate=today.
381
+ * 매칭은 (metricId, periodType) 기준 — MONTH 만 추가로 valueDate prefix.
382
+ */
383
+ _upsertValueForMetric(values, metric, value) {
384
+ const today = moment().tz('Asia/Seoul');
385
+ const todayYmd = today.format('YYYY-MM-DD');
386
+ const periodType = metric.periodType;
387
+ let updated = [...values];
388
+ if (periodType === 'MONTH') {
389
+ const lastMonth = today.clone().subtract(1, 'month');
390
+ const lastMonth1 = lastMonth.format('YYYY-MM-01');
391
+ const lastMonthYm = lastMonth.format('YYYY-MM');
392
+ const idx = updated.findIndex((i) => i.metricId === metric.id &&
393
+ i.periodType === 'MONTH' &&
394
+ (i.valueDate || '').startsWith(lastMonthYm));
395
+ if (idx !== -1) {
396
+ updated[idx] = Object.assign(Object.assign({}, updated[idx]), { value });
397
+ }
398
+ else {
399
+ updated.push({
361
400
  id: crypto.randomUUID(),
362
- value: inputVal,
401
+ value,
363
402
  metricId: metric.id,
364
403
  unit: metric.unit || '',
365
- org: this.project.id, // 프로젝트 ID 추가
366
- periodType: metric.periodType,
367
- valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')
368
- }
369
- ];
370
- }
371
- }
372
- _generateExcludeFieldsData() {
373
- var _a, _b;
374
- const excludeData = [];
375
- const constructionCostMetric = this.kpiMetrics.find(metric => { var _a; return ((_a = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)) === null || _a === void 0 ? void 0 : _a.projectKey) === 'constructionCost'; });
376
- // 공사기간 관련 메트릭을 찾음
377
- const constructionPeriodMetric = this.kpiMetrics.find(metric => { var _a; return ((_a = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)) === null || _a === void 0 ? void 0 : _a.projectKey) === 'constructionPeriod'; });
378
- // 실적공사비 값 (사용자가 입력한 constructionCost 값)
379
- const actualConstructionCostValue = constructionCostMetric
380
- ? ((_a = this.kpiMetricValues.find((item) => item.metricId === constructionCostMetric.id)) === null || _a === void 0 ? void 0 : _a.value) || 0
381
- : 0;
382
- // 실제공사기간 값 (사용자가 입력한 constructionPeriod 값)
383
- const actualConstructionPeriodValue = constructionPeriodMetric
384
- ? ((_b = this.kpiMetricValues.find((item) => item.metricId === constructionPeriodMetric.id)) === null || _b === void 0 ? void 0 : _b.value) || 0
385
- : 0;
386
- EXCLUDE_FIELDS.forEach(excludeField => {
387
- var _a, _b, _c, _d, _e;
388
- // 해당 excludeField.id에 매칭되는 메트릭 찾기
389
- const metric = this.kpiMetrics.find(m => m.id === excludeField.id);
390
- // 기존에 저장된 데이터가 있는지 확인
391
- const existingValue = this.kpiMetricValues.find((item) => item.metricId === excludeField.id);
392
- let value = 0;
393
- if (excludeField.name == '계획공사비') {
394
- // 키스콘 수집 계획값 우선, 없으면 buildingComplex
395
- value = (_d = (_a = this.collectedPlan['constructionCost']) !== null && _a !== void 0 ? _a : (_c = (_b = this.project) === null || _b === void 0 ? void 0 : _b.buildingComplex) === null || _c === void 0 ? void 0 : _c.constructionCost) !== null && _d !== void 0 ? _d : 0;
396
- }
397
- else if (excludeField.name == '실제공사비') {
398
- value = actualConstructionCostValue;
404
+ org: this.project.id,
405
+ periodType: 'MONTH',
406
+ valueDate: lastMonth1
407
+ });
399
408
  }
400
- else if (excludeField.name == '계획공사기간') {
401
- // 키스콘 수집 계획값 우선, 없으면 착공~준공 일자 계산
402
- value =
403
- (_e = this.collectedPlan['constructionPeriod']) !== null && _e !== void 0 ? _e : Math.ceil(calcDateDiff(this.project.startDate, this.project.endDate) / 30);
409
+ }
410
+ else {
411
+ // ALLTIME / DAY / WEEK / QUARTER / YEAR / RANGE — metric.periodType 그대로 단일 row upsert.
412
+ const idx = updated.findIndex((i) => i.metricId === metric.id && i.periodType === periodType);
413
+ if (idx !== -1) {
414
+ updated[idx] = Object.assign(Object.assign({}, updated[idx]), { value });
404
415
  }
405
- else if (excludeField.name == '실제공사기간') {
406
- value = actualConstructionPeriodValue;
416
+ else {
417
+ updated.push({
418
+ id: crypto.randomUUID(),
419
+ value,
420
+ metricId: metric.id,
421
+ unit: metric.unit || '',
422
+ org: this.project.id,
423
+ periodType,
424
+ valueDate: todayYmd
425
+ });
407
426
  }
408
- excludeData.push({
409
- id: (existingValue === null || existingValue === void 0 ? void 0 : existingValue.id) || crypto.randomUUID(), // 기존 데이터가 있으면 해당 id 사용
410
- value,
411
- metricId: excludeField.id,
412
- unit: (metric === null || metric === void 0 ? void 0 : metric.unit) || '',
413
- org: this.project.id,
414
- periodType: metric === null || metric === void 0 ? void 0 : metric.periodType,
415
- valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')
416
- });
417
- });
418
- return excludeData;
427
+ }
428
+ return updated;
419
429
  }
420
430
  async _save() {
421
- // EXCLUDE_FIELDS에 대한 값들을 자동으로 생성
422
- const excludeData = this._generateExcludeFieldsData();
423
- // EXCLUDE_FIELDS에 해당하는 metricId를 추출
424
- const excludeMetricIds = EXCLUDE_FIELDS.map(field => field.id);
425
- // kpiMetricValues에서 EXCLUDE_FIELDS에 해당하는 중복 항목들을 제외
426
- const filteredKpiMetricValues = this.kpiMetricValues.filter((item) => !excludeMetricIds.includes(item.metricId));
427
- // 기본 실제값 엔트리 생성 (사용자 입력이 없는 메트릭에 대해 기본값 채움)
428
- const existingMetricIds = new Set(filteredKpiMetricValues.map((item) => item.metricId));
429
- const defaultActualData = this.kpiMetrics
430
- .filter(metric => !EXCLUDE_FIELDS.some(field => field.id === metric.id) && !existingMetricIds.has(metric.id))
431
- .map(metric => {
432
- var _a, _b, _c;
433
- const projectKey = ((_a = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)) === null || _a === void 0 ? void 0 : _a.projectKey) || '';
434
- const basePlanValue = this.project[projectKey] || ((_c = (_b = this.project) === null || _b === void 0 ? void 0 : _b.buildingComplex) === null || _c === void 0 ? void 0 : _c[projectKey]) || 0;
435
- const value = NO_PLAN_FIELDS.includes(projectKey) ? basePlanValue : 0;
436
- return {
437
- id: crypto.randomUUID(),
438
- value,
439
- metricId: metric.id,
440
- unit: metric.unit || '',
441
- org: this.project.id,
442
- periodType: metric.periodType,
443
- valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')
444
- };
445
- });
446
- // 필터링된 데이터와 exclude 데이터, 기본 실제값 데이터를 합침
447
- const allKpiMetricValues = [...excludeData, ...filteredKpiMetricValues, ...defaultActualData];
448
- const response = await updateProjectCompleteStep1(allKpiMetricValues);
431
+ var _a;
432
+ // 사용자가 실제 편집한 row 만 저장. 자동 산정 metric (계획/실제 공사비·공기)
433
+ // 별도의 데이터 출처(project 정보, 키스콘 수집, 다른 metric 의 전월 row 등)에서
434
+ // 도출돼야 하므로 화면에서 하드코딩 derive 하지 않음.
435
+ const response = await updateProjectCompleteStep1(this.kpiMetricValues, (_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
449
436
  if (!response.errors) {
450
437
  notify({ message: '저장되었습니다.' });
451
438
  }
@@ -550,6 +537,11 @@ SvProjectCompleteTab1Plan.styles = [
550
537
  .ghost-btn.secondary {
551
538
  background: #24be7b;
552
539
  }
540
+ .ghost-btn.disabled,
541
+ .collect-btn.disabled {
542
+ opacity: 0.45;
543
+ cursor: not-allowed;
544
+ }
553
545
 
554
546
  /* ── 자동 수집 패널 ── */
555
547
  .collect-panel {
@@ -693,6 +685,16 @@ SvProjectCompleteTab1Plan.styles = [
693
685
  .cell.just-filled input {
694
686
  animation: flash 1.1s ease;
695
687
  }
688
+ /* 미입력 — kpiMetricValue 가 비어있는 metric 행 */
689
+ .label.pending::before {
690
+ content: '⚠';
691
+ color: #e74c3c;
692
+ margin-right: 4px;
693
+ }
694
+ .cell.pending input {
695
+ border-color: #e74c3c;
696
+ background-color: #fff5f5;
697
+ }
696
698
  @keyframes flash {
697
699
  0% {
698
700
  background: #fff7cc;
@@ -749,6 +751,14 @@ __decorate([
749
751
  state(),
750
752
  __metadata("design:type", Object)
751
753
  ], SvProjectCompleteTab1Plan.prototype, "collecting", void 0);
754
+ __decorate([
755
+ state(),
756
+ __metadata("design:type", Object)
757
+ ], SvProjectCompleteTab1Plan.prototype, "canSave", void 0);
758
+ __decorate([
759
+ state(),
760
+ __metadata("design:type", Object)
761
+ ], SvProjectCompleteTab1Plan.prototype, "canAutoCollect", void 0);
752
762
  SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = __decorate([
753
763
  customElement('sv-pc-tab1-plan')
754
764
  ], SvProjectCompleteTab1Plan);
@@ -1 +1 @@
1
- {"version":3,"file":"pc-tab1-plan.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab1-plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { calcDiff, calcDateDiff } from '../../shared/func'\nimport {\n getKpiMetricValues,\n getKpiMetrics,\n updateProjectCompleteStep1,\n collectProjectExternalData\n} from '../../shared/complete-api'\nimport { INTEGRATION_SOURCES } from '../../shared/integration-fetch'\nimport moment from 'moment-timezone'\nimport { notify } from '@operato/layout'\n\nconst KPI_METRIC_KEY_MAPPING = [\n { label: '공사기간', projectKey: 'constructionPeriod' },\n { label: '총 근로자수', projectKey: 'totalWorkerCount' },\n { label: '연간 근로자 수', projectKey: 'annualWorkerCount' },\n { label: '투입인력', projectKey: 'workerCount' },\n { label: '재해 건수', projectKey: 'accidentCount' },\n { label: '일정 이탈 수준', projectKey: 'scheduleDeviationLevel' },\n { label: '총 건설 폐기물 발생량', projectKey: 'totalConstructionWasteAmount' },\n { label: '용적율', projectKey: 'floorAreaRatio' },\n { label: '총 설계 변경 건', projectKey: 'designChangeCount' },\n { label: '공사비', projectKey: 'constructionCost' },\n { label: '연면적', projectKey: 'area' },\n { label: '지상층수', projectKey: 'upperFloorCount' },\n { label: '지하층수', projectKey: 'lowerFloorCount' }\n]\n// 계획값이 없는 것들 (실제값으로 표시할 필드)\nconst NO_PLAN_FIELDS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount', 'upperFloorCount', 'lowerFloorCount']\n\nconst EXCLUDE_FIELDS = [\n { id: 'b7a583c0-9de9-4c12-af49-dde65198dfce', name: '계획공사비' },\n { id: '9767747a-d2ec-4e36-96c4-4a78bba35b98', name: '실제공사비' },\n { id: '036d6e1c-193e-46bd-8d6e-93f248af8124', name: '계획공사기간' },\n { id: '5df77618-db44-4da3-a22d-43c067cb5e86', name: '실제공사기간' }\n]\n\n@customElement('sv-pc-tab1-plan')\nexport class SvProjectCompleteTab1Plan extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n .title {\n color: #212529;\n font-size: 13px;\n font-weight: 400;\n line-height: 24px;\n text-align: center;\n }\n\n .rows {\n display: flex;\n flex-direction: column;\n padding: 8px 6px;\n }\n .row.header {\n min-height: 35px;\n background: #f3f3fa;\n border-top: 2px #0c4da2 solid;\n grid-template-columns: 220px 1fr 1fr 200px;\n padding: 0px 25px;\n\n .header-label {\n color: #212529;\n text-align: center;\n }\n }\n .row {\n display: grid;\n grid-template-columns: 220px 1fr 1fr 200px;\n gap: 6px 10px;\n align-items: center;\n padding: 8px 25px;\n border-bottom: 1px rgba(0, 0, 0, 0.1) solid;\n\n .cell {\n display: flex;\n align-items: center;\n justify-content: center;\n }\n }\n .label {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n white-space: nowrap;\n display: flex;\n justify-content: center;\n }\n input {\n padding: 6px 8px;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 5px;\n background: #ffffff;\n color: #212529;\n font-size: 16px;\n margin-right: 5px;\n\n &:disabled {\n background: #f6f6f6;\n }\n }\n .unit {\n text-align: center;\n color: #212529;\n }\n .plus {\n color: #e13232;\n font-weight: 700;\n }\n .minus {\n color: #1e88e5;\n font-weight: 700;\n }\n .button-line {\n display: flex;\n justify-content: center;\n gap: 10px;\n margin-top: 16px;\n }\n .ghost-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n cursor: pointer;\n }\n .ghost-btn.secondary {\n background: #24be7b;\n }\n\n /* ── 자동 수집 패널 ── */\n .collect-panel {\n margin: 4px 6px 14px;\n border: 1px solid #d7e3f2;\n border-radius: 8px;\n background: linear-gradient(180deg, #f7fafe 0%, #ffffff 100%);\n overflow: hidden;\n }\n .collect-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background: #eaf2fb;\n border-bottom: 1px solid #d7e3f2;\n }\n .collect-head .ttl {\n color: #0c4da2;\n font-weight: 700;\n font-size: 14px;\n letter-spacing: -0.03em;\n }\n .collect-head .ttl small {\n color: #5a7da6;\n font-weight: 400;\n margin-left: 8px;\n }\n .collect-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 7px 14px;\n background: #0c4da2;\n color: #fff;\n border-radius: 6px;\n cursor: pointer;\n font-size: 13px;\n font-weight: 600;\n }\n .collect-btn[disabled] {\n background: #9bb4d2;\n cursor: default;\n }\n .source-list {\n display: flex;\n gap: 10px;\n padding: 12px 16px;\n flex-wrap: wrap;\n }\n .source-card {\n flex: 1 1 200px;\n border: 1px solid #e3e9f0;\n border-radius: 7px;\n padding: 10px 12px;\n background: #fff;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n .source-card.collecting {\n border-color: #2e79be;\n box-shadow: 0 0 0 3px rgba(46, 121, 190, 0.12);\n }\n .source-card.done {\n border-color: #24be7b;\n }\n .source-card .sc-head {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n color: #212529;\n font-size: 13px;\n }\n .source-card .sc-head .sys-icon {\n font-size: 16px;\n }\n .source-card .sc-status {\n margin-left: auto;\n font-size: 12px;\n }\n .sc-status.idle {\n color: #aab4c0;\n }\n .sc-status.collecting {\n color: #2e79be;\n }\n .sc-status.done {\n color: #24be7b;\n font-weight: 700;\n }\n .sc-status.error {\n color: #8a8a8a;\n font-weight: 600;\n }\n .source-card.error {\n background: #fafafa;\n border-color: #e0e0e0;\n }\n .source-card .sc-error-msg {\n margin-top: 6px;\n padding: 6px 8px;\n border-radius: 4px;\n background: #f3f4f6;\n color: #6b7280;\n font-size: 12px;\n line-height: 1.4;\n word-break: break-word;\n }\n .source-card .sc-fields {\n margin-top: 8px;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n .sc-field {\n font-size: 12px;\n color: #5a6b7b;\n display: flex;\n justify-content: space-between;\n }\n .sc-field b {\n color: #0c4da2;\n }\n .sc-desc {\n font-size: 11px;\n color: #9aa7b4;\n margin-top: 2px;\n }\n /* 가져온 값 출처 칩 */\n .src-chip {\n display: inline-flex;\n align-items: center;\n margin-left: 6px;\n padding: 1px 7px;\n font-size: 11px;\n border-radius: 10px;\n background: #e7f3ec;\n color: #1d9c63;\n white-space: nowrap;\n }\n .cell.just-filled input {\n animation: flash 1.1s ease;\n }\n @keyframes flash {\n 0% {\n background: #fff7cc;\n }\n 100% {\n background: #ffffff;\n }\n }\n `\n ]\n\n @state() kpiMetricValues: any = []\n @state() kpiMetrics: any = []\n @property({ type: Object }) project: any = {}\n\n /** 연동 진행 상태: source → 'idle' | 'collecting' | 'done' */\n @state() collectStatus: Record<string, 'idle' | 'collecting' | 'done' | 'error'> = {}\n @state() collectError: Record<string, string> = {}\n /** 자동 수집한 실제값의 출처: metricId → 시스템 라벨 */\n @state() valueSources: Record<string, string> = {}\n /** 자동 수집한 계획값: projectKey → 값 (키스콘이 제공하는 계획공사기간/계획공사비) */\n @state() collectedPlan: Record<string, number> = {}\n /** 계획값 출처: projectKey → 시스템 라벨 */\n @state() planSources: Record<string, string> = {}\n /** 시스템별 수집 요약 (카드 표시용): 라벨 → [{label, text}] */\n @state() collectedSummary: Record<string, { label: string; text: string }[]> = {}\n /** 방금 채워진 셀 강조용 (metricId 또는 plan:projectKey) */\n @state() justFilled: Record<string, boolean> = {}\n @state() collecting = false\n\n /** 계획 칼럼을 갖는 항목 (키스콘이 계획값을 제공) */\n private static readonly PLAN_KEYS = ['constructionPeriod', 'constructionCost']\n\n render() {\n return html`\n <div class=\"title\">\n <div>\n 당초 계획 대비 실제 진행 과정에서 변동된 공사비, 공기(공사기간), 면적, 기타 주요 항목을 현실에 맞게 수정·입력합니다.\n <br />이 정보는 성과 분석, KPI 평가, 통계 산출 등에 기준값으로 사용되므로, 가능한 한 실제 값 기준으로 정확히\n 입력해주시기 바랍니다.\n </div>\n </div>\n\n ${this._renderCollectPanel()}\n\n <div class=\"rows\">\n <div class=\"row header\">\n <div class=\"header-label\">기본정보</div>\n <div class=\"header-label\">계획</div>\n <div class=\"header-label\">실제</div>\n <div class=\"header-label\">편차</div>\n </div>\n\n ${this.kpiMetrics.map(metric => {\n // 제외 필드는 표시하지 않음\n if (EXCLUDE_FIELDS.some(item => item.id === metric.id)) return null\n\n // 상수로 정의된 매핑 정보에서 metric.name으로 projectKey를 찾음\n const projectKey = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)?.projectKey || ''\n const kpiMetricValue = this.kpiMetricValues.find((item: any) => item.metricId === metric.id) || {}\n const isDisplayInput = projectKey === 'constructionPeriod' || projectKey === 'constructionCost' // 유효한 계획 필드\n\n // planValue는 project에서 찾고 없으면 buildingComplex의 값에서 찾음\n const basePlanValue = this.project[projectKey] || this.project?.buildingComplex?.[projectKey] || 0\n let planValue = basePlanValue\n const unit = kpiMetricValue.unit || metric.unit || ''\n\n // 2. 계획 값 처리\n // 공사기간은 기간을 일 단위로 계산\n if (projectKey === 'constructionPeriod') {\n planValue = Math.ceil(calcDateDiff(this.project.startDate, this.project.endDate) / 30)\n } else if (!isDisplayInput) {\n planValue = 0\n }\n\n // 1. 실제 값 처리 kpi값을 찾고, 없으면 (NO_PLAN_FIELDS에 해당하는 필드는 원래 계획값 사용)\n const actualValue = kpiMetricValue.value ?? (NO_PLAN_FIELDS.includes(projectKey) ? basePlanValue : 0)\n\n const diffValue = calcDiff(planValue, actualValue)\n const diffClass = diffValue === 0 ? '' : diffValue > 0 ? 'plus' : 'minus'\n const diffSign = diffValue === 0 ? '' : diffValue > 0 ? '+' : '-'\n\n const src = this.valueSources[metric.id]\n // 계획값: 키스콘 수집값이 있으면 그것을, 없으면 계산된 planValue\n const planSrc = this.planSources[projectKey]\n const shownPlan = this.collectedPlan[projectKey] ?? planValue\n const planFilled = this.justFilled[`plan:${projectKey}`]\n // 계획값이 키스콘에서 들어왔으면 편차도 그 기준으로 재계산\n const effDiff = calcDiff(shownPlan, actualValue)\n const effDiffClass = effDiff === 0 ? '' : effDiff > 0 ? 'plus' : 'minus'\n const effDiffSign = effDiff === 0 ? '' : effDiff > 0 ? '+' : '-'\n\n return html`<div class=\"row\">\n <div class=\"label\">• ${metric.name}</div>\n <div class=\"cell ${planFilled ? 'just-filled' : ''}\">\n ${isDisplayInput\n ? html`<input .value=${shownPlan} disabled /> ${unit}\n ${planSrc ? html`<span class=\"src-chip\">🔗 ${planSrc}</span>` : ''}`\n : html`-`}\n </div>\n <div class=\"cell ${this.justFilled[metric.id] ? 'just-filled' : ''}\">\n <input numeric .value=${actualValue ?? 0} @input=${(e: InputEvent) => this._onInputChange(e, metric)} />\n ${unit}\n ${src ? html`<span class=\"src-chip\">🔗 ${src}</span>` : ''}\n </div>\n <div class=\"unit ${isDisplayInput ? effDiffClass : diffClass}\">\n ${isDisplayInput ? effDiffSign : diffSign}\n ${Math.abs(isDisplayInput ? effDiff : diffValue).toLocaleString()} ${unit}\n </div>\n </div>`\n })}\n\n <div class=\"button-line\">\n <div class=\"ghost-btn\" @click=${this._reset}>초기화</div>\n <div class=\"ghost-btn secondary\" @click=${() => this._save()}>저장</div>\n </div>\n </div>\n `\n }\n\n private _renderCollectPanel() {\n return html`\n <div class=\"collect-panel\">\n <div class=\"collect-head\">\n <div class=\"ttl\">시스템 연동 자동 수집 <small>외부 시스템에서 기본정보를 가져옵니다</small></div>\n <div\n class=\"collect-btn\"\n ?disabled=${this.collecting}\n @click=${() => (this.collecting ? null : this._autoCollect())}\n >\n ${this.collecting ? '수집 중…' : '⟳ 자동 수집'}\n </div>\n </div>\n <div class=\"source-list\">\n ${INTEGRATION_SOURCES.map(meta => {\n const status = this.collectStatus[meta.source] || 'idle'\n const errMsg = this.collectError[meta.source]\n // 카드 안의 수집 필드 summary — 실제/계획 무관 collectedSummary 기준으로 통일.\n // (valueSources 만 보면 키스콘처럼 계획값만 채우는 시스템은 표시 안 됨)\n const collectedFields = this._collectedFieldsOf(meta.label)\n const statusText =\n status === 'collecting'\n ? '연동 중…'\n : status === 'done'\n ? '완료 ✓'\n : status === 'error'\n ? '정보 없음 ⓘ'\n : '대기'\n return html`\n <div class=\"source-card ${status}\">\n <div class=\"sc-head\">\n <span class=\"sys-icon\">${meta.icon}</span>\n <span>${meta.label}</span>\n <span class=\"sc-status ${status}\">${statusText}</span>\n </div>\n <div class=\"sc-desc\">${meta.desc}</div>\n ${status === 'error' && errMsg ? html`<div class=\"sc-error-msg\">${errMsg}</div>` : ''}\n ${status === 'done' && collectedFields.length\n ? html`<div class=\"sc-fields\">\n ${collectedFields.map(\n f => html`<div class=\"sc-field\"><span>${f.label}</span><b>${f.text}</b></div>`\n )}\n </div>`\n : ''}\n </div>\n `\n })}\n </div>\n </div>\n `\n }\n\n /** 패널 카드에 표시할, 해당 시스템에서 수집한 값 요약 */\n private _collectedFieldsOf(sourceLabel: string): { label: string; text: string }[] {\n return this.collectedSummary[sourceLabel] || []\n }\n\n /** 자동 수집 — 백엔드의 collectProjectExternalData mutation 1번 호출.\n * 서버가 시나리오 3종 (세움터/올바로/키스콘) 을 순차 실행하고 시스템별 ok/메시지를 반환.\n * 성공/실패와 무관하게 시나리오 step 이 KPI Value 를 DB 에 적재하므로 호출 후\n * KPI Metric Values 를 재조회해 form 을 자동 갱신.\n *\n * 운영 현실: 키스콘·올바로는 시공사 자격증명 미확보가 많아 오류 빈도 높음.\n * 세움터는 공공데이터 서비스키만으로 동작 가능. 시스템별로 카드에 독립 표시.\n */\n private async _autoCollect() {\n if (!this.project?.id) {\n notify({ message: '프로젝트 정보가 없습니다.' })\n return\n }\n this.collecting = true\n this.collectStatus = {}\n this.collectError = {}\n this.collectedSummary = {}\n this.collectedPlan = {}\n this.planSources = {}\n this.valueSources = {}\n\n // 모든 카드를 collecting 상태로 표시\n const collecting: Record<string, 'collecting'> = {}\n for (const meta of INTEGRATION_SOURCES) collecting[meta.source] = 'collecting'\n this.collectStatus = collecting\n\n try {\n const results = await collectProjectExternalData(this.project.id)\n\n // 시스템별 status / error 메시지 업데이트 + result data 를 form 에 매핑\n const status: Record<string, 'done' | 'error'> = {}\n const errors: Record<string, string> = {}\n for (const r of results) {\n status[r.source] = r.ok ? 'done' : 'error'\n if (!r.ok) {\n errors[r.source] = r.message\n continue\n }\n // 시나리오 result 객체 = { projectKey: value, ... }.\n // KPI_METRIC_KEY_MAPPING 매칭 키 → form 채움. 미매칭 키 (siteType,\n // structureType 등 프로젝트/BuildingComplex 기본 속성) → 카드 요약만.\n const summary: { label: string; text: string }[] = []\n const NON_KPI_LABEL: Record<string, string> = {\n siteType: '건축현장유형',\n structureType: '구조형태'\n }\n for (const [projectKey, rawValue] of Object.entries(r.data || {})) {\n if (rawValue === null || rawValue === undefined || rawValue === '') continue\n // 비-primitive (객체/배열) 값은 시나리오 측 return 형태 오류 — form 매핑/표시\n // 모두 skip 하고 console 에 진단 로그만 남김. 흔한 원인은 시나리오 마지막 step 이\n // `return { result: { ... } }` 처럼 wrap 한 케이스.\n if (typeof rawValue === 'object') {\n console.warn(`[자동수집] '${r.label}'.${projectKey} 가 object — 시나리오 return 형태 점검`, rawValue)\n continue\n }\n const mapping = KPI_METRIC_KEY_MAPPING.find(x => x.projectKey === projectKey)\n const label = mapping?.label || NON_KPI_LABEL[projectKey] || projectKey\n const isPlan = SvProjectCompleteTab1Plan.PLAN_KEYS.includes(projectKey)\n\n // summary 카드 표시용 — string 값은 그대로, number 는 toLocaleString\n const numericValue = typeof rawValue === 'number' ? rawValue : Number(rawValue)\n const isNumeric = Number.isFinite(numericValue) && typeof rawValue !== 'string'\n const valueText = isNumeric ? numericValue.toLocaleString() : String(rawValue)\n summary.push({ label, text: valueText })\n\n // KPI 매핑이 없는 키 (siteType/structureType 등) 는 form 채움 skip — 단지 노출만.\n if (!mapping) continue\n\n if (isPlan) {\n // 계획값 (키스콘) — 계획 칼럼에 반영\n this.collectedPlan = { ...this.collectedPlan, [projectKey]: numericValue }\n this.planSources = { ...this.planSources, [projectKey]: r.label }\n this.justFilled = { ...this.justFilled, [`plan:${projectKey}`]: true }\n } else {\n // 실제값 (세움터/올바로/기타) — 매칭되는 metric 의 실제 칼럼에 반영\n const metric = this.kpiMetrics.find(\n (m: any) => KPI_METRIC_KEY_MAPPING.find(x => x.label === m.name)?.projectKey === projectKey\n )\n if (!metric) continue\n this._setMetricValue(metric, numericValue)\n this.valueSources = { ...this.valueSources, [metric.id]: r.label }\n this.justFilled = { ...this.justFilled, [metric.id]: true }\n }\n }\n this.collectedSummary = { ...this.collectedSummary, [r.label]: summary }\n }\n this.collectStatus = status\n this.collectError = errors\n\n // 재조회 안 함 — 시나리오는 fetch + result 반환만 책임 (DB 적재 X). 직전에\n // _setMetricValue 가 memory 에 채운 값들을 DB 의 옛 값으로 덮어쓰면 자동수집\n // 결과가 폼에 반영되지 않음. 사용자가 [저장] 버튼 누르면 그제서야 DB 반영.\n this.requestUpdate()\n setTimeout(() => (this.justFilled = {}), 1300)\n\n const okCount = results.filter(r => r.ok).length\n if (okCount === results.length) {\n notify({ message: `외부 시스템 ${okCount}건 모두 수집 완료. 검토 후 [저장]을 눌러주세요.` })\n } else if (okCount === 0) {\n notify({ message: '외부 시스템에서 가져올 정보가 없습니다.' })\n } else {\n notify({ message: `외부 시스템 수집 — ${okCount}건 완료, 나머지는 정보 없음.` })\n }\n } catch (e) {\n // mutation 자체 실패 (네트워크/권한 등) — 모든 카드를 정보 없음 으로\n const msg = e instanceof Error ? e.message : String(e)\n console.warn('[자동수집] 전체 호출 실패', e)\n const status: Record<string, 'error'> = {}\n const errors: Record<string, string> = {}\n for (const meta of INTEGRATION_SOURCES) {\n status[meta.source] = 'error'\n errors[meta.source] = '업데이트할 정보가 없습니다'\n }\n this.collectStatus = status\n this.collectError = errors\n notify({ message: '외부 시스템 연동이 일시적으로 안 됩니다.' })\n } finally {\n this.collecting = false\n }\n }\n\n /** 메트릭의 실제값 셀을 set/add (사용자 입력과 동일 경로) */\n private _setMetricValue(metric: any, value: number | string) {\n const idx = this.kpiMetricValues.findIndex((item: any) => item.metricId === metric.id)\n if (idx !== -1) {\n this.kpiMetricValues = this.kpiMetricValues.map((item: any) =>\n item.metricId === metric.id ? { ...item, value } : item\n )\n } else {\n this.kpiMetricValues = [\n ...this.kpiMetricValues,\n {\n id: crypto.randomUUID(),\n value,\n metricId: metric.id,\n unit: metric.unit || '',\n org: this.project.id,\n periodType: metric.periodType,\n valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')\n }\n ]\n }\n }\n\n willUpdate(changedProperties: Map<string, any>) {\n super.willUpdate(changedProperties)\n\n // project가 변경되고, project.id가 존재하면 데이터 로드\n if (changedProperties.has('project') && this.project?.id) {\n this._getInitData()\n }\n }\n\n private async _getInitData() {\n const kpiMetrics = await getKpiMetrics()\n this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || [] // 평가 텍스트가 들어간건 제외\n this.kpiMetricValues = await getKpiMetricValues(this.project.id)\n }\n\n // Input 요소의 값이 변경될 때 호출되는 콜백 함수\n private _onInputChange(event: InputEvent, metric: any) {\n const target = event.target as HTMLInputElement\n let inputVal: any = target.value\n\n // 숫자 타입은 다른 문자 입력 제거\n if (target.hasAttribute('numeric')) {\n inputVal = Number(inputVal.replace(/[^\\d.]/g, ''))\n }\n\n // 기존 배열에서 해당 id를 가진 항목을 찾음\n const existingItemIndex = this.kpiMetricValues.findIndex((item: any) => item.metricId === metric.id)\n\n if (existingItemIndex !== -1) {\n // 기존 항목이 있으면 업데이트\n this.kpiMetricValues = this.kpiMetricValues.map((item: any) =>\n item.metricId === metric.id ? { ...item, value: inputVal } : item\n )\n } else {\n // 기존 항목이 없으면 새로 추가\n this.kpiMetricValues = [\n ...this.kpiMetricValues,\n {\n id: crypto.randomUUID(),\n value: inputVal,\n metricId: metric.id,\n unit: metric.unit || '',\n org: this.project.id, // 프로젝트 ID 추가\n periodType: metric.periodType,\n valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')\n }\n ]\n }\n }\n\n private _generateExcludeFieldsData() {\n const excludeData: any[] = []\n\n const constructionCostMetric = this.kpiMetrics.find(\n metric => KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)?.projectKey === 'constructionCost'\n )\n\n // 공사기간 관련 메트릭을 찾음\n const constructionPeriodMetric = this.kpiMetrics.find(\n metric => KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)?.projectKey === 'constructionPeriod'\n )\n\n // 실적공사비 값 (사용자가 입력한 constructionCost 값)\n const actualConstructionCostValue = constructionCostMetric\n ? this.kpiMetricValues.find((item: any) => item.metricId === constructionCostMetric.id)?.value || 0\n : 0\n\n // 실제공사기간 값 (사용자가 입력한 constructionPeriod 값)\n const actualConstructionPeriodValue = constructionPeriodMetric\n ? this.kpiMetricValues.find((item: any) => item.metricId === constructionPeriodMetric.id)?.value || 0\n : 0\n\n EXCLUDE_FIELDS.forEach(excludeField => {\n // 해당 excludeField.id에 매칭되는 메트릭 찾기\n const metric = this.kpiMetrics.find(m => m.id === excludeField.id)\n\n // 기존에 저장된 데이터가 있는지 확인\n const existingValue = this.kpiMetricValues.find((item: any) => item.metricId === excludeField.id)\n\n let value = 0\n if (excludeField.name == '계획공사비') {\n // 키스콘 수집 계획값 우선, 없으면 buildingComplex\n value = this.collectedPlan['constructionCost'] ?? this.project?.buildingComplex?.constructionCost ?? 0\n } else if (excludeField.name == '실제공사비') {\n value = actualConstructionCostValue\n } else if (excludeField.name == '계획공사기간') {\n // 키스콘 수집 계획값 우선, 없으면 착공~준공 일자 계산\n value =\n this.collectedPlan['constructionPeriod'] ??\n Math.ceil(calcDateDiff(this.project.startDate, this.project.endDate) / 30)\n } else if (excludeField.name == '실제공사기간') {\n value = actualConstructionPeriodValue\n }\n\n excludeData.push({\n id: existingValue?.id || crypto.randomUUID(), // 기존 데이터가 있으면 해당 id 사용\n value,\n metricId: excludeField.id,\n unit: metric?.unit || '',\n org: this.project.id,\n periodType: metric?.periodType,\n valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')\n })\n })\n\n return excludeData\n }\n\n private async _save() {\n // EXCLUDE_FIELDS에 대한 값들을 자동으로 생성\n const excludeData = this._generateExcludeFieldsData()\n\n // EXCLUDE_FIELDS에 해당하는 metricId를 추출\n const excludeMetricIds = EXCLUDE_FIELDS.map(field => field.id)\n // kpiMetricValues에서 EXCLUDE_FIELDS에 해당하는 중복 항목들을 제외\n const filteredKpiMetricValues = this.kpiMetricValues.filter((item: any) => !excludeMetricIds.includes(item.metricId))\n\n // 기본 실제값 엔트리 생성 (사용자 입력이 없는 메트릭에 대해 기본값 채움)\n const existingMetricIds = new Set(filteredKpiMetricValues.map((item: any) => item.metricId))\n const defaultActualData = this.kpiMetrics\n .filter(metric => !EXCLUDE_FIELDS.some(field => field.id === metric.id) && !existingMetricIds.has(metric.id))\n .map(metric => {\n const projectKey = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)?.projectKey || ''\n const basePlanValue = this.project[projectKey] || this.project?.buildingComplex?.[projectKey] || 0\n const value = NO_PLAN_FIELDS.includes(projectKey) ? basePlanValue : 0\n\n return {\n id: crypto.randomUUID(),\n value,\n metricId: metric.id,\n unit: metric.unit || '',\n org: this.project.id,\n periodType: metric.periodType,\n valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')\n }\n })\n\n // 필터링된 데이터와 exclude 데이터, 기본 실제값 데이터를 합침\n const allKpiMetricValues = [...excludeData, ...filteredKpiMetricValues, ...defaultActualData]\n const response = await updateProjectCompleteStep1(allKpiMetricValues)\n if (!response.errors) {\n notify({ message: '저장되었습니다.' })\n }\n }\n\n private _reset() {\n this._getInitData()\n }\n}\n"]}
1
+ {"version":3,"file":"pc-tab1-plan.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab1-plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { calcDiff, calcDateDiff } from '../../shared/func'\nimport {\n getKpiMetricValues,\n getKpiMetrics,\n updateProjectCompleteStep1,\n collectProjectExternalData\n} from '../../shared/complete-api'\nimport { INTEGRATION_SOURCES } from '../../shared/integration-fetch'\nimport moment from 'moment-timezone'\nimport { notify } from '@operato/layout'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\nconst KPI_METRIC_KEY_MAPPING = [\n { label: '공사기간', projectKey: 'constructionPeriod' },\n { label: '총 근로자수', projectKey: 'totalWorkerCount' },\n { label: '연간 근로자 수', projectKey: 'annualWorkerCount' },\n { label: '투입인력', projectKey: 'workerCount' },\n { label: '재해 건수', projectKey: 'accidentCount' },\n { label: '일정 이탈 수준', projectKey: 'scheduleDeviationLevel' },\n { label: '총 건설 폐기물 발생량', projectKey: 'totalConstructionWasteAmount' },\n { label: '용적율', projectKey: 'floorAreaRatio' },\n { label: '총 설계 변경 건', projectKey: 'designChangeCount' },\n { label: '공사비', projectKey: 'constructionCost' },\n { label: '연면적', projectKey: 'area' },\n { label: '지상층수', projectKey: 'upperFloorCount' },\n { label: '지하층수', projectKey: 'lowerFloorCount' }\n]\n// 계획값이 없는 것들 (실제값으로 표시할 필드)\nconst NO_PLAN_FIELDS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount', 'upperFloorCount', 'lowerFloorCount']\n\n@customElement('sv-pc-tab1-plan')\nexport class SvProjectCompleteTab1Plan extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n .title {\n color: #212529;\n font-size: 13px;\n font-weight: 400;\n line-height: 24px;\n text-align: center;\n }\n\n .rows {\n display: flex;\n flex-direction: column;\n padding: 8px 6px;\n }\n .row.header {\n min-height: 35px;\n background: #f3f3fa;\n border-top: 2px #0c4da2 solid;\n grid-template-columns: 220px 1fr 1fr 200px;\n padding: 0px 25px;\n\n .header-label {\n color: #212529;\n text-align: center;\n }\n }\n .row {\n display: grid;\n grid-template-columns: 220px 1fr 1fr 200px;\n gap: 6px 10px;\n align-items: center;\n padding: 8px 25px;\n border-bottom: 1px rgba(0, 0, 0, 0.1) solid;\n\n .cell {\n display: flex;\n align-items: center;\n justify-content: center;\n }\n }\n .label {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n white-space: nowrap;\n display: flex;\n justify-content: center;\n }\n input {\n padding: 6px 8px;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 5px;\n background: #ffffff;\n color: #212529;\n font-size: 16px;\n margin-right: 5px;\n\n &:disabled {\n background: #f6f6f6;\n }\n }\n .unit {\n text-align: center;\n color: #212529;\n }\n .plus {\n color: #e13232;\n font-weight: 700;\n }\n .minus {\n color: #1e88e5;\n font-weight: 700;\n }\n .button-line {\n display: flex;\n justify-content: center;\n gap: 10px;\n margin-top: 16px;\n }\n .ghost-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n cursor: pointer;\n }\n .ghost-btn.secondary {\n background: #24be7b;\n }\n .ghost-btn.disabled,\n .collect-btn.disabled {\n opacity: 0.45;\n cursor: not-allowed;\n }\n\n /* ── 자동 수집 패널 ── */\n .collect-panel {\n margin: 4px 6px 14px;\n border: 1px solid #d7e3f2;\n border-radius: 8px;\n background: linear-gradient(180deg, #f7fafe 0%, #ffffff 100%);\n overflow: hidden;\n }\n .collect-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background: #eaf2fb;\n border-bottom: 1px solid #d7e3f2;\n }\n .collect-head .ttl {\n color: #0c4da2;\n font-weight: 700;\n font-size: 14px;\n letter-spacing: -0.03em;\n }\n .collect-head .ttl small {\n color: #5a7da6;\n font-weight: 400;\n margin-left: 8px;\n }\n .collect-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 7px 14px;\n background: #0c4da2;\n color: #fff;\n border-radius: 6px;\n cursor: pointer;\n font-size: 13px;\n font-weight: 600;\n }\n .collect-btn[disabled] {\n background: #9bb4d2;\n cursor: default;\n }\n .source-list {\n display: flex;\n gap: 10px;\n padding: 12px 16px;\n flex-wrap: wrap;\n }\n .source-card {\n flex: 1 1 200px;\n border: 1px solid #e3e9f0;\n border-radius: 7px;\n padding: 10px 12px;\n background: #fff;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n .source-card.collecting {\n border-color: #2e79be;\n box-shadow: 0 0 0 3px rgba(46, 121, 190, 0.12);\n }\n .source-card.done {\n border-color: #24be7b;\n }\n .source-card .sc-head {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n color: #212529;\n font-size: 13px;\n }\n .source-card .sc-head .sys-icon {\n font-size: 16px;\n }\n .source-card .sc-status {\n margin-left: auto;\n font-size: 12px;\n }\n .sc-status.idle {\n color: #aab4c0;\n }\n .sc-status.collecting {\n color: #2e79be;\n }\n .sc-status.done {\n color: #24be7b;\n font-weight: 700;\n }\n .sc-status.error {\n color: #8a8a8a;\n font-weight: 600;\n }\n .source-card.error {\n background: #fafafa;\n border-color: #e0e0e0;\n }\n .source-card .sc-error-msg {\n margin-top: 6px;\n padding: 6px 8px;\n border-radius: 4px;\n background: #f3f4f6;\n color: #6b7280;\n font-size: 12px;\n line-height: 1.4;\n word-break: break-word;\n }\n .source-card .sc-fields {\n margin-top: 8px;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n .sc-field {\n font-size: 12px;\n color: #5a6b7b;\n display: flex;\n justify-content: space-between;\n }\n .sc-field b {\n color: #0c4da2;\n }\n .sc-desc {\n font-size: 11px;\n color: #9aa7b4;\n margin-top: 2px;\n }\n /* 가져온 값 출처 칩 */\n .src-chip {\n display: inline-flex;\n align-items: center;\n margin-left: 6px;\n padding: 1px 7px;\n font-size: 11px;\n border-radius: 10px;\n background: #e7f3ec;\n color: #1d9c63;\n white-space: nowrap;\n }\n .cell.just-filled input {\n animation: flash 1.1s ease;\n }\n /* 미입력 — kpiMetricValue 가 비어있는 metric 행 */\n .label.pending::before {\n content: '⚠';\n color: #e74c3c;\n margin-right: 4px;\n }\n .cell.pending input {\n border-color: #e74c3c;\n background-color: #fff5f5;\n }\n @keyframes flash {\n 0% {\n background: #fff7cc;\n }\n 100% {\n background: #ffffff;\n }\n }\n `\n ]\n\n @state() kpiMetricValues: any = []\n @state() kpiMetrics: any = []\n @property({ type: Object }) project: any = {}\n\n /** 연동 진행 상태: source → 'idle' | 'collecting' | 'done' */\n @state() collectStatus: Record<string, 'idle' | 'collecting' | 'done' | 'error'> = {}\n @state() collectError: Record<string, string> = {}\n /** 자동 수집한 실제값의 출처: metricId → 시스템 라벨 */\n @state() valueSources: Record<string, string> = {}\n /** 자동 수집한 계획값: projectKey → 값 (키스콘이 제공하는 계획공사기간/계획공사비) */\n @state() collectedPlan: Record<string, number> = {}\n /** 계획값 출처: projectKey → 시스템 라벨 */\n @state() planSources: Record<string, string> = {}\n /** 시스템별 수집 요약 (카드 표시용): 라벨 → [{label, text}] */\n @state() collectedSummary: Record<string, { label: string; text: string }[]> = {}\n /** 방금 채워진 셀 강조용 (metricId 또는 plan:projectKey) */\n @state() justFilled: Record<string, boolean> = {}\n @state() collecting = false\n\n /** kpi:input — Step1 계획값 입력/저장 권한 */\n @state() canSave = false\n /** kpi:auto-collect — 외부 시스템 자동 수집 권한 */\n @state() canAutoCollect = false\n\n /** 계획 칼럼을 갖는 항목 (키스콘이 계획값을 제공) */\n private static readonly PLAN_KEYS = ['constructionPeriod', 'constructionCost']\n\n render() {\n // 전월 (last month) YYYY-MM. 월별 metric 의 \"전월 데이터 입력\" 검사 기준.\n const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM')\n\n return html`\n <div class=\"title\">\n <div>\n 당초 계획 대비 실제 진행 과정에서 변동된 공사비, 공기(공사기간), 면적, 기타 주요 항목을 현실에 맞게 수정·입력합니다.\n <br />이 정보는 성과 분석, KPI 평가, 통계 산출 등에 기준값으로 사용되므로, 가능한 한 실제 값 기준으로 정확히\n 입력해주시기 바랍니다.\n </div>\n </div>\n\n ${this._renderCollectPanel()}\n\n <div class=\"rows\">\n <div class=\"row header\">\n <div class=\"header-label\">기본정보</div>\n <div class=\"header-label\">계획</div>\n <div class=\"header-label\">실제</div>\n <div class=\"header-label\">편차</div>\n </div>\n\n ${this.kpiMetrics.map(metric => {\n // 상수로 정의된 매핑 정보에서 metric.name으로 projectKey를 찾음\n const projectKey = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)?.projectKey || ''\n const isMonthly = metric.periodType === 'MONTH'\n // 현재값 lookup:\n // MONTH → 전월 row (단일 진실원)\n // ALLTIME → 단일 ALLTIME row\n // 그 외 → metricId+periodType 매칭\n const kpiMetricValue = isMonthly\n ? this.kpiMetricValues.find(\n (item: any) =>\n item.metricId === metric.id &&\n item.periodType === 'MONTH' &&\n (item.valueDate || '').startsWith(lastMonth)\n ) || {}\n : this.kpiMetricValues.find(\n (item: any) => item.metricId === metric.id && item.periodType === metric.periodType\n ) || {}\n const isDisplayInput = projectKey === 'constructionPeriod' || projectKey === 'constructionCost' // 유효한 계획 필드\n\n // planValue는 project에서 찾고 없으면 buildingComplex의 값에서 찾음\n const basePlanValue = this.project[projectKey] || this.project?.buildingComplex?.[projectKey] || 0\n let planValue = basePlanValue\n const unit = kpiMetricValue.unit || metric.unit || ''\n\n // 2. 계획 값 처리\n // 공사기간은 기간을 일 단위로 계산\n if (projectKey === 'constructionPeriod') {\n planValue = Math.ceil(calcDateDiff(this.project.startDate, this.project.endDate) / 30)\n } else if (!isDisplayInput) {\n planValue = 0\n }\n\n // 1. 실제 값 처리 kpi값을 찾고, 없으면 (NO_PLAN_FIELDS에 해당하는 필드는 원래 계획값 사용)\n const actualValue = kpiMetricValue.value ?? (NO_PLAN_FIELDS.includes(projectKey) ? basePlanValue : 0)\n\n const diffValue = calcDiff(planValue, actualValue)\n const diffClass = diffValue === 0 ? '' : diffValue > 0 ? 'plus' : 'minus'\n const diffSign = diffValue === 0 ? '' : diffValue > 0 ? '+' : '-'\n\n const src = this.valueSources[metric.id]\n // 계획값: 키스콘 수집값이 있으면 그것을, 없으면 계산된 planValue\n const planSrc = this.planSources[projectKey]\n const shownPlan = this.collectedPlan[projectKey] ?? planValue\n const planFilled = this.justFilled[`plan:${projectKey}`]\n // 계획값이 키스콘에서 들어왔으면 편차도 그 기준으로 재계산\n const effDiff = calcDiff(shownPlan, actualValue)\n const effDiffClass = effDiff === 0 ? '' : effDiff > 0 ? 'plus' : 'minus'\n const effDiffSign = effDiff === 0 ? '' : effDiff > 0 ? '+' : '-'\n\n // periodType 무관하게 값 없으면 pending. 메시지는 MONTH 만 \"전월 데이터 입력 필요\".\n const isPending = kpiMetricValue.value === undefined || kpiMetricValue.value === null\n\n return html`<div class=\"row\">\n <div class=\"label ${isPending ? 'pending' : ''}\"\n title=${isPending\n ? isMonthly\n ? '전월 데이터 입력 필요'\n : '아직 입력되지 않은 항목'\n : ''}>• ${metric.name}</div>\n <div class=\"cell ${planFilled ? 'just-filled' : ''}\">\n ${isDisplayInput\n ? html`<input .value=${shownPlan} disabled /> ${unit}\n ${planSrc ? html`<span class=\"src-chip\">🔗 ${planSrc}</span>` : ''}`\n : html`-`}\n </div>\n <div class=\"cell ${this.justFilled[metric.id] ? 'just-filled' : ''} ${isPending ? 'pending' : ''}\">\n <input numeric .value=${actualValue ?? 0} @input=${(e: InputEvent) => this._onInputChange(e, metric)} />\n ${unit}\n ${src ? html`<span class=\"src-chip\">🔗 ${src}</span>` : ''}\n </div>\n <div class=\"unit ${isDisplayInput ? effDiffClass : diffClass}\">\n ${isDisplayInput ? effDiffSign : diffSign}\n ${Math.abs(isDisplayInput ? effDiff : diffValue).toLocaleString()} ${unit}\n </div>\n </div>`\n })}\n\n <div class=\"button-line\">\n <div class=\"ghost-btn\" @click=${this._reset}>초기화</div>\n <div\n class=\"ghost-btn secondary ${this.canSave ? '' : 'disabled'}\"\n title=${this.canSave ? '' : 'kpi:input 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>\n </div>\n `\n }\n\n private _renderCollectPanel() {\n return html`\n <div class=\"collect-panel\">\n <div class=\"collect-head\">\n <div class=\"ttl\">시스템 연동 자동 수집 <small>외부 시스템에서 기본정보를 가져옵니다</small></div>\n <div\n class=\"collect-btn ${this.canAutoCollect ? '' : 'disabled'}\"\n ?disabled=${this.collecting}\n title=${this.canAutoCollect ? '' : 'kpi:auto-collect 권한 필요'}\n @click=${() =>\n this.canAutoCollect && !this.collecting ? this._autoCollect() : null}\n >\n ${this.collecting ? '수집 중…' : '⟳ 자동 수집'}\n </div>\n </div>\n <div class=\"source-list\">\n ${INTEGRATION_SOURCES.map(meta => {\n const status = this.collectStatus[meta.source] || 'idle'\n const errMsg = this.collectError[meta.source]\n // 카드 안의 수집 필드 summary — 실제/계획 무관 collectedSummary 기준으로 통일.\n // (valueSources 만 보면 키스콘처럼 계획값만 채우는 시스템은 표시 안 됨)\n const collectedFields = this._collectedFieldsOf(meta.label)\n const statusText =\n status === 'collecting'\n ? '연동 중…'\n : status === 'done'\n ? '완료 ✓'\n : status === 'error'\n ? '정보 없음 ⓘ'\n : '대기'\n return html`\n <div class=\"source-card ${status}\">\n <div class=\"sc-head\">\n <span class=\"sys-icon\">${meta.icon}</span>\n <span>${meta.label}</span>\n <span class=\"sc-status ${status}\">${statusText}</span>\n </div>\n <div class=\"sc-desc\">${meta.desc}</div>\n ${status === 'error' && errMsg ? html`<div class=\"sc-error-msg\">${errMsg}</div>` : ''}\n ${status === 'done' && collectedFields.length\n ? html`<div class=\"sc-fields\">\n ${collectedFields.map(\n f => html`<div class=\"sc-field\"><span>${f.label}</span><b>${f.text}</b></div>`\n )}\n </div>`\n : ''}\n </div>\n `\n })}\n </div>\n </div>\n `\n }\n\n /** 패널 카드에 표시할, 해당 시스템에서 수집한 값 요약 */\n private _collectedFieldsOf(sourceLabel: string): { label: string; text: string }[] {\n return this.collectedSummary[sourceLabel] || []\n }\n\n /** 자동 수집 — 백엔드의 collectProjectExternalData mutation 1번 호출.\n * 서버가 시나리오 3종 (세움터/올바로/키스콘) 을 순차 실행하고 시스템별 ok/메시지를 반환.\n * 성공/실패와 무관하게 시나리오 step 이 KPI Value 를 DB 에 적재하므로 호출 후\n * KPI Metric Values 를 재조회해 form 을 자동 갱신.\n *\n * 운영 현실: 키스콘·올바로는 시공사 자격증명 미확보가 많아 오류 빈도 높음.\n * 세움터는 공공데이터 서비스키만으로 동작 가능. 시스템별로 카드에 독립 표시.\n */\n private async _autoCollect() {\n if (!this.project?.id) {\n notify({ message: '프로젝트 정보가 없습니다.' })\n return\n }\n this.collecting = true\n this.collectStatus = {}\n this.collectError = {}\n this.collectedSummary = {}\n this.collectedPlan = {}\n this.planSources = {}\n this.valueSources = {}\n\n // 모든 카드를 collecting 상태로 표시\n const collecting: Record<string, 'collecting'> = {}\n for (const meta of INTEGRATION_SOURCES) collecting[meta.source] = 'collecting'\n this.collectStatus = collecting\n\n try {\n const results = await collectProjectExternalData(this.project.id)\n\n // 시스템별 status / error 메시지 업데이트 + result data 를 form 에 매핑\n const status: Record<string, 'done' | 'error'> = {}\n const errors: Record<string, string> = {}\n for (const r of results) {\n status[r.source] = r.ok ? 'done' : 'error'\n if (!r.ok) {\n errors[r.source] = r.message\n continue\n }\n // 시나리오 result 객체 = { projectKey: value, ... }.\n // KPI_METRIC_KEY_MAPPING 매칭 키 → form 채움. 미매칭 키 (siteType,\n // structureType 등 프로젝트/BuildingComplex 기본 속성) → 카드 요약만.\n const summary: { label: string; text: string }[] = []\n // KPI_METRIC_KEY_MAPPING 에 없는 키들의 한글 라벨. BuildingComplex/Project 직접 속성용.\n const NON_KPI_LABEL: Record<string, string> = {\n siteType: '건축현장유형',\n structureType: '구조형태',\n coverageRatio: '건폐율',\n householdCount: '세대수',\n buildingCount: '동수',\n startDate: '착공일',\n endDate: '준공일',\n permitDate: '건축허가일',\n bldNm: '건물명'\n }\n for (const [projectKey, rawValue] of Object.entries(r.data || {})) {\n if (rawValue === null || rawValue === undefined || rawValue === '') continue\n // 비-primitive (객체/배열) 값은 시나리오 측 return 형태 오류 — form 매핑/표시\n // 모두 skip 하고 console 에 진단 로그만 남김. 흔한 원인은 시나리오 마지막 step 이\n // `return { result: { ... } }` 처럼 wrap 한 케이스.\n if (typeof rawValue === 'object') {\n console.warn(`[자동수집] '${r.label}'.${projectKey} 가 object — 시나리오 return 형태 점검`, rawValue)\n continue\n }\n const mapping = KPI_METRIC_KEY_MAPPING.find(x => x.projectKey === projectKey)\n const label = mapping?.label || NON_KPI_LABEL[projectKey] || projectKey\n const isPlan = SvProjectCompleteTab1Plan.PLAN_KEYS.includes(projectKey)\n\n // summary 카드 표시용 — string 값은 그대로, number 는 toLocaleString\n const numericValue = typeof rawValue === 'number' ? rawValue : Number(rawValue)\n const isNumeric = Number.isFinite(numericValue) && typeof rawValue !== 'string'\n const valueText = isNumeric ? numericValue.toLocaleString() : String(rawValue)\n summary.push({ label, text: valueText })\n\n // KPI 매핑이 없는 키 (siteType/structureType 등) 는 form 채움 skip — 단지 노출만.\n if (!mapping) continue\n\n if (isPlan) {\n // 계획값 (키스콘) — 계획 칼럼에 반영\n this.collectedPlan = { ...this.collectedPlan, [projectKey]: numericValue }\n this.planSources = { ...this.planSources, [projectKey]: r.label }\n this.justFilled = { ...this.justFilled, [`plan:${projectKey}`]: true }\n } else {\n // 실제값 (세움터/올바로/기타) — 매칭되는 metric 의 실제 칼럼에 반영\n const metric = this.kpiMetrics.find(\n (m: any) => KPI_METRIC_KEY_MAPPING.find(x => x.label === m.name)?.projectKey === projectKey\n )\n if (!metric) continue\n this._setMetricValue(metric, numericValue)\n this.valueSources = { ...this.valueSources, [metric.id]: r.label }\n this.justFilled = { ...this.justFilled, [metric.id]: true }\n }\n }\n this.collectedSummary = { ...this.collectedSummary, [r.label]: summary }\n }\n this.collectStatus = status\n this.collectError = errors\n\n // 재조회 안 함 — 시나리오는 fetch + result 반환만 책임 (DB 적재 X). 직전에\n // _setMetricValue 가 memory 에 채운 값들을 DB 의 옛 값으로 덮어쓰면 자동수집\n // 결과가 폼에 반영되지 않음. 사용자가 [저장] 버튼 누르면 그제서야 DB 반영.\n this.requestUpdate()\n setTimeout(() => (this.justFilled = {}), 1300)\n\n const okCount = results.filter(r => r.ok).length\n if (okCount === results.length) {\n notify({ message: `외부 시스템 ${okCount}건 모두 수집 완료. 검토 후 [저장]을 눌러주세요.` })\n } else if (okCount === 0) {\n notify({ message: '외부 시스템에서 가져올 정보가 없습니다.' })\n } else {\n notify({ message: `외부 시스템 수집 — ${okCount}건 완료, 나머지는 정보 없음.` })\n }\n } catch (e) {\n // mutation 자체 실패 (네트워크/권한 등) — 모든 카드를 정보 없음 으로\n const msg = e instanceof Error ? e.message : String(e)\n console.warn('[자동수집] 전체 호출 실패', e)\n const status: Record<string, 'error'> = {}\n const errors: Record<string, string> = {}\n for (const meta of INTEGRATION_SOURCES) {\n status[meta.source] = 'error'\n errors[meta.source] = '업데이트할 정보가 없습니다'\n }\n this.collectStatus = status\n this.collectError = errors\n notify({ message: '외부 시스템 연동이 일시적으로 안 됩니다.' })\n } finally {\n this.collecting = false\n }\n }\n\n /**\n * 메트릭의 실제값 set/add — _onInputChange 와 동일하게 periodType 별 row upsert.\n */\n private _setMetricValue(metric: any, value: number | string) {\n this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, value)\n }\n\n async connectedCallback() {\n super.connectedCallback()\n const [canSave, canAutoCollect] = await Promise.all([\n hasPrivilege({ category: 'kpi', privilege: 'input', domainOwnerGranted: true, superUserGranted: true }),\n hasPrivilege({ category: 'kpi', privilege: 'auto-collect', domainOwnerGranted: true, superUserGranted: true })\n ])\n this.canSave = canSave\n this.canAutoCollect = canAutoCollect\n }\n\n willUpdate(changedProperties: Map<string, any>) {\n super.willUpdate(changedProperties)\n\n // project가 변경되고, project.id가 존재하면 데이터 로드\n if (changedProperties.has('project') && this.project?.id) {\n this._getInitData()\n }\n }\n\n private async _getInitData() {\n const kpiMetrics = await getKpiMetrics(this.project?.code)\n this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || [] // 평가 텍스트가 들어간건 제외\n this.kpiMetricValues = await getKpiMetricValues(this.project.id, this.project?.code)\n }\n\n // Input 요소의 값이 변경될 때 호출되는 콜백 함수\n private _onInputChange(event: InputEvent, metric: any) {\n const target = event.target as HTMLInputElement\n let inputVal: any = target.value\n\n // 숫자 타입은 다른 문자 입력 제거\n if (target.hasAttribute('numeric')) {\n inputVal = Number(inputVal.replace(/[^\\d.]/g, ''))\n }\n\n this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, inputVal)\n }\n\n /**\n * metric.periodType 에 따라 적절한 row 를 upsert.\n * - MONTH : 전월 row (YYYY-MM-01). \"오늘 입력 = 지난달 데이터\" 운영 원칙.\n * - ALLTIME : 단일 row (valueDate sentinel 무관).\n * - 그 외 (DAY/WEEK/QUARTER/YEAR/RANGE) : metric.periodType 그대로, valueDate=today.\n * 매칭은 (metricId, periodType) 기준 — MONTH 만 추가로 valueDate prefix.\n */\n private _upsertValueForMetric(values: any[], metric: any, value: any): any[] {\n const today = moment().tz('Asia/Seoul')\n const todayYmd = today.format('YYYY-MM-DD')\n const periodType = metric.periodType\n let updated = [...values]\n\n if (periodType === 'MONTH') {\n const lastMonth = today.clone().subtract(1, 'month')\n const lastMonth1 = lastMonth.format('YYYY-MM-01')\n const lastMonthYm = lastMonth.format('YYYY-MM')\n const idx = updated.findIndex(\n (i: any) =>\n i.metricId === metric.id &&\n i.periodType === 'MONTH' &&\n (i.valueDate || '').startsWith(lastMonthYm)\n )\n if (idx !== -1) {\n updated[idx] = { ...updated[idx], value }\n } else {\n updated.push({\n id: crypto.randomUUID(),\n value,\n metricId: metric.id,\n unit: metric.unit || '',\n org: this.project.id,\n periodType: 'MONTH',\n valueDate: lastMonth1\n })\n }\n } else {\n // ALLTIME / DAY / WEEK / QUARTER / YEAR / RANGE — metric.periodType 그대로 단일 row upsert.\n const idx = updated.findIndex(\n (i: any) => i.metricId === metric.id && i.periodType === periodType\n )\n if (idx !== -1) {\n updated[idx] = { ...updated[idx], value }\n } else {\n updated.push({\n id: crypto.randomUUID(),\n value,\n metricId: metric.id,\n unit: metric.unit || '',\n org: this.project.id,\n periodType,\n valueDate: todayYmd\n })\n }\n }\n\n return updated\n }\n\n private async _save() {\n // 사용자가 실제 편집한 row 만 저장. 자동 산정 metric (계획/실제 공사비·공기) 는\n // 별도의 데이터 출처(project 정보, 키스콘 수집, 다른 metric 의 전월 row 등)에서\n // 도출돼야 하므로 이 화면에서 하드코딩 derive 하지 않음.\n const response = await updateProjectCompleteStep1(this.kpiMetricValues, this.project?.code)\n if (!response.errors) {\n notify({ message: '저장되었습니다.' })\n }\n }\n\n private _reset() {\n this._getInitData()\n }\n}\n"]}