@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.
- package/KPI-STATISTICS-SERVICE.md +233 -0
- package/_index.html +4 -9
- package/assets/helps/index.md +1 -1
- package/assets/images/project-image.png +0 -0
- package/assets/manifest.json +2 -2
- package/config/config.development.js +2 -1
- package/config/config.production.js +2 -1
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +15 -0
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
- package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
- package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
- package/dist-client/components/kpi-boxplot-chart.js +306 -0
- package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
- package/dist-client/components/kpi-lookup-chart.js +464 -0
- package/dist-client/components/kpi-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
- package/dist-client/components/kpi-mini-trend-chart.js +180 -0
- package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
- package/dist-client/components/kpi-radar-chart.d.ts +17 -0
- package/dist-client/components/kpi-radar-chart.js +259 -0
- package/dist-client/components/kpi-radar-chart.js.map +1 -0
- package/dist-client/components/kpi-single-boxplot-chart.d.ts +14 -2
- package/dist-client/components/kpi-single-boxplot-chart.js +237 -137
- package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
- package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
- package/dist-client/components/kpi-step-lookup-chart.js +181 -0
- package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-trend-chart.d.ts +25 -0
- package/dist-client/components/kpi-trend-chart.js +241 -0
- package/dist-client/components/kpi-trend-chart.js.map +1 -0
- package/dist-client/entries/auth/checkin.d.ts +38 -0
- package/dist-client/entries/auth/checkin.js +551 -0
- package/dist-client/entries/auth/checkin.js.map +1 -0
- package/dist-client/google-map/common-google-map.d.ts +35 -0
- package/dist-client/google-map/common-google-map.js +349 -0
- package/dist-client/google-map/common-google-map.js.map +1 -0
- package/dist-client/google-map/google-map-loader.d.ts +6 -0
- package/dist-client/google-map/google-map-loader.js +23 -0
- package/dist-client/google-map/google-map-loader.js.map +1 -0
- package/dist-client/pages/component/project-update-header.d.ts +1 -0
- package/dist-client/pages/component/project-update-header.js +127 -0
- package/dist-client/pages/component/project-update-header.js.map +1 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-list-page.d.ts +22 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js +57 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js.map +1 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
- package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.d.ts +20 -0
- package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js +445 -0
- package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js.map +1 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +18 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +259 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +22 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +346 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +26 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +433 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +78 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-alert-panel.d.ts +18 -0
- package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js +131 -0
- package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +63 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +1097 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.d.ts +12 -0
- package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js +82 -0
- package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-history-viewer.d.ts +11 -0
- package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js +65 -0
- package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-list-summary.d.ts +13 -0
- package/dist-client/pages/kpi-dashboard/kpi-list-summary.js +115 -0
- package/dist-client/pages/kpi-dashboard/kpi-list-summary.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-performance-summary.d.ts +15 -0
- package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js +147 -0
- package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-value-entry.d.ts +7 -0
- package/dist-client/pages/kpi-dashboard/kpi-value-entry.js +86 -0
- package/dist-client/pages/kpi-dashboard/kpi-value-entry.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +1 -13
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +20 -11
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -2
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +22 -33
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +56 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +609 -67
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +15 -3
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +180 -65
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +13 -4
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +213 -91
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
- package/dist-client/pages/sv-project-complete.d.ts +12 -3
- package/dist-client/pages/sv-project-complete.js +175 -29
- package/dist-client/pages/sv-project-complete.js.map +1 -1
- package/dist-client/pages/sv-project-completed-list.d.ts +3 -0
- package/dist-client/pages/sv-project-completed-list.js +81 -22
- package/dist-client/pages/sv-project-completed-list.js.map +1 -1
- package/dist-client/pages/sv-project-detail.d.ts +60 -0
- package/dist-client/pages/sv-project-detail.js +1092 -85
- package/dist-client/pages/sv-project-detail.js.map +1 -1
- package/dist-client/pages/sv-project-list.d.ts +20 -2
- package/dist-client/pages/sv-project-list.js +180 -30
- package/dist-client/pages/sv-project-list.js.map +1 -1
- package/dist-client/pages/sv-project-update.d.ts +91 -0
- package/dist-client/pages/sv-project-update.js +1150 -0
- package/dist-client/pages/sv-project-update.js.map +1 -0
- package/dist-client/pages/sv-user-management.d.ts +1 -0
- package/dist-client/pages/sv-user-management.js +5 -0
- package/dist-client/pages/sv-user-management.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +29 -2
- package/dist-client/route.js.map +1 -1
- package/dist-client/shared/complete-api.d.ts +45 -5
- package/dist-client/shared/complete-api.js +177 -28
- package/dist-client/shared/complete-api.js.map +1 -1
- package/dist-client/shared/func.d.ts +1 -1
- package/dist-client/shared/func.js.map +1 -1
- package/dist-client/shared/geo-group-mapping.d.ts +25 -0
- package/dist-client/shared/geo-group-mapping.js +189 -0
- package/dist-client/shared/geo-group-mapping.js.map +1 -0
- package/dist-client/shared/integration-fetch.d.ts +35 -0
- package/dist-client/shared/integration-fetch.js +53 -0
- package/dist-client/shared/integration-fetch.js.map +1 -0
- package/dist-client/shared/kpi-project-sync.d.ts +16 -0
- package/dist-client/shared/kpi-project-sync.js +129 -0
- package/dist-client/shared/kpi-project-sync.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/menu-tools.d.ts +11 -3
- package/dist-client/viewparts/menu-tools.js +126 -75
- package/dist-client/viewparts/menu-tools.js.map +1 -1
- package/dist-server/scripts/calculate-kpi-scores.js +65 -3
- package/dist-server/scripts/calculate-kpi-scores.js.map +1 -1
- package/dist-server/scripts/load-grade-data-migration.d.ts +4 -0
- package/dist-server/scripts/load-grade-data-migration.js +95 -10
- package/dist-server/scripts/load-grade-data-migration.js.map +1 -1
- package/dist-server/scripts/propagate-parent-kpi-values.d.ts +4 -0
- package/dist-server/scripts/propagate-parent-kpi-values.js +423 -77
- package/dist-server/scripts/propagate-parent-kpi-values.js.map +1 -1
- package/dist-server/scripts/recalculate-by-project-name.d.ts +2 -0
- package/dist-server/scripts/recalculate-by-project-name.js +72 -0
- package/dist-server/scripts/recalculate-by-project-name.js.map +1 -0
- package/dist-server/service/index.d.ts +1 -3
- package/dist-server/service/index.js +3 -4
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +192 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +993 -38
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +9 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +33 -8
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
- package/dist-server/service/kpi-stat/index.d.ts +4 -0
- package/dist-server/service/kpi-stat/index.js +8 -0
- package/dist-server/service/kpi-stat/index.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js +180 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +2 -1
- package/dist-server/service/kpi-value/kpi-value-query.js +49 -6
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/kpi-module-service-tests.md +1286 -0
- package/kpi-module-test-report.md +676 -0
- package/kpi-module-unit-test-detailed-report.md +925 -0
- package/kpi-module-unit-tests-detailed.md +1452 -0
- package/openapi/unstable.yaml +2 -2
- package/package.json +58 -54
- package/recalculate-batch.sh +64 -0
- package/recalculate-projects-range.sh +98 -0
- package/schema.graphql +1159 -55
- package/things-factory.config.js +6 -1
- package/translations/en.json +5 -1
- package/translations/ko.json +5 -1
- package/views/auth-page.html +4 -5
- 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 {
|
|
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: '
|
|
20
|
+
{ label: '용적률', projectKey: 'floorAreaRatio' },
|
|
17
21
|
{ label: '총 설계 변경 건', projectKey: 'designChangeCount' },
|
|
18
|
-
{ label: '
|
|
22
|
+
{ label: '계획공사비', projectKey: 'constructionCost' },
|
|
19
23
|
{ label: '연면적', projectKey: 'area' },
|
|
20
24
|
{ label: '지상층수', projectKey: 'upperFloorCount' },
|
|
21
25
|
{ label: '지하층수', projectKey: 'lowerFloorCount' }
|
|
22
26
|
];
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
372
|
+
inputVal = Number(inputVal.replace(/[^\d.-]/g, ''));
|
|
101
373
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
417
|
+
value,
|
|
115
418
|
metricId: metric.id,
|
|
116
419
|
unit: metric.unit || '',
|
|
117
|
-
org: this.project.id,
|
|
118
|
-
periodType
|
|
119
|
-
valueDate:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
735
|
+
state(),
|
|
234
736
|
__metadata("design:type", Object)
|
|
235
737
|
], SvProjectCompleteTab1Plan.prototype, "kpiMetricValues", void 0);
|
|
236
738
|
__decorate([
|
|
237
|
-
|
|
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
|
-
|
|
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 };
|