@dssp/dkpi 1.0.0-y.0 → 1.0.1

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 (213) hide show
  1. package/KPI-STATISTICS-SERVICE.md +233 -0
  2. package/_index.html +4 -9
  3. package/assets/helps/index.md +1 -1
  4. package/assets/images/project-image.png +0 -0
  5. package/assets/manifest.json +2 -2
  6. package/config/config.development.js +2 -1
  7. package/config/config.production.js +2 -1
  8. package/dist-client/bootstrap.d.ts +1 -0
  9. package/dist-client/bootstrap.js +15 -0
  10. package/dist-client/bootstrap.js.map +1 -1
  11. package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
  12. package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
  13. package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
  14. package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
  15. package/dist-client/components/kpi-boxplot-chart.js +306 -0
  16. package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
  17. package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
  18. package/dist-client/components/kpi-lookup-chart.js +464 -0
  19. package/dist-client/components/kpi-lookup-chart.js.map +1 -0
  20. package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
  21. package/dist-client/components/kpi-mini-trend-chart.js +180 -0
  22. package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
  23. package/dist-client/components/kpi-radar-chart.d.ts +17 -0
  24. package/dist-client/components/kpi-radar-chart.js +259 -0
  25. package/dist-client/components/kpi-radar-chart.js.map +1 -0
  26. package/dist-client/components/kpi-single-boxplot-chart.d.ts +14 -2
  27. package/dist-client/components/kpi-single-boxplot-chart.js +237 -137
  28. package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
  29. package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
  30. package/dist-client/components/kpi-step-lookup-chart.js +181 -0
  31. package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
  32. package/dist-client/components/kpi-trend-chart.d.ts +25 -0
  33. package/dist-client/components/kpi-trend-chart.js +241 -0
  34. package/dist-client/components/kpi-trend-chart.js.map +1 -0
  35. package/dist-client/entries/auth/checkin.d.ts +38 -0
  36. package/dist-client/entries/auth/checkin.js +551 -0
  37. package/dist-client/entries/auth/checkin.js.map +1 -0
  38. package/dist-client/google-map/common-google-map.d.ts +35 -0
  39. package/dist-client/google-map/common-google-map.js +349 -0
  40. package/dist-client/google-map/common-google-map.js.map +1 -0
  41. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  42. package/dist-client/google-map/google-map-loader.js +23 -0
  43. package/dist-client/google-map/google-map-loader.js.map +1 -0
  44. package/dist-client/pages/component/project-update-header.d.ts +1 -0
  45. package/dist-client/pages/component/project-update-header.js +127 -0
  46. package/dist-client/pages/component/project-update-header.js.map +1 -0
  47. package/dist-client/pages/kpi-admin/dssp-kpi-list-page.d.ts +22 -0
  48. package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js +57 -0
  49. package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js.map +1 -0
  50. package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
  51. package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
  52. package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
  53. package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.d.ts +20 -0
  54. package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js +445 -0
  55. package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js.map +1 -0
  56. package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
  57. package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
  58. package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
  59. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +18 -0
  60. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +259 -0
  61. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
  62. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +22 -0
  63. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +346 -0
  64. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
  65. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +26 -0
  66. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +433 -0
  67. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
  68. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
  69. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +78 -0
  70. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
  71. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
  72. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
  73. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  74. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
  75. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
  76. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  77. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
  78. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
  79. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  80. package/dist-client/pages/kpi-dashboard/kpi-alert-panel.d.ts +18 -0
  81. package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js +131 -0
  82. package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js.map +1 -0
  83. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
  84. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
  85. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  86. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +63 -0
  87. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +1097 -0
  88. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -0
  89. package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.d.ts +12 -0
  90. package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js +82 -0
  91. package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js.map +1 -0
  92. package/dist-client/pages/kpi-dashboard/kpi-history-viewer.d.ts +11 -0
  93. package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js +65 -0
  94. package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js.map +1 -0
  95. package/dist-client/pages/kpi-dashboard/kpi-list-summary.d.ts +13 -0
  96. package/dist-client/pages/kpi-dashboard/kpi-list-summary.js +115 -0
  97. package/dist-client/pages/kpi-dashboard/kpi-list-summary.js.map +1 -0
  98. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.d.ts +15 -0
  99. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js +147 -0
  100. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js.map +1 -0
  101. package/dist-client/pages/kpi-dashboard/kpi-value-entry.d.ts +7 -0
  102. package/dist-client/pages/kpi-dashboard/kpi-value-entry.js +86 -0
  103. package/dist-client/pages/kpi-dashboard/kpi-value-entry.js.map +1 -0
  104. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -2
  105. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +1 -13
  106. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
  107. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -2
  108. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +20 -11
  109. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  110. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts +1 -2
  111. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +1 -2
  112. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
  113. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -2
  114. package/dist-client/pages/kpi-value/kpi-value-list-page.js +22 -33
  115. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  116. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +56 -0
  117. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +609 -67
  118. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
  119. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +15 -3
  120. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +180 -65
  121. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
  122. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +13 -4
  123. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +213 -91
  124. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
  125. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
  126. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
  127. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
  128. package/dist-client/pages/sv-project-complete.d.ts +12 -3
  129. package/dist-client/pages/sv-project-complete.js +175 -29
  130. package/dist-client/pages/sv-project-complete.js.map +1 -1
  131. package/dist-client/pages/sv-project-completed-list.d.ts +3 -0
  132. package/dist-client/pages/sv-project-completed-list.js +81 -22
  133. package/dist-client/pages/sv-project-completed-list.js.map +1 -1
  134. package/dist-client/pages/sv-project-detail.d.ts +60 -0
  135. package/dist-client/pages/sv-project-detail.js +1092 -85
  136. package/dist-client/pages/sv-project-detail.js.map +1 -1
  137. package/dist-client/pages/sv-project-list.d.ts +20 -2
  138. package/dist-client/pages/sv-project-list.js +180 -30
  139. package/dist-client/pages/sv-project-list.js.map +1 -1
  140. package/dist-client/pages/sv-project-update.d.ts +91 -0
  141. package/dist-client/pages/sv-project-update.js +1150 -0
  142. package/dist-client/pages/sv-project-update.js.map +1 -0
  143. package/dist-client/pages/sv-user-management.d.ts +1 -0
  144. package/dist-client/pages/sv-user-management.js +5 -0
  145. package/dist-client/pages/sv-user-management.js.map +1 -1
  146. package/dist-client/route.d.ts +1 -1
  147. package/dist-client/route.js +29 -2
  148. package/dist-client/route.js.map +1 -1
  149. package/dist-client/shared/complete-api.d.ts +45 -5
  150. package/dist-client/shared/complete-api.js +177 -28
  151. package/dist-client/shared/complete-api.js.map +1 -1
  152. package/dist-client/shared/func.d.ts +1 -1
  153. package/dist-client/shared/func.js.map +1 -1
  154. package/dist-client/shared/geo-group-mapping.d.ts +25 -0
  155. package/dist-client/shared/geo-group-mapping.js +189 -0
  156. package/dist-client/shared/geo-group-mapping.js.map +1 -0
  157. package/dist-client/shared/integration-fetch.d.ts +35 -0
  158. package/dist-client/shared/integration-fetch.js +53 -0
  159. package/dist-client/shared/integration-fetch.js.map +1 -0
  160. package/dist-client/shared/kpi-project-sync.d.ts +16 -0
  161. package/dist-client/shared/kpi-project-sync.js +129 -0
  162. package/dist-client/shared/kpi-project-sync.js.map +1 -0
  163. package/dist-client/tsconfig.tsbuildinfo +1 -1
  164. package/dist-client/viewparts/menu-tools.d.ts +11 -3
  165. package/dist-client/viewparts/menu-tools.js +126 -75
  166. package/dist-client/viewparts/menu-tools.js.map +1 -1
  167. package/dist-server/scripts/calculate-kpi-scores.js +65 -3
  168. package/dist-server/scripts/calculate-kpi-scores.js.map +1 -1
  169. package/dist-server/scripts/load-grade-data-migration.d.ts +4 -0
  170. package/dist-server/scripts/load-grade-data-migration.js +95 -10
  171. package/dist-server/scripts/load-grade-data-migration.js.map +1 -1
  172. package/dist-server/scripts/propagate-parent-kpi-values.d.ts +4 -0
  173. package/dist-server/scripts/propagate-parent-kpi-values.js +423 -77
  174. package/dist-server/scripts/propagate-parent-kpi-values.js.map +1 -1
  175. package/dist-server/scripts/recalculate-by-project-name.d.ts +2 -0
  176. package/dist-server/scripts/recalculate-by-project-name.js +72 -0
  177. package/dist-server/scripts/recalculate-by-project-name.js.map +1 -0
  178. package/dist-server/service/index.d.ts +1 -3
  179. package/dist-server/service/index.js +3 -4
  180. package/dist-server/service/index.js.map +1 -1
  181. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +192 -1
  182. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +993 -38
  183. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  184. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +9 -1
  185. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +33 -8
  186. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
  187. package/dist-server/service/kpi-stat/index.d.ts +4 -0
  188. package/dist-server/service/kpi-stat/index.js +8 -0
  189. package/dist-server/service/kpi-stat/index.js.map +1 -0
  190. package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
  191. package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
  192. package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
  193. package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
  194. package/dist-server/service/kpi-stat/kpi-stat-types.js +180 -0
  195. package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
  196. package/dist-server/service/kpi-value/kpi-value-query.d.ts +2 -1
  197. package/dist-server/service/kpi-value/kpi-value-query.js +49 -6
  198. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  199. package/dist-server/tsconfig.tsbuildinfo +1 -1
  200. package/kpi-module-service-tests.md +1286 -0
  201. package/kpi-module-test-report.md +676 -0
  202. package/kpi-module-unit-test-detailed-report.md +925 -0
  203. package/kpi-module-unit-tests-detailed.md +1452 -0
  204. package/openapi/unstable.yaml +2 -2
  205. package/package.json +58 -54
  206. package/recalculate-batch.sh +64 -0
  207. package/recalculate-projects-range.sh +98 -0
  208. package/schema.graphql +1159 -55
  209. package/things-factory.config.js +6 -1
  210. package/translations/en.json +5 -1
  211. package/translations/ko.json +5 -1
  212. package/views/auth-page.html +4 -5
  213. package/views/public/home.html +5 -6
@@ -1,33 +1,59 @@
1
+ var SvProjectCompleteTab1Plan_1;
1
2
  import { __decorate, __metadata } from "tslib";
2
3
  import { css, html, LitElement } from 'lit';
3
- import { customElement, property } from 'lit/decorators.js';
4
- import { calcDiff, calcDateDiff } from '../../shared/func';
5
- import { getKpiMetricValues, getKpiMetrics, updateProjectCompleteStep1 } from '../../shared/complete-api';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { calcDateDiff } from '../../shared/func';
6
+ import { getKpiMetricValues, getKpiMetrics, updateProjectCompleteStep1, collectProjectExternalData } from '../../shared/complete-api';
7
+ import { INTEGRATION_SOURCES } from '../../shared/integration-fetch';
8
+ import { syncKpiToProjectInfo } from '../../shared/kpi-project-sync';
6
9
  import moment from 'moment-timezone';
7
10
  import { notify } from '@operato/layout';
11
+ import { hasPrivilege } from '@things-factory/auth-base/dist-client';
8
12
  const KPI_METRIC_KEY_MAPPING = [
9
13
  { label: '공사기간', projectKey: 'constructionPeriod' },
10
14
  { label: '총 근로자수', projectKey: 'totalWorkerCount' },
11
15
  { label: '연간 근로자 수', projectKey: 'annualWorkerCount' },
12
- { label: '투입인력', projectKey: 'workerCount' },
16
+ { label: '투입인력(M/M)', projectKey: 'workerCount' },
13
17
  { label: '재해 건수', projectKey: 'accidentCount' },
14
18
  { label: '일정 이탈 수준', projectKey: 'scheduleDeviationLevel' },
15
19
  { label: '총 건설 폐기물 발생량', projectKey: 'totalConstructionWasteAmount' },
16
- { label: '용적율', projectKey: 'floorAreaRatio' },
20
+ { label: '용적률', projectKey: 'floorAreaRatio' },
17
21
  { label: '총 설계 변경 건', projectKey: 'designChangeCount' },
18
- { label: '공사비', projectKey: 'constructionCost' },
22
+ { label: '계획공사비', projectKey: 'constructionCost' },
19
23
  { label: '연면적', projectKey: 'area' },
20
24
  { label: '지상층수', projectKey: 'upperFloorCount' },
21
25
  { label: '지하층수', projectKey: 'lowerFloorCount' }
22
26
  ];
23
- let SvProjectCompleteTab1Plan = class SvProjectCompleteTab1Plan extends LitElement {
27
+ // 계획값이 없는 것들 (실제값으로 표시할 필드)
28
+ const NO_PLAN_FIELDS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount', 'upperFloorCount', 'lowerFloorCount'];
29
+ let SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = class SvProjectCompleteTab1Plan extends LitElement {
24
30
  constructor() {
25
31
  super(...arguments);
26
32
  this.kpiMetricValues = [];
27
33
  this.kpiMetrics = [];
28
34
  this.project = {};
35
+ /** 연동 진행 상태: source → 'idle' | 'collecting' | 'done' */
36
+ this.collectStatus = {};
37
+ this.collectError = {};
38
+ /** 자동 수집한 실제값의 출처: metricId → 시스템 라벨 */
39
+ this.valueSources = {};
40
+ /** 자동 수집한 계획값: projectKey → 값 (키스콘이 제공하는 계획공사기간/계획공사비) */
41
+ this.collectedPlan = {};
42
+ /** 계획값 출처: projectKey → 시스템 라벨 */
43
+ this.planSources = {};
44
+ /** 시스템별 수집 요약 (카드 표시용): 라벨 → [{label, text}] */
45
+ this.collectedSummary = {};
46
+ /** 방금 채워진 셀 강조용 (metricId 또는 plan:projectKey) */
47
+ this.justFilled = {};
48
+ this.collecting = false;
49
+ /** kpi:input — Step1 계획값 입력/저장 권한 */
50
+ this.canSave = false;
51
+ /** kpi:auto-collect — 외부 시스템 자동 수집 권한 */
52
+ this.canAutoCollect = false;
29
53
  }
30
54
  render() {
55
+ // 전월 (last month) YYYY-MM. 월별 metric 의 "전월 데이터 입력" 검사 기준.
56
+ const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
31
57
  return html `
32
58
  <div class="title">
33
59
  <div>
@@ -37,47 +63,279 @@ let SvProjectCompleteTab1Plan = class SvProjectCompleteTab1Plan extends LitEleme
37
63
  </div>
38
64
  </div>
39
65
 
40
- <div class="rows">
41
- <div class="row header">
42
- <div class="header-label">기본정보</div>
43
- <div class="header-label">계획</div>
44
- <div class="header-label">실제</div>
45
- <div class="header-label">편차</div>
46
- </div>
66
+ ${this._renderCollectPanel()}
47
67
 
48
- ${this.kpiMetrics.map(metric => {
49
- var _a, _b, _c;
50
- // 상수로 정의된 매핑 정보에서 metric.name으로 projectKey를 찾음
51
- const projectKey = ((_a = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)) === null || _a === void 0 ? void 0 : _a.projectKey) || '';
52
- const kpiMetricValue = this.kpiMetricValues.find((item) => item.metricId === metric.id) || {};
53
- // planValue는 project에서 찾고 없으면 buildingComplex의 값에서 찾음
54
- let planValue = this.project[projectKey] || ((_c = (_b = this.project) === null || _b === void 0 ? void 0 : _b.buildingComplex) === null || _c === void 0 ? void 0 : _c[projectKey]) || 0;
55
- // 공사기간은 기간을 일 단위로 계산
56
- if (projectKey === 'constructionPeriod') {
57
- planValue = calcDateDiff(this.project.startDate, this.project.endDate);
58
- }
59
- const diffValue = calcDiff(planValue, kpiMetricValue.value);
60
- const diffClass = diffValue === 0 ? '' : diffValue > 0 ? 'plus' : 'minus';
61
- const diffSign = diffValue === 0 ? '' : diffValue > 0 ? '+' : '-';
62
- const unit = kpiMetricValue.unit || metric.unit || '';
63
- return html `<div class="row">
64
- <div class="label">• ${metric.name}</div>
65
- <div class="cell"><input .value=${planValue} disabled /> ${unit}</div>
66
- <div class="cell">
67
- <input numeric .value=${kpiMetricValue.value || 0} @input=${(e) => this._onInputChange(e, metric)} />
68
- ${unit}
69
- </div>
70
- <div class="unit ${diffClass}">${diffSign} ${Math.abs(diffValue).toLocaleString()} ${unit}</div>
71
- </div>`;
72
- })}
68
+ <div class="rows">
69
+ ${this._renderSection('기본정보', this.kpiMetrics.filter((m) => m.periodType !== 'MONTH'), lastMonth)}
70
+ ${
71
+ // '월별 데이터' 섹션 당분간 숨김. 복원하려면 아래 주석을 해제하고 '' 제거.
72
+ // this._renderSection('월별 데이터', this.kpiMetrics.filter((m: any) => m.periodType === 'MONTH'), lastMonth, true)
73
+ ''}
73
74
 
74
75
  <div class="button-line">
75
76
  <div class="ghost-btn" @click=${this._reset}>초기화</div>
76
- <div class="ghost-btn secondary" @click=${() => this._save()}>저장</div>
77
+ <div
78
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
79
+ title=${this.canSave ? '' : 'kpi:input 권한 필요'}
80
+ @click=${() => this.canSave && this._save()}
81
+ >
82
+ 저장
83
+ </div>
77
84
  </div>
78
85
  </div>
79
86
  `;
80
87
  }
88
+ /** '기본정보' / '월별 데이터' 섹션 렌더. 해당 그룹에 metric 이 없으면 섹션 자체를 생략. */
89
+ _renderSection(title, metrics, lastMonth, topGap = false) {
90
+ if (!metrics.length)
91
+ return '';
92
+ return html `
93
+ <div class="row header ${topGap ? 'section-gap' : ''}">
94
+ <div class="header-label">${title}</div>
95
+ <div class="header-label">값</div>
96
+ <div class="header-label">설명</div>
97
+ </div>
98
+ ${metrics.map(metric => this._renderMetricRow(metric, lastMonth))}
99
+ `;
100
+ }
101
+ _renderMetricRow(metric, lastMonth) {
102
+ var _a, _b, _c, _d;
103
+ // 상수로 정의된 매핑 정보에서 metric.name으로 projectKey를 찾음
104
+ const projectKey = ((_a = KPI_METRIC_KEY_MAPPING.find(item => item.label === metric.name)) === null || _a === void 0 ? void 0 : _a.projectKey) || '';
105
+ const isMonthly = metric.periodType === 'MONTH';
106
+ // 현재값 lookup:
107
+ // MONTH → 전월 row (단일 진실원)
108
+ // ALLTIME → 단일 ALLTIME row
109
+ // 그 외 → metricId+periodType 매칭
110
+ const kpiMetricValue = isMonthly
111
+ ? this.kpiMetricValues.find((item) => item.metricId === metric.id && item.periodType === 'MONTH' && (item.valueDate || '').startsWith(lastMonth)) || {}
112
+ : this.kpiMetricValues.find((item) => item.metricId === metric.id && item.periodType === metric.periodType) || {};
113
+ const isDisplayInput = projectKey === 'constructionPeriod' || projectKey === 'constructionCost'; // 유효한 계획 필드
114
+ // planValue는 project에서 찾고 없으면 buildingComplex의 값에서 찾음
115
+ 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;
116
+ let planValue = basePlanValue;
117
+ const unit = kpiMetricValue.unit || metric.unit || '';
118
+ // 2. 계획 값 처리
119
+ // 공사기간은 기간을 일 단위로 계산
120
+ if (projectKey === 'constructionPeriod') {
121
+ planValue = Math.ceil(calcDateDiff(this.project.startDate, this.project.endDate) / 30);
122
+ }
123
+ else if (!isDisplayInput) {
124
+ planValue = 0;
125
+ }
126
+ // 1. 실제 값 처리 kpi값을 찾고, 없으면 (NO_PLAN_FIELDS에 해당하는 필드는 원래 계획값 사용)
127
+ const actualValue = (_d = kpiMetricValue.value) !== null && _d !== void 0 ? _d : (NO_PLAN_FIELDS.includes(projectKey) ? basePlanValue : 0);
128
+ const src = this.valueSources[metric.id];
129
+ // periodType 무관하게 값 없으면 pending. 메시지는 MONTH 만 "전월 데이터 입력 필요".
130
+ const isPending = kpiMetricValue.value === undefined || kpiMetricValue.value === null;
131
+ return html `<div class="row">
132
+ <div
133
+ class="label ${isPending ? 'pending' : ''}"
134
+ title=${isPending ? (isMonthly ? '전월 데이터 입력 필요' : '아직 입력되지 않은 항목') : ''}
135
+ >
136
+ ${metric.name}
137
+ </div>
138
+ <div class="cell ${this.justFilled[metric.id] ? 'just-filled' : ''} ${isPending ? 'pending' : ''}">
139
+ <input numeric .value=${actualValue !== null && actualValue !== void 0 ? actualValue : 0} @input=${(e) => this._onInputChange(e, metric)} />
140
+ ${unit} ${src ? html `<span class="src-chip">🔗 ${src}</span>` : ''}
141
+ </div>
142
+ <div class="desc">${metric.description || ''}</div>
143
+ </div>`;
144
+ }
145
+ _renderCollectPanel() {
146
+ return html `
147
+ <div class="collect-panel">
148
+ <div class="collect-head">
149
+ <div class="ttl">시스템 연동 자동 수집 <small>외부 시스템에서 기본정보를 가져옵니다</small></div>
150
+ <div
151
+ class="collect-btn ${this.canAutoCollect ? '' : 'disabled'}"
152
+ ?disabled=${this.collecting}
153
+ title=${this.canAutoCollect ? '' : 'kpi:auto-collect 권한 필요'}
154
+ @click=${() => (this.canAutoCollect && !this.collecting ? this._autoCollect() : null)}
155
+ >
156
+ ${this.collecting ? '수집 중…' : '⟳ 자동 수집'}
157
+ </div>
158
+ </div>
159
+ <div class="source-list">
160
+ ${INTEGRATION_SOURCES.map(meta => {
161
+ const status = this.collectStatus[meta.source] || 'idle';
162
+ const errMsg = this.collectError[meta.source];
163
+ // 카드 안의 수집 필드 summary — 실제/계획 무관 collectedSummary 기준으로 통일.
164
+ // (valueSources 만 보면 키스콘처럼 계획값만 채우는 시스템은 표시 안 됨)
165
+ const collectedFields = this._collectedFieldsOf(meta.label);
166
+ const statusText = status === 'collecting' ? '연동 중…' : status === 'done' ? '완료 ✓' : status === 'error' ? '정보 없음 ⓘ' : '대기';
167
+ return html `
168
+ <div class="source-card ${status}">
169
+ <div class="sc-head">
170
+ <span class="sys-icon">${meta.icon}</span>
171
+ <span>${meta.label}</span>
172
+ <span class="sc-status ${status}">${statusText}</span>
173
+ </div>
174
+ <div class="sc-desc">${meta.desc}</div>
175
+ ${status === 'error' && errMsg ? html `<div class="sc-error-msg">${errMsg}</div>` : ''}
176
+ ${status === 'done' && collectedFields.length
177
+ ? html `<div class="sc-fields">
178
+ ${collectedFields.map(f => html `<div class="sc-field"><span>${f.label}</span><b>${f.text}</b></div>`)}
179
+ </div>`
180
+ : ''}
181
+ </div>
182
+ `;
183
+ })}
184
+ </div>
185
+ </div>
186
+ `;
187
+ }
188
+ /** 패널 카드에 표시할, 해당 시스템에서 수집한 값 요약 */
189
+ _collectedFieldsOf(sourceLabel) {
190
+ return this.collectedSummary[sourceLabel] || [];
191
+ }
192
+ /** 자동 수집 — 백엔드의 collectProjectExternalData mutation 1번 호출.
193
+ * 서버가 시나리오 3종 (세움터/올바로/키스콘) 을 순차 실행하고 시스템별 ok/메시지를 반환.
194
+ * 성공/실패와 무관하게 시나리오 step 이 KPI Value 를 DB 에 적재하므로 호출 후
195
+ * KPI Metric Values 를 재조회해 form 을 자동 갱신.
196
+ *
197
+ * 운영 현실: 키스콘·올바로는 시공사 자격증명 미확보가 많아 오류 빈도 높음.
198
+ * 세움터는 공공데이터 서비스키만으로 동작 가능. 시스템별로 카드에 독립 표시.
199
+ */
200
+ async _autoCollect() {
201
+ var _a;
202
+ if (!((_a = this.project) === null || _a === void 0 ? void 0 : _a.id)) {
203
+ notify({ message: '프로젝트 정보가 없습니다.' });
204
+ return;
205
+ }
206
+ this.collecting = true;
207
+ this.collectStatus = {};
208
+ this.collectError = {};
209
+ this.collectedSummary = {};
210
+ this.collectedPlan = {};
211
+ this.planSources = {};
212
+ this.valueSources = {};
213
+ // 모든 카드를 collecting 상태로 표시
214
+ const collecting = {};
215
+ for (const meta of INTEGRATION_SOURCES)
216
+ collecting[meta.source] = 'collecting';
217
+ this.collectStatus = collecting;
218
+ try {
219
+ const results = await collectProjectExternalData(this.project.id);
220
+ // 시스템별 status / error 메시지 업데이트 + result data 를 form 에 매핑
221
+ const status = {};
222
+ const errors = {};
223
+ for (const r of results) {
224
+ status[r.source] = r.ok ? 'done' : 'error';
225
+ if (!r.ok) {
226
+ errors[r.source] = r.message;
227
+ continue;
228
+ }
229
+ // 시나리오 result 객체 = { projectKey: value, ... }.
230
+ // KPI_METRIC_KEY_MAPPING 매칭 키 → form 채움. 미매칭 키 (siteType,
231
+ // structureType 등 프로젝트/BuildingComplex 기본 속성) → 카드 요약만.
232
+ const summary = [];
233
+ // KPI_METRIC_KEY_MAPPING 에 없는 키들의 한글 라벨. BuildingComplex/Project 직접 속성용.
234
+ const NON_KPI_LABEL = {
235
+ siteType: '건축프로젝트유형',
236
+ structureType: '구조형태',
237
+ coverageRatio: '건폐율',
238
+ householdCount: '세대수',
239
+ buildingCount: '동수',
240
+ startDate: '착공일',
241
+ endDate: '준공일',
242
+ permitDate: '건축허가일',
243
+ bldNm: '건물명'
244
+ };
245
+ for (const [projectKey, rawValue] of Object.entries(r.data || {})) {
246
+ if (rawValue === null || rawValue === undefined || rawValue === '')
247
+ continue;
248
+ // 비-primitive (객체/배열) 값은 시나리오 측 return 형태 오류 — form 매핑/표시
249
+ // 모두 skip 하고 console 에 진단 로그만 남김. 흔한 원인은 시나리오 마지막 step 이
250
+ // `return { result: { ... } }` 처럼 wrap 한 케이스.
251
+ if (typeof rawValue === 'object') {
252
+ console.warn(`[자동수집] '${r.label}'.${projectKey} 가 object — 시나리오 return 형태 점검`, rawValue);
253
+ continue;
254
+ }
255
+ const mapping = KPI_METRIC_KEY_MAPPING.find(x => x.projectKey === projectKey);
256
+ const label = (mapping === null || mapping === void 0 ? void 0 : mapping.label) || NON_KPI_LABEL[projectKey] || projectKey;
257
+ const isPlan = SvProjectCompleteTab1Plan_1.PLAN_KEYS.includes(projectKey);
258
+ // summary 카드 표시용 — string 값은 그대로, number 는 toLocaleString
259
+ const numericValue = typeof rawValue === 'number' ? rawValue : Number(rawValue);
260
+ const isNumeric = Number.isFinite(numericValue) && typeof rawValue !== 'string';
261
+ const valueText = isNumeric ? numericValue.toLocaleString() : String(rawValue);
262
+ summary.push({ label, text: valueText });
263
+ // KPI 매핑이 없는 키 (siteType/structureType 등) 는 form 채움 skip — 단지 노출만.
264
+ if (!mapping)
265
+ continue;
266
+ if (isPlan) {
267
+ // 계획값 (키스콘 제공) — 계획 칼럼 표시 + KpiMetric value 에도 upsert
268
+ // 하여 _save 시 patches 에 포함. 사용자가 계획 input 직접 편집해도 같은 흐름.
269
+ this.collectedPlan = Object.assign(Object.assign({}, this.collectedPlan), { [projectKey]: numericValue });
270
+ this.planSources = Object.assign(Object.assign({}, this.planSources), { [projectKey]: r.label });
271
+ this.justFilled = Object.assign(Object.assign({}, this.justFilled), { [`plan:${projectKey}`]: true });
272
+ 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; });
273
+ if (planMetric)
274
+ this._setMetricValue(planMetric, numericValue);
275
+ }
276
+ else {
277
+ // 실제값 (세움터/올바로/기타) — 매칭되는 metric 의 실제 칼럼에 반영
278
+ const metric = 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; });
279
+ if (!metric)
280
+ continue;
281
+ this._setMetricValue(metric, numericValue);
282
+ this.valueSources = Object.assign(Object.assign({}, this.valueSources), { [metric.id]: r.label });
283
+ this.justFilled = Object.assign(Object.assign({}, this.justFilled), { [metric.id]: true });
284
+ }
285
+ }
286
+ this.collectedSummary = Object.assign(Object.assign({}, this.collectedSummary), { [r.label]: summary });
287
+ }
288
+ this.collectStatus = status;
289
+ this.collectError = errors;
290
+ // 재조회 안 함 — 시나리오는 fetch + result 반환만 책임 (DB 적재 X). 직전에
291
+ // _setMetricValue 가 memory 에 채운 값들을 DB 의 옛 값으로 덮어쓰면 자동수집
292
+ // 결과가 폼에 반영되지 않음. 사용자가 [저장] 버튼 누르면 그제서야 DB 반영.
293
+ this.requestUpdate();
294
+ setTimeout(() => (this.justFilled = {}), 1300);
295
+ const okCount = results.filter(r => r.ok).length;
296
+ if (okCount === results.length) {
297
+ notify({ message: `외부 시스템 ${okCount}건 모두 수집 완료. 검토 후 [저장]을 눌러주세요.` });
298
+ }
299
+ else if (okCount === 0) {
300
+ notify({ message: '외부 시스템에서 가져올 정보가 없습니다.' });
301
+ }
302
+ else {
303
+ notify({ message: `외부 시스템 수집 — ${okCount}건 완료, 나머지는 정보 없음.` });
304
+ }
305
+ }
306
+ catch (e) {
307
+ // mutation 자체 실패 (네트워크/권한 등) — 모든 카드를 정보 없음 으로
308
+ const msg = e instanceof Error ? e.message : String(e);
309
+ console.warn('[자동수집] 전체 호출 실패', e);
310
+ const status = {};
311
+ const errors = {};
312
+ for (const meta of INTEGRATION_SOURCES) {
313
+ status[meta.source] = 'error';
314
+ errors[meta.source] = '업데이트할 정보가 없습니다';
315
+ }
316
+ this.collectStatus = status;
317
+ this.collectError = errors;
318
+ notify({ message: '외부 시스템 연동이 일시적으로 안 됩니다.' });
319
+ }
320
+ finally {
321
+ this.collecting = false;
322
+ }
323
+ }
324
+ /**
325
+ * 메트릭의 실제값 set/add — _onInputChange 와 동일하게 periodType 별 row upsert.
326
+ */
327
+ _setMetricValue(metric, value) {
328
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, value);
329
+ }
330
+ async connectedCallback() {
331
+ super.connectedCallback();
332
+ const [canSave, canAutoCollect] = await Promise.all([
333
+ hasPrivilege({ category: 'kpi', privilege: 'input', domainOwnerGranted: true, superUserGranted: true }),
334
+ hasPrivilege({ category: 'kpi', privilege: 'auto-collect', domainOwnerGranted: true, superUserGranted: true })
335
+ ]);
336
+ this.canSave = canSave;
337
+ this.canAutoCollect = canAutoCollect;
338
+ }
81
339
  willUpdate(changedProperties) {
82
340
  var _a;
83
341
  super.willUpdate(changedProperties);
@@ -87,9 +345,23 @@ let SvProjectCompleteTab1Plan = class SvProjectCompleteTab1Plan extends LitEleme
87
345
  }
88
346
  }
89
347
  async _getInitData() {
90
- const kpiMetrics = await getKpiMetrics();
91
- this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || []; // 평가 텍스트가 들어간건 제외
92
- this.kpiMetricValues = await getKpiMetricValues(this.project.id);
348
+ var _a, _b;
349
+ // getKpiMetrics 이미 active=true 반환. 여기선 평가 (Step2) 추가 제외.
350
+ // 비활성화는 KPI 관리 admin 에서 active=false 처리.
351
+ const kpiMetrics = await getKpiMetrics((_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
352
+ // 평가(Step2) 만 제외. 나머지는 기본정보 탭에서 '기본정보'/'월별 데이터' 두 섹션으로 구분 렌더.
353
+ this.kpiMetrics = kpiMetrics.filter(item => !item.name.includes('평가')) || [];
354
+ this.kpiMetricValues = await getKpiMetricValues(this.project.id, (_b = this.project) === null || _b === void 0 ? void 0 : _b.code);
355
+ }
356
+ /**
357
+ * 계획공사비/계획공사기간 의 "계획" 컬럼 input 편집 핸들러 — collectedPlan 표시값 동기화 +
358
+ * 해당 KpiMetric 의 value 도 upsert 하여 _save 시 patches 에 포함되도록.
359
+ */
360
+ _onPlanInputChange(event, metric, projectKey) {
361
+ const target = event.target;
362
+ const inputVal = Number(target.value.replace(/[^\d.-]/g, ''));
363
+ this.collectedPlan = Object.assign(Object.assign({}, this.collectedPlan), { [projectKey]: inputVal });
364
+ this.kpiMetricValues = this._upsertValueForMetric(this.kpiMetricValues, metric, inputVal);
93
365
  }
94
366
  // Input 요소의 값이 변경될 때 호출되는 콜백 함수
95
367
  _onInputChange(event, metric) {
@@ -97,33 +369,77 @@ let SvProjectCompleteTab1Plan = class SvProjectCompleteTab1Plan extends LitEleme
97
369
  let inputVal = target.value;
98
370
  // 숫자 타입은 다른 문자 입력 제거
99
371
  if (target.hasAttribute('numeric')) {
100
- inputVal = Number(inputVal.replace(/[^\d.]/g, ''));
372
+ inputVal = Number(inputVal.replace(/[^\d.-]/g, ''));
101
373
  }
102
- // 기존 배열에서 해당 id를 가진 항목을 찾음
103
- const existingItemIndex = this.kpiMetricValues.findIndex((item) => item.metricId === metric.id);
104
- if (existingItemIndex !== -1) {
105
- // 기존 항목이 있으면 업데이트
106
- this.kpiMetricValues = this.kpiMetricValues.map((item) => item.metricId === metric.id ? Object.assign(Object.assign({}, item), { value: inputVal }) : item);
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 && i.periodType === 'MONTH' && (i.valueDate || '').startsWith(lastMonthYm));
393
+ if (idx !== -1) {
394
+ updated[idx] = Object.assign(Object.assign({}, updated[idx]), { value });
395
+ }
396
+ else {
397
+ updated.push({
398
+ id: crypto.randomUUID(),
399
+ value,
400
+ metricId: metric.id,
401
+ unit: metric.unit || '',
402
+ org: this.project.id,
403
+ periodType: 'MONTH',
404
+ valueDate: lastMonth1
405
+ });
406
+ }
107
407
  }
108
408
  else {
109
- // 기존 항목이 없으면 새로 추가
110
- this.kpiMetricValues = [
111
- ...this.kpiMetricValues,
112
- {
409
+ // ALLTIME / DAY / WEEK / QUARTER / YEAR / RANGE — metric.periodType 그대로 단일 row upsert.
410
+ const idx = updated.findIndex((i) => i.metricId === metric.id && i.periodType === periodType);
411
+ if (idx !== -1) {
412
+ updated[idx] = Object.assign(Object.assign({}, updated[idx]), { value });
413
+ }
414
+ else {
415
+ updated.push({
113
416
  id: crypto.randomUUID(),
114
- value: inputVal,
417
+ value,
115
418
  metricId: metric.id,
116
419
  unit: metric.unit || '',
117
- org: this.project.id, // 프로젝트 ID 추가
118
- periodType: metric.periodType,
119
- valueDate: moment().tz('Asia/Seoul').format('YYYY-MM-DD')
120
- }
121
- ];
420
+ org: this.project.id,
421
+ periodType,
422
+ valueDate: todayYmd
423
+ });
424
+ }
122
425
  }
426
+ return updated;
123
427
  }
124
428
  async _save() {
125
- const response = await updateProjectCompleteStep1(this.kpiMetricValues);
429
+ var _a, _b;
430
+ // 사용자가 실제 편집한 row 만 저장. 자동 산정 metric (계획/실제 공사비·공기) 는
431
+ // 별도의 데이터 출처(project 정보, 키스콘 수집, 다른 metric 의 전월 row 등)에서
432
+ // 도출돼야 하므로 이 화면에서 하드코딩 derive 하지 않음.
433
+ const response = await updateProjectCompleteStep1(this.kpiMetricValues, (_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
126
434
  if (!response.errors) {
435
+ // 양방향 동기 — 연면적/용적률/투입인력/설계변경건 값을 BuildingComplex(프로젝트 정보)에도 반영.
436
+ // 보조 동기 실패가 저장 성공 UX 를 막지 않도록 warn 만.
437
+ try {
438
+ await syncKpiToProjectInfo(this.project, this.kpiMetrics, this.kpiMetricValues, (_b = this.project) === null || _b === void 0 ? void 0 : _b.code);
439
+ }
440
+ catch (e) {
441
+ console.warn('[KPI→프로젝트정보] BuildingComplex 동기 실패', e);
442
+ }
127
443
  notify({ message: '저장되었습니다.' });
128
444
  }
129
445
  }
@@ -153,7 +469,7 @@ SvProjectCompleteTab1Plan.styles = [
153
469
  min-height: 35px;
154
470
  background: #f3f3fa;
155
471
  border-top: 2px #0c4da2 solid;
156
- grid-template-columns: 220px 1fr 1fr 200px;
472
+ grid-template-columns: 220px 320px 1fr;
157
473
  padding: 0px 25px;
158
474
 
159
475
  .header-label {
@@ -161,9 +477,13 @@ SvProjectCompleteTab1Plan.styles = [
161
477
  text-align: center;
162
478
  }
163
479
  }
480
+ /* '기본정보' / '월별 데이터' 섹션 사이 간격 */
481
+ .row.header.section-gap {
482
+ margin-top: 24px;
483
+ }
164
484
  .row {
165
485
  display: grid;
166
- grid-template-columns: 220px 1fr 1fr 200px;
486
+ grid-template-columns: 220px 320px 1fr;
167
487
  gap: 6px 10px;
168
488
  align-items: center;
169
489
  padding: 8px 25px;
@@ -172,7 +492,11 @@ SvProjectCompleteTab1Plan.styles = [
172
492
  .cell {
173
493
  display: flex;
174
494
  align-items: center;
175
- justify-content: center;
495
+ justify-content: flex-start;
496
+ flex-wrap: nowrap;
497
+ white-space: nowrap;
498
+ gap: 6px;
499
+ padding-right: 12px;
176
500
  }
177
501
  }
178
502
  .label {
@@ -181,7 +505,8 @@ SvProjectCompleteTab1Plan.styles = [
181
505
  letter-spacing: -0.05em;
182
506
  white-space: nowrap;
183
507
  display: flex;
184
- justify-content: center;
508
+ justify-content: flex-end;
509
+ padding-right: 12px;
185
510
  }
186
511
  input {
187
512
  padding: 6px 8px;
@@ -191,6 +516,7 @@ SvProjectCompleteTab1Plan.styles = [
191
516
  color: #212529;
192
517
  font-size: 16px;
193
518
  margin-right: 5px;
519
+ text-align: right;
194
520
 
195
521
  &:disabled {
196
522
  background: #f6f6f6;
@@ -200,6 +526,12 @@ SvProjectCompleteTab1Plan.styles = [
200
526
  text-align: center;
201
527
  color: #212529;
202
528
  }
529
+ .desc {
530
+ color: #5a6168;
531
+ font-size: 12px;
532
+ line-height: 1.4;
533
+ padding-left: 8px;
534
+ }
203
535
  .plus {
204
536
  color: #e13232;
205
537
  font-weight: 700;
@@ -227,21 +559,231 @@ SvProjectCompleteTab1Plan.styles = [
227
559
  .ghost-btn.secondary {
228
560
  background: #24be7b;
229
561
  }
562
+ .ghost-btn.disabled,
563
+ .collect-btn.disabled {
564
+ opacity: 0.45;
565
+ cursor: not-allowed;
566
+ }
567
+
568
+ /* ── 자동 수집 패널 ── */
569
+ .collect-panel {
570
+ margin: 4px 6px 14px;
571
+ border: 1px solid #d7e3f2;
572
+ border-radius: 8px;
573
+ background: linear-gradient(180deg, #f7fafe 0%, #ffffff 100%);
574
+ overflow: hidden;
575
+ }
576
+ .collect-head {
577
+ display: flex;
578
+ align-items: center;
579
+ justify-content: space-between;
580
+ padding: 10px 16px;
581
+ background: #eaf2fb;
582
+ border-bottom: 1px solid #d7e3f2;
583
+ }
584
+ .collect-head .ttl {
585
+ color: #0c4da2;
586
+ font-weight: 700;
587
+ font-size: 14px;
588
+ letter-spacing: -0.03em;
589
+ }
590
+ .collect-head .ttl small {
591
+ color: #5a7da6;
592
+ font-weight: 400;
593
+ margin-left: 8px;
594
+ }
595
+ .collect-btn {
596
+ display: inline-flex;
597
+ align-items: center;
598
+ gap: 6px;
599
+ padding: 7px 14px;
600
+ background: #0c4da2;
601
+ color: #fff;
602
+ border-radius: 6px;
603
+ cursor: pointer;
604
+ font-size: 13px;
605
+ font-weight: 600;
606
+ }
607
+ .collect-btn[disabled] {
608
+ background: #9bb4d2;
609
+ cursor: default;
610
+ }
611
+ .source-list {
612
+ display: flex;
613
+ gap: 10px;
614
+ padding: 12px 16px;
615
+ flex-wrap: wrap;
616
+ }
617
+ .source-card {
618
+ flex: 1 1 200px;
619
+ border: 1px solid #e3e9f0;
620
+ border-radius: 7px;
621
+ padding: 10px 12px;
622
+ background: #fff;
623
+ transition:
624
+ border-color 0.2s,
625
+ box-shadow 0.2s;
626
+ }
627
+ .source-card.collecting {
628
+ border-color: #2e79be;
629
+ box-shadow: 0 0 0 3px rgba(46, 121, 190, 0.12);
630
+ }
631
+ .source-card.done {
632
+ border-color: #24be7b;
633
+ }
634
+ .source-card .sc-head {
635
+ display: flex;
636
+ align-items: center;
637
+ gap: 8px;
638
+ font-weight: 600;
639
+ color: #212529;
640
+ font-size: 13px;
641
+ }
642
+ .source-card .sc-head .sys-icon {
643
+ font-size: 16px;
644
+ }
645
+ .source-card .sc-status {
646
+ margin-left: auto;
647
+ font-size: 12px;
648
+ }
649
+ .sc-status.idle {
650
+ color: #aab4c0;
651
+ }
652
+ .sc-status.collecting {
653
+ color: #2e79be;
654
+ }
655
+ .sc-status.done {
656
+ color: #24be7b;
657
+ font-weight: 700;
658
+ }
659
+ .sc-status.error {
660
+ color: #8a8a8a;
661
+ font-weight: 600;
662
+ }
663
+ .source-card.error {
664
+ background: #fafafa;
665
+ border-color: #e0e0e0;
666
+ }
667
+ .source-card .sc-error-msg {
668
+ margin-top: 6px;
669
+ padding: 6px 8px;
670
+ border-radius: 4px;
671
+ background: #f3f4f6;
672
+ color: #6b7280;
673
+ font-size: 12px;
674
+ line-height: 1.4;
675
+ word-break: break-word;
676
+ }
677
+ .source-card .sc-fields {
678
+ margin-top: 8px;
679
+ display: flex;
680
+ flex-direction: column;
681
+ gap: 3px;
682
+ }
683
+ .sc-field {
684
+ font-size: 12px;
685
+ color: #5a6b7b;
686
+ display: flex;
687
+ justify-content: space-between;
688
+ }
689
+ .sc-field b {
690
+ color: #0c4da2;
691
+ }
692
+ .sc-desc {
693
+ font-size: 11px;
694
+ color: #9aa7b4;
695
+ margin-top: 2px;
696
+ }
697
+ /* 가져온 값 출처 칩 */
698
+ .src-chip {
699
+ display: inline-flex;
700
+ align-items: center;
701
+ margin-left: 6px;
702
+ padding: 1px 7px;
703
+ font-size: 11px;
704
+ border-radius: 10px;
705
+ background: #e7f3ec;
706
+ color: #1d9c63;
707
+ white-space: nowrap;
708
+ }
709
+ .cell.just-filled input {
710
+ animation: flash 1.1s ease;
711
+ }
712
+ /* 미입력 — kpiMetricValue 가 비어있는 metric 행 */
713
+ .label.pending::before {
714
+ content: '⚠';
715
+ color: #e74c3c;
716
+ margin-right: 4px;
717
+ }
718
+ .cell.pending input {
719
+ border-color: #e74c3c;
720
+ background-color: #fff5f5;
721
+ }
722
+ @keyframes flash {
723
+ 0% {
724
+ background: #fff7cc;
725
+ }
726
+ 100% {
727
+ background: #ffffff;
728
+ }
729
+ }
230
730
  `
231
731
  ];
732
+ /** 계획 칼럼을 갖는 항목 (키스콘이 계획값을 제공) */
733
+ SvProjectCompleteTab1Plan.PLAN_KEYS = ['constructionPeriod', 'constructionCost'];
232
734
  __decorate([
233
- property({ type: Array }),
735
+ state(),
234
736
  __metadata("design:type", Object)
235
737
  ], SvProjectCompleteTab1Plan.prototype, "kpiMetricValues", void 0);
236
738
  __decorate([
237
- property({ type: Array }),
739
+ state(),
238
740
  __metadata("design:type", Object)
239
741
  ], SvProjectCompleteTab1Plan.prototype, "kpiMetrics", void 0);
240
742
  __decorate([
241
743
  property({ type: Object }),
242
744
  __metadata("design:type", Object)
243
745
  ], SvProjectCompleteTab1Plan.prototype, "project", void 0);
244
- SvProjectCompleteTab1Plan = __decorate([
746
+ __decorate([
747
+ state(),
748
+ __metadata("design:type", Object)
749
+ ], SvProjectCompleteTab1Plan.prototype, "collectStatus", void 0);
750
+ __decorate([
751
+ state(),
752
+ __metadata("design:type", Object)
753
+ ], SvProjectCompleteTab1Plan.prototype, "collectError", void 0);
754
+ __decorate([
755
+ state(),
756
+ __metadata("design:type", Object)
757
+ ], SvProjectCompleteTab1Plan.prototype, "valueSources", void 0);
758
+ __decorate([
759
+ state(),
760
+ __metadata("design:type", Object)
761
+ ], SvProjectCompleteTab1Plan.prototype, "collectedPlan", void 0);
762
+ __decorate([
763
+ state(),
764
+ __metadata("design:type", Object)
765
+ ], SvProjectCompleteTab1Plan.prototype, "planSources", void 0);
766
+ __decorate([
767
+ state(),
768
+ __metadata("design:type", Object)
769
+ ], SvProjectCompleteTab1Plan.prototype, "collectedSummary", void 0);
770
+ __decorate([
771
+ state(),
772
+ __metadata("design:type", Object)
773
+ ], SvProjectCompleteTab1Plan.prototype, "justFilled", void 0);
774
+ __decorate([
775
+ state(),
776
+ __metadata("design:type", Object)
777
+ ], SvProjectCompleteTab1Plan.prototype, "collecting", void 0);
778
+ __decorate([
779
+ state(),
780
+ __metadata("design:type", Object)
781
+ ], SvProjectCompleteTab1Plan.prototype, "canSave", void 0);
782
+ __decorate([
783
+ state(),
784
+ __metadata("design:type", Object)
785
+ ], SvProjectCompleteTab1Plan.prototype, "canAutoCollect", void 0);
786
+ SvProjectCompleteTab1Plan = SvProjectCompleteTab1Plan_1 = __decorate([
245
787
  customElement('sv-pc-tab1-plan')
246
788
  ], SvProjectCompleteTab1Plan);
247
789
  export { SvProjectCompleteTab1Plan };