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

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 (68) 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-admin/kpi-system-guide.d.ts +1 -1
  11. package/dist-client/pages/kpi-admin/kpi-system-guide.js +29 -21
  12. package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -1
  13. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -1
  14. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +1 -1
  15. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  16. package/dist-client/pages/kpi-value/kpi-value-list-page.js +1 -1
  17. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  18. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +21 -2
  19. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +166 -134
  20. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
  21. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +4 -2
  22. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +109 -44
  23. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
  24. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +3 -0
  25. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +32 -4
  26. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
  27. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +24 -0
  28. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +365 -157
  29. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -1
  30. package/dist-client/pages/sv-project-complete.d.ts +4 -1
  31. package/dist-client/pages/sv-project-complete.js +43 -12
  32. package/dist-client/pages/sv-project-complete.js.map +1 -1
  33. package/dist-client/pages/sv-project-completed-list.js +3 -3
  34. package/dist-client/pages/sv-project-completed-list.js.map +1 -1
  35. package/dist-client/pages/sv-project-detail.d.ts +11 -0
  36. package/dist-client/pages/sv-project-detail.js +188 -46
  37. package/dist-client/pages/sv-project-detail.js.map +1 -1
  38. package/dist-client/pages/sv-project-list.d.ts +10 -0
  39. package/dist-client/pages/sv-project-list.js +96 -6
  40. package/dist-client/pages/sv-project-list.js.map +1 -1
  41. package/dist-client/pages/sv-project-update.d.ts +86 -0
  42. package/dist-client/pages/sv-project-update.js +1121 -0
  43. package/dist-client/pages/sv-project-update.js.map +1 -0
  44. package/dist-client/route.d.ts +1 -1
  45. package/dist-client/route.js +3 -0
  46. package/dist-client/route.js.map +1 -1
  47. package/dist-client/shared/complete-api.d.ts +10 -9
  48. package/dist-client/shared/complete-api.js +47 -19
  49. package/dist-client/shared/complete-api.js.map +1 -1
  50. package/dist-client/tsconfig.tsbuildinfo +1 -1
  51. package/dist-client/viewparts/menu-tools.js +47 -54
  52. package/dist-client/viewparts/menu-tools.js.map +1 -1
  53. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +23 -0
  54. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +72 -28
  55. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  56. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +9 -2
  57. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
  58. package/dist-server/service/kpi-stat/kpi-stat-query.js +19 -18
  59. package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -1
  60. package/dist-server/service/kpi-value/kpi-value-query.js +2 -2
  61. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  62. package/dist-server/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +3 -3
  64. package/schema.graphql +13 -1
  65. package/things-factory.config.js +1 -0
  66. package/dist-client/shared/domain-context.d.ts +0 -7
  67. package/dist-client/shared/domain-context.js +0 -13
  68. 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,29 +113,48 @@ 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
- ? html `<input .value=${shownPlan} disabled /> ${unit}
127
+ ? html `<input
128
+ numeric
129
+ .value=${shownPlan !== null && shownPlan !== void 0 ? shownPlan : 0}
130
+ @input=${(e) => this._onPlanInputChange(e, metric, projectKey)}
131
+ />
132
+ ${unit}
114
133
  ${planSrc ? html `<span class="src-chip">🔗 ${planSrc}</span>` : ''}`
115
134
  : html `-`}
116
135
  </div>
117
- <div class="cell ${this.justFilled[metric.id] ? 'just-filled' : ''}">
136
+ <div class="cell ${this.justFilled[metric.id] ? 'just-filled' : ''} ${isPending ? 'pending' : ''}">
118
137
  <input numeric .value=${actualValue !== null && actualValue !== void 0 ? actualValue : 0} @input=${(e) => this._onInputChange(e, metric)} />
119
138
  ${unit}
120
139
  ${src ? html `<span class="src-chip">🔗 ${src}</span>` : ''}
121
140
  </div>
122
- <div class="unit ${isDisplayInput ? effDiffClass : diffClass}">
123
- ${isDisplayInput ? effDiffSign : diffSign}
124
- ${Math.abs(isDisplayInput ? effDiff : diffValue).toLocaleString()} ${unit}
141
+ <div class="unit ${isDisplayInput ? effDiffClass : ''}">
142
+ ${isDisplayInput
143
+ ? html `${effDiffSign} ${Math.abs(effDiff).toLocaleString()} ${unit}`
144
+ : ''}
125
145
  </div>
126
146
  </div>`;
127
147
  })}
128
148
 
129
149
  <div class="button-line">
130
150
  <div class="ghost-btn" @click=${this._reset}>초기화</div>
131
- <div class="ghost-btn secondary" @click=${() => this._save()}>저장</div>
151
+ <div
152
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
153
+ title=${this.canSave ? '' : 'kpi:input 권한 필요'}
154
+ @click=${() => this.canSave && this._save()}
155
+ >
156
+ 저장
157
+ </div>
132
158
  </div>
133
159
  </div>
134
160
  `;
@@ -139,9 +165,10 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
139
165
  <div class="collect-head">
140
166
  <div class="ttl">시스템 연동 자동 수집 <small>외부 시스템에서 기본정보를 가져옵니다</small></div>
141
167
  <div
142
- class="collect-btn"
168
+ class="collect-btn ${this.canAutoCollect ? '' : 'disabled'}"
143
169
  ?disabled=${this.collecting}
144
- @click=${() => (this.collecting ? null : this._autoCollect())}
170
+ title=${this.canAutoCollect ? '' : 'kpi:auto-collect 권한 필요'}
171
+ @click=${() => this.canAutoCollect && !this.collecting ? this._autoCollect() : null}
145
172
  >
146
173
  ${this.collecting ? '수집 중…' : '⟳ 자동 수집'}
147
174
  </div>
@@ -226,9 +253,17 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
226
253
  // KPI_METRIC_KEY_MAPPING 매칭 키 → form 채움. 미매칭 키 (siteType,
227
254
  // structureType 등 프로젝트/BuildingComplex 기본 속성) → 카드 요약만.
228
255
  const summary = [];
256
+ // KPI_METRIC_KEY_MAPPING 에 없는 키들의 한글 라벨. BuildingComplex/Project 직접 속성용.
229
257
  const NON_KPI_LABEL = {
230
258
  siteType: '건축현장유형',
231
- structureType: '구조형태'
259
+ structureType: '구조형태',
260
+ coverageRatio: '건폐율',
261
+ householdCount: '세대수',
262
+ buildingCount: '동수',
263
+ startDate: '착공일',
264
+ endDate: '준공일',
265
+ permitDate: '건축허가일',
266
+ bldNm: '건물명'
232
267
  };
233
268
  for (const [projectKey, rawValue] of Object.entries(r.data || {})) {
234
269
  if (rawValue === null || rawValue === undefined || rawValue === '')
@@ -252,10 +287,14 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
252
287
  if (!mapping)
253
288
  continue;
254
289
  if (isPlan) {
255
- // 계획값 (키스콘) — 계획 칼럼에 반영
290
+ // 계획값 (키스콘 제공) — 계획 칼럼 표시 + KpiMetric value 에도 upsert
291
+ // 하여 _save 시 patches 에 포함. 사용자가 계획 input 직접 편집해도 같은 흐름.
256
292
  this.collectedPlan = Object.assign(Object.assign({}, this.collectedPlan), { [projectKey]: numericValue });
257
293
  this.planSources = Object.assign(Object.assign({}, this.planSources), { [projectKey]: r.label });
258
294
  this.justFilled = Object.assign(Object.assign({}, this.justFilled), { [`plan:${projectKey}`]: true });
295
+ const planMetric = this.kpiMetrics.find((m) => { var _a; return ((_a = KPI_METRIC_KEY_MAPPING.find(x => x.label === m.name)) === null || _a === void 0 ? void 0 : _a.projectKey) === projectKey; });
296
+ if (planMetric)
297
+ this._setMetricValue(planMetric, numericValue);
259
298
  }
260
299
  else {
261
300
  // 실제값 (세움터/올바로/기타) — 매칭되는 metric 의 실제 칼럼에 반영
@@ -305,26 +344,20 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
305
344
  this.collecting = false;
306
345
  }
307
346
  }
308
- /** 메트릭의 실제값 셀을 set/add (사용자 입력과 동일 경로) */
347
+ /**
348
+ * 메트릭의 실제값 set/add — _onInputChange 와 동일하게 periodType 별 row upsert.
349
+ */
309
350
  _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
- }
351
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, value);
352
+ }
353
+ async connectedCallback() {
354
+ super.connectedCallback();
355
+ const [canSave, canAutoCollect] = await Promise.all([
356
+ hasPrivilege({ category: 'kpi', privilege: 'input', domainOwnerGranted: true, superUserGranted: true }),
357
+ hasPrivilege({ category: 'kpi', privilege: 'auto-collect', domainOwnerGranted: true, superUserGranted: true })
358
+ ]);
359
+ this.canSave = canSave;
360
+ this.canAutoCollect = canAutoCollect;
328
361
  }
329
362
  willUpdate(changedProperties) {
330
363
  var _a;
@@ -335,9 +368,22 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
335
368
  }
336
369
  }
337
370
  async _getInitData() {
338
- const kpiMetrics = await getKpiMetrics();
339
- this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || []; // 평가 텍스트가 들어간건 제외
340
- this.kpiMetricValues = await getKpiMetricValues(this.project.id);
371
+ var _a, _b;
372
+ // getKpiMetrics 이미 active=true 반환. 여기선 평가 (Step2) 추가 제외.
373
+ // 비활성화는 KPI 관리 admin 에서 active=false 처리.
374
+ const kpiMetrics = await getKpiMetrics((_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
375
+ this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || [];
376
+ this.kpiMetricValues = await getKpiMetricValues(this.project.id, (_b = this.project) === null || _b === void 0 ? void 0 : _b.code);
377
+ }
378
+ /**
379
+ * 공사비/공사기간 의 "계획" 컬럼 input 편집 핸들러 — collectedPlan 표시값 동기화 +
380
+ * 해당 KpiMetric 의 value 도 upsert 하여 _save 시 patches 에 포함되도록.
381
+ */
382
+ _onPlanInputChange(event, metric, projectKey) {
383
+ const target = event.target;
384
+ const inputVal = Number(target.value.replace(/[^\d.]/g, ''));
385
+ this.collectedPlan = Object.assign(Object.assign({}, this.collectedPlan), { [projectKey]: inputVal });
386
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, inputVal);
341
387
  }
342
388
  // Input 요소의 값이 변경될 때 호출되는 콜백 함수
343
389
  _onInputChange(event, metric) {
@@ -347,105 +393,68 @@ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCom
347
393
  if (target.hasAttribute('numeric')) {
348
394
  inputVal = Number(inputVal.replace(/[^\d.]/g, ''));
349
395
  }
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
- {
396
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, inputVal);
397
+ }
398
+ /**
399
+ * metric.periodType 따라 적절한 row 를 upsert.
400
+ * - MONTH : 전월 row (YYYY-MM-01). "오늘 입력 = 지난달 데이터" 운영 원칙.
401
+ * - ALLTIME : 단일 row (valueDate sentinel 무관).
402
+ * - 그 외 (DAY/WEEK/QUARTER/YEAR/RANGE) : metric.periodType 그대로, valueDate=today.
403
+ * 매칭은 (metricId, periodType) 기준 — MONTH 만 추가로 valueDate prefix.
404
+ */
405
+ _upsertValueForMetric(values, metric, value) {
406
+ const today = moment().tz('Asia/Seoul');
407
+ const todayYmd = today.format('YYYY-MM-DD');
408
+ const periodType = metric.periodType;
409
+ let updated = [...values];
410
+ if (periodType === 'MONTH') {
411
+ const lastMonth = today.clone().subtract(1, 'month');
412
+ const lastMonth1 = lastMonth.format('YYYY-MM-01');
413
+ const lastMonthYm = lastMonth.format('YYYY-MM');
414
+ const idx = updated.findIndex((i) => i.metricId === metric.id &&
415
+ i.periodType === 'MONTH' &&
416
+ (i.valueDate || '').startsWith(lastMonthYm));
417
+ if (idx !== -1) {
418
+ updated[idx] = Object.assign(Object.assign({}, updated[idx]), { value });
419
+ }
420
+ else {
421
+ updated.push({
361
422
  id: crypto.randomUUID(),
362
- value: inputVal,
423
+ value,
363
424
  metricId: metric.id,
364
425
  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;
426
+ org: this.project.id,
427
+ periodType: 'MONTH',
428
+ valueDate: lastMonth1
429
+ });
399
430
  }
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);
431
+ }
432
+ else {
433
+ // ALLTIME / DAY / WEEK / QUARTER / YEAR / RANGE — metric.periodType 그대로 단일 row upsert.
434
+ const idx = updated.findIndex((i) => i.metricId === metric.id && i.periodType === periodType);
435
+ if (idx !== -1) {
436
+ updated[idx] = Object.assign(Object.assign({}, updated[idx]), { value });
404
437
  }
405
- else if (excludeField.name == '실제공사기간') {
406
- value = actualConstructionPeriodValue;
438
+ else {
439
+ updated.push({
440
+ id: crypto.randomUUID(),
441
+ value,
442
+ metricId: metric.id,
443
+ unit: metric.unit || '',
444
+ org: this.project.id,
445
+ periodType,
446
+ valueDate: todayYmd
447
+ });
407
448
  }
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;
449
+ }
450
+ return updated;
419
451
  }
420
452
  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);
453
+ var _a;
454
+ // 사용자가 실제 편집한 row 만 저장. 자동 산정 metric (계획/실제 공사비·공기)
455
+ // 별도의 데이터 출처(project 정보, 키스콘 수집, 다른 metric 의 전월 row 등)에서
456
+ // 도출돼야 하므로 화면에서 하드코딩 derive 하지 않음.
457
+ const response = await updateProjectCompleteStep1(this.kpiMetricValues, (_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
449
458
  if (!response.errors) {
450
459
  notify({ message: '저장되었습니다.' });
451
460
  }
@@ -550,6 +559,11 @@ SvProjectCompleteTab1Plan.styles = [
550
559
  .ghost-btn.secondary {
551
560
  background: #24be7b;
552
561
  }
562
+ .ghost-btn.disabled,
563
+ .collect-btn.disabled {
564
+ opacity: 0.45;
565
+ cursor: not-allowed;
566
+ }
553
567
 
554
568
  /* ── 자동 수집 패널 ── */
555
569
  .collect-panel {
@@ -693,6 +707,16 @@ SvProjectCompleteTab1Plan.styles = [
693
707
  .cell.just-filled input {
694
708
  animation: flash 1.1s ease;
695
709
  }
710
+ /* 미입력 — kpiMetricValue 가 비어있는 metric 행 */
711
+ .label.pending::before {
712
+ content: '⚠';
713
+ color: #e74c3c;
714
+ margin-right: 4px;
715
+ }
716
+ .cell.pending input {
717
+ border-color: #e74c3c;
718
+ background-color: #fff5f5;
719
+ }
696
720
  @keyframes flash {
697
721
  0% {
698
722
  background: #fff7cc;
@@ -749,6 +773,14 @@ __decorate([
749
773
  state(),
750
774
  __metadata("design:type", Object)
751
775
  ], SvProjectCompleteTab1Plan.prototype, "collecting", void 0);
776
+ __decorate([
777
+ state(),
778
+ __metadata("design:type", Object)
779
+ ], SvProjectCompleteTab1Plan.prototype, "canSave", void 0);
780
+ __decorate([
781
+ state(),
782
+ __metadata("design:type", Object)
783
+ ], SvProjectCompleteTab1Plan.prototype, "canAutoCollect", void 0);
752
784
  SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = __decorate([
753
785
  customElement('sv-pc-tab1-plan')
754
786
  ], SvProjectCompleteTab1Plan);