@dssp/dkpi 1.0.0-alpha.55 → 1.0.0-alpha.56
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/assets/images/project-image.png +0 -0
- package/dist-client/components/kpi-boxplot-chart.js +48 -76
- package/dist-client/components/kpi-boxplot-chart.js.map +1 -1
- package/dist-client/components/kpi-radar-chart.d.ts +1 -0
- package/dist-client/components/kpi-radar-chart.js +85 -7
- package/dist-client/components/kpi-radar-chart.js.map +1 -1
- package/dist-client/components/kpi-single-boxplot-chart.js +24 -6
- package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
- package/dist-client/pages/sv-project-complete.js +3 -1
- package/dist-client/pages/sv-project-complete.js.map +1 -1
- package/dist-client/pages/sv-project-completed-list.d.ts +2 -2
- package/dist-client/pages/sv-project-completed-list.js +14 -14
- package/dist-client/pages/sv-project-completed-list.js.map +1 -1
- package/dist-client/pages/sv-project-detail.d.ts +8 -0
- package/dist-client/pages/sv-project-detail.js +203 -58
- package/dist-client/pages/sv-project-detail.js.map +1 -1
- package/dist-client/pages/sv-project-list.d.ts +7 -2
- package/dist-client/pages/sv-project-list.js +13 -13
- package/dist-client/pages/sv-project-list.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +5 -6
- package/dist-server/service/kpi-stat/kpi-stat-query.js +108 -100
- package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -1
- package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +5 -12
- package/dist-server/service/kpi-stat/kpi-stat-types.js +72 -42
- package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/schema.graphql +41 -0
|
@@ -13,10 +13,13 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
13
13
|
constructor() {
|
|
14
14
|
super(...arguments);
|
|
15
15
|
this.project = {};
|
|
16
|
+
this.kpiComprehensiveStats = [];
|
|
17
|
+
this.kpiYComprehensiveStats = [];
|
|
16
18
|
}
|
|
17
19
|
get context() {
|
|
20
|
+
var _a;
|
|
18
21
|
return {
|
|
19
|
-
title: '프로젝트 상세 정보'
|
|
22
|
+
title: this.project ? `프로젝트 상세 정보 - ${(_a = this.project) === null || _a === void 0 ? void 0 : _a.name}` : '프로젝트 상세 정보'
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
render() {
|
|
@@ -31,9 +34,12 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
31
34
|
</div>
|
|
32
35
|
|
|
33
36
|
<div class="top-info" style="margin-top: 10px;">
|
|
34
|
-
<img pic src=${((_a = this.project.mainPhoto) === null || _a === void 0 ? void 0 : _a.fullpath) || '/assets/images/
|
|
37
|
+
<img pic src=${((_a = this.project.mainPhoto) === null || _a === void 0 ? void 0 : _a.fullpath) || '/assets/images/project-image.png'} alt="project" />
|
|
35
38
|
<div class="info">
|
|
36
|
-
<div class="
|
|
39
|
+
<div class="row">
|
|
40
|
+
<div class="label">• 주소</div>
|
|
41
|
+
<div class="value">${(_b = this.project.buildingComplex) === null || _b === void 0 ? void 0 : _b.address}</div>
|
|
42
|
+
</div>
|
|
37
43
|
<div class="row">
|
|
38
44
|
<div class="label">• 공사기간</div>
|
|
39
45
|
<div class="value">${this.project.startDate} ~ ${this.project.endDate}</div>
|
|
@@ -44,11 +50,11 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
44
50
|
</div>
|
|
45
51
|
<div class="row">
|
|
46
52
|
<div class="label">• 요청상세</div>
|
|
47
|
-
<div class="value bold"
|
|
53
|
+
<div class="value bold">000</div>
|
|
48
54
|
</div>
|
|
49
55
|
<div class="row">
|
|
50
56
|
<div class="label">• 알람</div>
|
|
51
|
-
<div class="value"
|
|
57
|
+
<div class="value">000</div>
|
|
52
58
|
</div>
|
|
53
59
|
</div>
|
|
54
60
|
</div>
|
|
@@ -64,11 +70,11 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
64
70
|
</div>
|
|
65
71
|
<div class="grid-item row">
|
|
66
72
|
<div class="label">• 용적률</div>
|
|
67
|
-
<div class="value"
|
|
73
|
+
<div class="value">000 %</div>
|
|
68
74
|
</div>
|
|
69
75
|
<div class="grid-item row">
|
|
70
76
|
<div class="label">• 투입인력</div>
|
|
71
|
-
<div class="value"
|
|
77
|
+
<div class="value">000 명</div>
|
|
72
78
|
</div>
|
|
73
79
|
</div>
|
|
74
80
|
</div>
|
|
@@ -135,17 +141,7 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
135
141
|
<div class="kpi-summary">
|
|
136
142
|
<div class="mini-boxplot" title="boxplot placeholder">
|
|
137
143
|
<div class="boxplot-area">
|
|
138
|
-
<kpi-single-boxplot-chart
|
|
139
|
-
.data=${{
|
|
140
|
-
min: 0,
|
|
141
|
-
max: 100,
|
|
142
|
-
mean: 50,
|
|
143
|
-
median: 60,
|
|
144
|
-
q1: 25,
|
|
145
|
-
q3: 75,
|
|
146
|
-
value: this.project.kpi
|
|
147
|
-
}}
|
|
148
|
-
></kpi-single-boxplot-chart>
|
|
144
|
+
<kpi-single-boxplot-chart .data=${this.getBoxPlotDataForProject(this.project)}></kpi-single-boxplot-chart>
|
|
149
145
|
</div>
|
|
150
146
|
|
|
151
147
|
<div class="boxplot-point">
|
|
@@ -156,40 +152,9 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
156
152
|
|
|
157
153
|
<div class="spider-area">
|
|
158
154
|
<kpi-radar-chart
|
|
159
|
-
.data=${
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
category: '생산성 성과',
|
|
163
|
-
value: 41
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
group: '평균',
|
|
167
|
-
category: '일정 성과',
|
|
168
|
-
value: 26
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
group: '평균',
|
|
172
|
-
category: '비용 성과',
|
|
173
|
-
value: 30
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
group: '평균',
|
|
177
|
-
category: '품질 성과',
|
|
178
|
-
value: 21
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
group: '평균',
|
|
182
|
-
category: '안전 성과',
|
|
183
|
-
value: 33
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
group: '평균',
|
|
187
|
-
category: '환경 성과',
|
|
188
|
-
value: 15
|
|
189
|
-
}
|
|
190
|
-
]}
|
|
191
|
-
.categories=${['생산성 성과', '일정 성과', '비용 성과', '품질 성과', '안전 성과', '환경 성과']}
|
|
192
|
-
.currentGroup=${'평균'}
|
|
155
|
+
.data=${this.getRadarChartData().data}
|
|
156
|
+
.categories=${this.getRadarChartData().categories}
|
|
157
|
+
.currentGroup=${'프로젝트성과'}
|
|
193
158
|
></kpi-radar-chart>
|
|
194
159
|
</div>
|
|
195
160
|
</div>
|
|
@@ -197,15 +162,19 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
197
162
|
|
|
198
163
|
<div class="card group-box">
|
|
199
164
|
<div class="kpi-header">
|
|
200
|
-
<div
|
|
201
|
-
|
|
165
|
+
<div>
|
|
166
|
+
<div>그룹별 분포</div>
|
|
167
|
+
<div class="triangle"></div>
|
|
168
|
+
</div>
|
|
202
169
|
</div>
|
|
203
170
|
<div class="desc">
|
|
204
171
|
Boxplot(박스플롯)은 각 카테고리별로 값의 분포(최소, 1사분위, 중앙값, 3사분위, 최대, 평균, 이상치 등)를 보여줍니다.
|
|
205
172
|
박스는 중앙 50% 구간, 수염은 전체 범위, 굵은 검정색 가로선은 중앙값(메디안), 초록색 원은 평균값(Mean)을 의미합니다.
|
|
206
173
|
이 프로젝트의 값은 진한 오렌지색 원으로 별도 강조되어 표시되며, 중앙값/평균과 다를 수 있습니다.
|
|
207
174
|
</div>
|
|
208
|
-
<div class="group-chart"
|
|
175
|
+
<div class="group-chart">
|
|
176
|
+
<kpi-boxplot-chart .data=${this.getYKpiBoxplotData()}></kpi-boxplot-chart>
|
|
177
|
+
</div>
|
|
209
178
|
</div>
|
|
210
179
|
</div>
|
|
211
180
|
</div>
|
|
@@ -213,7 +182,9 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
213
182
|
}
|
|
214
183
|
async pageUpdated(changes, lifecycle) {
|
|
215
184
|
if (this.active) {
|
|
216
|
-
|
|
185
|
+
this.initProject(lifecycle.resourceId);
|
|
186
|
+
this.getKpiComprehensiveStats();
|
|
187
|
+
this.getKpiYComprehensiveStats();
|
|
217
188
|
}
|
|
218
189
|
}
|
|
219
190
|
async initProject(projectId = '') {
|
|
@@ -234,6 +205,10 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
234
205
|
totalProgress
|
|
235
206
|
weeklyProgress
|
|
236
207
|
kpi
|
|
208
|
+
kpiValues {
|
|
209
|
+
kpiName
|
|
210
|
+
value
|
|
211
|
+
}
|
|
237
212
|
inspPassRate
|
|
238
213
|
robotProgressRate
|
|
239
214
|
structuralSafetyRate
|
|
@@ -274,6 +249,167 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
|
|
|
274
249
|
if (response.errors)
|
|
275
250
|
return;
|
|
276
251
|
this.project = ((_a = response.data) === null || _a === void 0 ? void 0 : _a.project) || {};
|
|
252
|
+
this.updateContext();
|
|
253
|
+
}
|
|
254
|
+
async getKpiComprehensiveStats() {
|
|
255
|
+
try {
|
|
256
|
+
const response = await client.query({
|
|
257
|
+
query: gql `
|
|
258
|
+
query GetKpiZValueComprehensiveStats {
|
|
259
|
+
totalKpiZValueComprehensiveStats {
|
|
260
|
+
minVal
|
|
261
|
+
q1Val
|
|
262
|
+
medVal
|
|
263
|
+
q3Val
|
|
264
|
+
maxVal
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
`
|
|
268
|
+
});
|
|
269
|
+
this.kpiComprehensiveStats = response.data.totalKpiZValueComprehensiveStats || [];
|
|
270
|
+
console.log('KPI Z-Value Comprehensive Stats:', this.kpiComprehensiveStats);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.error('Failed to fetch KPI comprehensive stats:', error);
|
|
274
|
+
this.kpiComprehensiveStats = [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async getKpiYComprehensiveStats() {
|
|
278
|
+
try {
|
|
279
|
+
const response = await client.query({
|
|
280
|
+
query: gql `
|
|
281
|
+
query GetKpiYValueComprehensiveStats {
|
|
282
|
+
totalKpiYValueComprehensiveStats {
|
|
283
|
+
kpiName
|
|
284
|
+
minVal
|
|
285
|
+
q1Val
|
|
286
|
+
medVal
|
|
287
|
+
q3Val
|
|
288
|
+
maxVal
|
|
289
|
+
avgVal
|
|
290
|
+
projectCount
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
`
|
|
294
|
+
});
|
|
295
|
+
this.kpiYComprehensiveStats = response.data.totalKpiYValueComprehensiveStats || [];
|
|
296
|
+
console.log('KPI Y-Value Comprehensive Stats:', this.kpiYComprehensiveStats);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
console.error('Failed to fetch KPI Y comprehensive stats:', error);
|
|
300
|
+
this.kpiYComprehensiveStats = [];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
getYKpiBoxplotData() {
|
|
304
|
+
// KPI 카테고리 매핑 (Y-category KPI names to display categories)
|
|
305
|
+
const categoryMapping = {
|
|
306
|
+
Y01_productivity: '생산성과',
|
|
307
|
+
Y02_schedule: '일정성과',
|
|
308
|
+
Y03_cost: '비용성과',
|
|
309
|
+
Y04_quality: '품질성과',
|
|
310
|
+
Y05_safety: '안전성과',
|
|
311
|
+
Y06_environment: '환경성과'
|
|
312
|
+
};
|
|
313
|
+
const data = [];
|
|
314
|
+
// Y-KPI 카테고리별로 통합된 통계 생성
|
|
315
|
+
this.kpiYComprehensiveStats.forEach(stat => {
|
|
316
|
+
const category = categoryMapping[stat.kpiName] || stat.kpiName;
|
|
317
|
+
data.push({
|
|
318
|
+
org: category, // org 필드 사용 (boxplot 차트가 기대하는 필드명)
|
|
319
|
+
min: stat.minVal * 20,
|
|
320
|
+
max: stat.maxVal * 20,
|
|
321
|
+
q1: stat.q1Val * 20,
|
|
322
|
+
q3: stat.q3Val * 20,
|
|
323
|
+
median: stat.medVal * 20,
|
|
324
|
+
mean: stat.avgVal * 20,
|
|
325
|
+
value: this.getProjectYKpiValue(stat.kpiName) // 현재 프로젝트의 실제 Y-KPI 값
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
return data.sort((a, b) => a.org.localeCompare(b.org));
|
|
329
|
+
}
|
|
330
|
+
getProjectYKpiValue(kpiName) {
|
|
331
|
+
var _a, _b;
|
|
332
|
+
// 현재 프로젝트의 kpiValues 배열에서 해당 KPI 값을 찾습니다
|
|
333
|
+
const kpiValue = (_b = (_a = this.project) === null || _a === void 0 ? void 0 : _a.kpiValues) === null || _b === void 0 ? void 0 : _b.find(kv => kv.kpiName === kpiName);
|
|
334
|
+
return (kpiValue === null || kpiValue === void 0 ? void 0 : kpiValue.value) || 0;
|
|
335
|
+
}
|
|
336
|
+
getRadarChartData() {
|
|
337
|
+
// KPI 카테고리 매핑 (Y-category KPI names to display categories)
|
|
338
|
+
const categoryMapping = {
|
|
339
|
+
Y01_productivity: '생산성',
|
|
340
|
+
Y02_schedule: '일정',
|
|
341
|
+
Y03_cost: '비용',
|
|
342
|
+
Y04_quality: '품질',
|
|
343
|
+
Y05_safety: '안전',
|
|
344
|
+
Y06_environment: '환경'
|
|
345
|
+
};
|
|
346
|
+
const data = [];
|
|
347
|
+
const categories = [];
|
|
348
|
+
// Create baseline data from comprehensive stats (using avgVal as baseline)
|
|
349
|
+
this.kpiYComprehensiveStats.forEach(stat => {
|
|
350
|
+
const category = categoryMapping[stat.kpiName] || stat.kpiName;
|
|
351
|
+
if (!categories.includes(category)) {
|
|
352
|
+
categories.push(category);
|
|
353
|
+
}
|
|
354
|
+
// Add baseline average
|
|
355
|
+
data.push({
|
|
356
|
+
org: '기준평균',
|
|
357
|
+
category,
|
|
358
|
+
value: Math.round(stat.avgVal * 20) // Scale to match expected range
|
|
359
|
+
});
|
|
360
|
+
// Add current project performance (using project's individual values)
|
|
361
|
+
// For now, using avgVal + some variation as placeholder
|
|
362
|
+
data.push({
|
|
363
|
+
org: '프로젝트성과',
|
|
364
|
+
category,
|
|
365
|
+
value: this.getProjectYKpiValue(stat.kpiName)
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
return {
|
|
369
|
+
data,
|
|
370
|
+
categories: categories.length > 0
|
|
371
|
+
? categories
|
|
372
|
+
: ['Y1. 일정성과', 'Y2. 비용성과', 'Y3. 품질성과', 'Y4. 안전성과', 'Y5. 환경성과', 'Y6. 생산성성과']
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
getBoxPlotDataForProject(project) {
|
|
376
|
+
var _a;
|
|
377
|
+
// 프로젝트의 지역(geo_group)에 해당하는 통계 찾기
|
|
378
|
+
const stats = (_a = this.kpiComprehensiveStats) === null || _a === void 0 ? void 0 : _a[0];
|
|
379
|
+
if (!stats) {
|
|
380
|
+
// 전체 통계의 평균값 사용 또는 기본값 반환
|
|
381
|
+
const defaultStats = this.kpiComprehensiveStats.length > 0 ? this.kpiComprehensiveStats[0] : null;
|
|
382
|
+
if (defaultStats) {
|
|
383
|
+
return {
|
|
384
|
+
min: defaultStats.minVal,
|
|
385
|
+
max: defaultStats.maxVal,
|
|
386
|
+
mean: (defaultStats.q1Val + defaultStats.q3Val) / 2,
|
|
387
|
+
median: defaultStats.medVal,
|
|
388
|
+
q1: defaultStats.q1Val,
|
|
389
|
+
q3: defaultStats.q3Val,
|
|
390
|
+
value: project.kpi || 0
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// 완전한 기본값
|
|
394
|
+
return {
|
|
395
|
+
min: 0,
|
|
396
|
+
max: 100,
|
|
397
|
+
mean: 50,
|
|
398
|
+
median: 50,
|
|
399
|
+
q1: 25,
|
|
400
|
+
q3: 75,
|
|
401
|
+
value: project.kpi || 0
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
min: stats.minVal * 20,
|
|
406
|
+
max: stats.maxVal * 20,
|
|
407
|
+
mean: ((stats.q1Val + stats.q3Val) / 2) * 20, // 대략적 평균값
|
|
408
|
+
median: stats.medVal * 20,
|
|
409
|
+
q1: stats.q1Val * 20,
|
|
410
|
+
q3: stats.q3Val * 20,
|
|
411
|
+
value: project.kpi || 0
|
|
412
|
+
};
|
|
277
413
|
}
|
|
278
414
|
};
|
|
279
415
|
SvProjectDetailPage.styles = [
|
|
@@ -363,7 +499,7 @@ SvProjectDetailPage.styles = [
|
|
|
363
499
|
letter-spacing: -0.05em;
|
|
364
500
|
}
|
|
365
501
|
.value {
|
|
366
|
-
color: #
|
|
502
|
+
color: #333;
|
|
367
503
|
font-size: 16px;
|
|
368
504
|
}
|
|
369
505
|
.status-value {
|
|
@@ -483,6 +619,7 @@ SvProjectDetailPage.styles = [
|
|
|
483
619
|
align-items: center;
|
|
484
620
|
padding: 0 30px;
|
|
485
621
|
max-height: 360px;
|
|
622
|
+
gap: 20px;
|
|
486
623
|
}
|
|
487
624
|
.mini-boxplot {
|
|
488
625
|
flex: 1;
|
|
@@ -538,7 +675,7 @@ SvProjectDetailPage.styles = [
|
|
|
538
675
|
margin-top: 4px;
|
|
539
676
|
}
|
|
540
677
|
.group-chart {
|
|
541
|
-
height:
|
|
678
|
+
height: 300px;
|
|
542
679
|
margin-top: 25px;
|
|
543
680
|
}
|
|
544
681
|
|
|
@@ -551,6 +688,14 @@ __decorate([
|
|
|
551
688
|
state(),
|
|
552
689
|
__metadata("design:type", Object)
|
|
553
690
|
], SvProjectDetailPage.prototype, "project", void 0);
|
|
691
|
+
__decorate([
|
|
692
|
+
state(),
|
|
693
|
+
__metadata("design:type", Array)
|
|
694
|
+
], SvProjectDetailPage.prototype, "kpiComprehensiveStats", void 0);
|
|
695
|
+
__decorate([
|
|
696
|
+
state(),
|
|
697
|
+
__metadata("design:type", Array)
|
|
698
|
+
], SvProjectDetailPage.prototype, "kpiYComprehensiveStats", void 0);
|
|
554
699
|
SvProjectDetailPage = __decorate([
|
|
555
700
|
customElement('sv-project-detail')
|
|
556
701
|
], SvProjectDetailPage);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sv-project-detail.js","sourceRoot":"","sources":["../../client/pages/sv-project-detail.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAiB,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAW,aAAa,EAAgB,MAAM,mBAAmB,CAAA;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,iCAAiC,CAAA;AACxC,OAAO,wCAAwC,CAAA;AAC/C,OAAO,+BAA+B,CAAA;AAG/B,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,mBAAmB,CAAC,QAAQ,CAAC;IAA/D;;QAuRY,YAAO,GAAY,EAAa,CAAA;IAmQnD,CAAC;IAzQC,IAAI,OAAO;QACT,OAAO;YACL,KAAK,EAAE,YAAY;SACpB,CAAA;IACH,CAAC;IAID,MAAM;;QACJ,OAAO,IAAI,CAAA;;;;;qBAKM,IAAI,CAAC,OAAO,CAAC,IAAI;;;;;6BAKT,CAAA,MAAA,IAAI,CAAC,OAAO,CAAC,SAAS,0CAAE,QAAQ,KAAI,6BAA6B;;yCAErD,MAAA,IAAI,CAAC,OAAO,CAAC,eAAe,0CAAE,OAAO;;;uCAGvC,IAAI,CAAC,OAAO,CAAC,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO;;;;oDAInC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;qCAgBhD,CAAA,MAAA,MAAA,IAAI,CAAC,OAAO,CAAC,eAAe,0CAAE,gBAAgB,0CAAE,cAAc,EAAE,KAAI,GAAG;;;;qCAIvE,CAAA,MAAA,MAAA,IAAI,CAAC,OAAO,CAAC,eAAe,0CAAE,IAAI,0CAAE,cAAc,EAAE,KAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAkDpF,IAAI,CAAA;;8CAE8B,GAAG,EAAE,CAAC,QAAQ,CAAC,kBAAkB,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;;;;8CAInD,GAAG,EAAE,CAAC,QAAQ,CAAC,oBAAoB,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;;;;;WAKxF;;;;;;;;;;;;;;;4BAeiB;YACN,GAAG,EAAE,CAAC;YACN,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,EAAE;YACR,MAAM,EAAE,EAAE;YACV,EAAE,EAAE,EAAE;YACN,EAAE,EAAE,EAAE;YACN,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;SACxB;;;;;uCAKkB,MAAA,MAAA,IAAI,CAAC,OAAO,CAAC,GAAG,0CAAE,OAAO,CAAC,CAAC,CAAC,mCAAI,GAAG;;;;;;;0BAOhD;YACN;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,EAAE;aACV;YACD;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,EAAE;aACV;YACD;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,EAAE;aACV;YACD;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,EAAE;aACV;YACD;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,EAAE;aACV;YACD;gBACE,KAAK,EAAE,IAAI;gBACX,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,EAAE;aACV;SACF;gCACa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;kCACrD,IAAI;;;;;;;;;;;;;;;;;;;;KAoBjC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,SAAwB;QACtD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,YAAoB,EAAE;;QACtC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDT;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;SAC7B,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM;YAAE,OAAM;QAE3B,IAAI,CAAC,OAAO,GAAG,CAAA,MAAA,QAAQ,CAAC,IAAI,0CAAE,OAAO,KAAK,EAAc,CAAA;IAC1D,CAAC;;AAxhBM,0BAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4QF;CACF,AA9QY,CA8QZ;AAQgB;IAAhB,KAAK,EAAE;;oDAAyC;AAvRtC,mBAAmB;IAD/B,aAAa,CAAC,mBAAmB,CAAC;GACtB,mBAAmB,CA0hB/B","sourcesContent":["import { navigate, PageLifecycle, PageView } from '@operato/shell'\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\nimport { ScopedElementsMixin } from '@open-wc/scoped-elements'\nimport { Project, PROJECT_STATE, ProjectState } from './sv-project-list'\nimport { client } from '@operato/graphql'\nimport gql from 'graphql-tag'\nimport '../components/kpi-boxplot-chart'\nimport '../components/kpi-single-boxplot-chart'\nimport '../components/kpi-radar-chart'\n\n@customElement('sv-project-detail')\nexport class SvProjectDetailPage extends ScopedElementsMixin(PageView) {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n\n width: 100%;\n height: 100%;\n background-color: var(--md-sys-color-background, #f6f6f6);\n }\n\n /* content layout */\n div[content] {\n display: flex;\n gap: 35px;\n padding: 25px;\n }\n div[left] {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n }\n div[right] {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 0 1px;\n }\n\n /* card */\n .card {\n background: #ffffff;\n border-radius: 10px;\n padding: 15px;\n box-shadow: 3px 3px 3px 0 rgba(0, 0, 0, 0.1);\n }\n .card-title {\n display: flex;\n align-items: center;\n gap: 5px;\n color: #35618e;\n font-weight: 700;\n }\n .triangle {\n width: 0;\n height: 0;\n border-left: 6px solid transparent;\n border-right: 6px solid transparent;\n border-top: 8px solid #35618e;\n }\n\n /* left top info box */\n .bold {\n font-weight: bold;\n }\n .top-info {\n display: flex;\n gap: 10px;\n }\n .top-info img[pic] {\n width: 284px;\n height: 160px;\n object-fit: fill;\n background: #e5e7eb;\n }\n .top-info .info {\n display: flex;\n flex-direction: column;\n gap: 15px;\n padding: 10px 0;\n flex: 1;\n }\n .row {\n display: flex;\n align-items: center;\n gap: 10px;\n }\n .label {\n min-width: fit-content;\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n }\n .value {\n color: #212529;\n font-size: 16px;\n }\n .status-value {\n color: #4cbb49;\n font-weight: 700;\n }\n\n .address {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n }\n\n .detail-grid {\n display: grid;\n grid-template-columns: 284px 1fr;\n gap: 15px 10px;\n padding: 5px 0;\n }\n .detail-grid .grid-item .label {\n width: auto;\n }\n\n /* left second card (requests) */\n .sub-desc {\n color: #35618e;\n font-size: 14px;\n letter-spacing: -0.05em;\n margin-bottom: 8px;\n }\n .list-block {\n display: flex;\n flex-direction: column;\n gap: 5px;\n padding: 5px 25px;\n }\n .list-title {\n color: #212529;\n font-weight: 700;\n font-size: 16px;\n letter-spacing: -0.05em;\n }\n .list-text {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n margin-left: 8px;\n }\n .list-text .warn {\n margin-left: 10px;\n color: #e13232;\n\n md-icon {\n font-size: 17px;\n width: 13px;\n height: 13px;\n }\n }\n\n /* right buttons */\n .button-line {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n }\n .ghost-btn {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 5px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.1);\n cursor: pointer;\n\n /* Material Symbols Outlined (stroke style) */\n md-icon {\n font-variation-settings:\n 'FILL' 0,\n 'wght' 300,\n 'GRAD' 0,\n 'opsz' 24;\n }\n }\n\n /* right KPI box */\n .kpi-box {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 15px;\n max-height: 420px;\n min-width: 560px;\n }\n .kpi-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n color: #35618e;\n font-weight: 700;\n font-size: 20px;\n letter-spacing: -0.05em;\n }\n .kpi-header div {\n display: flex;\n align-items: center;\n gap: 5px;\n }\n .kpi-desc {\n color: #35618e;\n font-size: 13px;\n letter-spacing: -0.05em;\n }\n .kpi-summary {\n display: flex;\n align-items: center;\n padding: 0 30px;\n max-height: 360px;\n }\n .mini-boxplot {\n flex: 1;\n display: flex;\n flex-direction: column;\n height: 100%;\n position: relative;\n min-width: 120px;\n }\n .boxplot-area {\n flex: 1;\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n background: #ffffff;\n }\n\n .boxplot-point {\n display: flex;\n flex-direction: column;\n align-items: center;\n color: #35618e;\n }\n .boxplot-point .value {\n font-size: 42px;\n font-weight: 800;\n color: #35618e;\n border-radius: 6px;\n }\n .boxplot-point .label {\n font-size: 14px;\n font-weight: 300;\n width: auto;\n }\n\n /* spider chart placeholder */\n .spider-area {\n flex: 5;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 10px;\n height: 100%;\n }\n\n /* group distribution */\n .group-box .desc {\n color: #35618e;\n font-size: 13px;\n letter-spacing: -0.05em;\n margin-top: 4px;\n }\n .group-chart {\n height: 205px;\n margin-top: 25px;\n }\n\n kpi-single-boxplot-chart {\n flex: 1;\n }\n `\n ]\n\n get context() {\n return {\n title: '프로젝트 상세 정보'\n }\n }\n\n @state() private project: Project = {} as Project\n\n render() {\n return html`\n <div content>\n <div left>\n <div class=\"card\">\n <div class=\"card-title\">\n <div>${this.project.name}</div>\n <div class=\"triangle\"></div>\n </div>\n\n <div class=\"top-info\" style=\"margin-top: 10px;\">\n <img pic src=${this.project.mainPhoto?.fullpath || '/assets/images/no-image.png'} alt=\"project\" />\n <div class=\"info\">\n <div class=\"address\">• ${this.project.buildingComplex?.address}</div>\n <div class=\"row\">\n <div class=\"label\">• 공사기간</div>\n <div class=\"value\">${this.project.startDate} ~ ${this.project.endDate}</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 현장상태</div>\n <div class=\"value status-value\">${PROJECT_STATE[this.project.state]}</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 요청상세</div>\n <div class=\"value bold\">???</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 알람</div>\n <div class=\"value\">???</div>\n </div>\n </div>\n </div>\n\n <div class=\"detail-grid\">\n <div class=\"grid-item row\">\n <div class=\"label\">• 공사비</div>\n <div class=\"value\">${this.project.buildingComplex?.constructionCost?.toLocaleString() || '-'} 원</div>\n </div>\n <div class=\"grid-item row\">\n <div class=\"label\">• 연면적</div>\n <div class=\"value\">${this.project.buildingComplex?.area?.toLocaleString() || '-'} ㎡</div>\n </div>\n <div class=\"grid-item row\">\n <div class=\"label\">• 용적률</div>\n <div class=\"value\">??? %</div>\n </div>\n <div class=\"grid-item row\">\n <div class=\"label\">• 투입인력</div>\n <div class=\"value\">??? 명</div>\n </div>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-title\">\n <div>입력 대기 중</div>\n <div class=\"triangle\"></div>\n </div>\n <div class=\"sub-desc\">\n 현재 입력 기한이 도래했거나, 입력이 지연되고 있는 KPI 항목입니다. 해당 항목에 대한 데이터를 입력 해 주시기 바랍니다.\n </div>\n\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 대기 중 2건</div>\n <div class=\"list-text\"># 2033312-Y5.1 : 검수자재 불합격률</div>\n <div class=\"list-text\"># 2033312-Y5.1 : 품질시험 불합격 건수</div>\n </div>\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 기한 초과 1건</div>\n <div class=\"list-text\">\n # 2033312-Y5.2 : 검측 불합격률 <span class=\"warn\">2025.2.12 <md-icon>report</md-icon></span>\n </div>\n </div>\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 예정 4건</div>\n <div class=\"list-text\"># 2033320-Y4.32 : 연면적 대비 공기입력</div>\n <div class=\"list-text\"># 2012345-Y4.32 : 책임감리원의 일정성과 수준 평가</div>\n <div class=\"list-text\"># 2035641-Y5.22 : 설계 변경에 따른 공기 증감률</div>\n <div class=\"list-text\"># 2096412-Y5.31 : 현장별 일정 이탈 수준</div>\n </div>\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 기한 없음 3건</div>\n <div class=\"list-text\"># 2033320-Y4.32 : 연면적 대비 공기입력</div>\n <div class=\"list-text\"># 2012345-Y4.32 : 책임감리원의 일정성과 수준 평가</div>\n <div class=\"list-text\"># 2035641-Y5.22 : 설계 변경에 따른 공기 증감률</div>\n </div>\n </div>\n </div>\n\n <div right>\n ${html`\n <div class=\"button-line\">\n <div class=\"ghost-btn\" @click=${() => navigate(`project-update/${this.project.id}`)}>\n <md-icon slot=\"\">assignment</md-icon>\n <div>프로젝트 수정</div>\n </div>\n <div class=\"ghost-btn\" @click=${() => navigate(`project-complete/${this.project.id}`)}>\n <md-icon slot=\"\">assignment_turned_in</md-icon>\n <div>프로젝트 완공 처리</div>\n </div>\n </div>\n `}\n\n <div class=\"card kpi-box\">\n <div class=\"kpi-header\">\n <div>\n <div>종합 KPI</div>\n <div class=\"triangle\"></div>\n </div>\n <div class=\"kpi-desc\">프로젝트의 6개 성과를 종합한 종합 KPI포인트와 각 지표별 분포</div>\n </div>\n\n <div class=\"kpi-summary\">\n <div class=\"mini-boxplot\" title=\"boxplot placeholder\">\n <div class=\"boxplot-area\">\n <kpi-single-boxplot-chart\n .data=${{\n min: 0,\n max: 100,\n mean: 50,\n median: 60,\n q1: 25,\n q3: 75,\n value: this.project.kpi\n }}\n ></kpi-single-boxplot-chart>\n </div>\n\n <div class=\"boxplot-point\">\n <div class=\"value\">${this.project.kpi?.toFixed(1) ?? '-'}</div>\n <div class=\"label\">Point</div>\n </div>\n </div>\n\n <div class=\"spider-area\">\n <kpi-radar-chart\n .data=${[\n {\n group: '평균',\n category: '생산성 성과',\n value: 41\n },\n {\n group: '평균',\n category: '일정 성과',\n value: 26\n },\n {\n group: '평균',\n category: '비용 성과',\n value: 30\n },\n {\n group: '평균',\n category: '품질 성과',\n value: 21\n },\n {\n group: '평균',\n category: '안전 성과',\n value: 33\n },\n {\n group: '평균',\n category: '환경 성과',\n value: 15\n }\n ]}\n .categories=${['생산성 성과', '일정 성과', '비용 성과', '품질 성과', '안전 성과', '환경 성과']}\n .currentGroup=${'평균'}\n ></kpi-radar-chart>\n </div>\n </div>\n </div>\n\n <div class=\"card group-box\">\n <div class=\"kpi-header\">\n <div>그룹별 분포</div>\n <div class=\"triangle\"></div>\n </div>\n <div class=\"desc\">\n Boxplot(박스플롯)은 각 카테고리별로 값의 분포(최소, 1사분위, 중앙값, 3사분위, 최대, 평균, 이상치 등)를 보여줍니다.\n 박스는 중앙 50% 구간, 수염은 전체 범위, 굵은 검정색 가로선은 중앙값(메디안), 초록색 원은 평균값(Mean)을 의미합니다.\n 이 프로젝트의 값은 진한 오렌지색 원으로 별도 강조되어 표시되며, 중앙값/평균과 다를 수 있습니다.\n </div>\n <div class=\"group-chart\"></div>\n </div>\n </div>\n </div>\n `\n }\n\n async pageUpdated(changes: any, lifecycle: PageLifecycle) {\n if (this.active) {\n await this.initProject(lifecycle.resourceId)\n }\n }\n\n async initProject(projectId: string = '') {\n const response = await client.query({\n query: gql`\n query Project($id: String!) {\n project(id: $id) {\n id\n name\n state\n startDate\n endDate\n projectType\n mainPhoto {\n fullpath\n }\n totalProgress\n weeklyProgress\n kpi\n inspPassRate\n robotProgressRate\n structuralSafetyRate\n robotCount\n buildingComplex {\n id\n address\n latitude\n longitude\n area\n clientCompany\n constructionCompany\n supervisoryCompany\n designCompany\n drawing {\n id\n name\n fullpath\n }\n constructionType\n constructionCost\n etc\n notice\n householdCount\n buildingCount\n virtualTourLink\n buildings {\n id\n name\n floorCount\n }\n }\n }\n }\n `,\n variables: { id: projectId }\n })\n\n if (response.errors) return\n\n this.project = response.data?.project || ({} as Project)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sv-project-detail.js","sourceRoot":"","sources":["../../client/pages/sv-project-detail.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAiB,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAW,aAAa,EAAgB,MAAM,mBAAmB,CAAA;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,iCAAiC,CAAA;AACxC,OAAO,wCAAwC,CAAA;AAC/C,OAAO,+BAA+B,CAAA;AAG/B,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,mBAAmB,CAAC,QAAQ,CAAC;IAA/D;;QAwRY,YAAO,GAAY,EAAa,CAAA;QAChC,0BAAqB,GAAU,EAAE,CAAA;QACjC,2BAAsB,GAAU,EAAE,CAAA;IAmarD,CAAC;IA3aC,IAAI,OAAO;;QACT,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,EAAE,CAAC,CAAC,CAAC,YAAY;SAC1E,CAAA;IACH,CAAC;IAMD,MAAM;;QACJ,OAAO,IAAI,CAAA;;;;;qBAKM,IAAI,CAAC,OAAO,CAAC,IAAI;;;;;6BAKT,CAAA,MAAA,IAAI,CAAC,OAAO,CAAC,SAAS,0CAAE,QAAQ,KAAI,kCAAkC;;;;uCAI5D,MAAA,IAAI,CAAC,OAAO,CAAC,eAAe,0CAAE,OAAO;;;;uCAIrC,IAAI,CAAC,OAAO,CAAC,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO;;;;oDAInC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;qCAgBhD,CAAA,MAAA,MAAA,IAAI,CAAC,OAAO,CAAC,eAAe,0CAAE,gBAAgB,0CAAE,cAAc,EAAE,KAAI,GAAG;;;;qCAIvE,CAAA,MAAA,MAAA,IAAI,CAAC,OAAO,CAAC,eAAe,0CAAE,IAAI,0CAAE,cAAc,EAAE,KAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAkDpF,IAAI,CAAA;;8CAE8B,GAAG,EAAE,CAAC,QAAQ,CAAC,kBAAkB,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;;;;8CAInD,GAAG,EAAE,CAAC,QAAQ,CAAC,oBAAoB,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;;;;;WAKxF;;;;;;;;;;;;;;oDAcyC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC;;;;uCAIxD,MAAA,MAAA,IAAI,CAAC,OAAO,CAAC,GAAG,0CAAE,OAAO,CAAC,CAAC,CAAC,mCAAI,GAAG;;;;;;;0BAOhD,IAAI,CAAC,iBAAiB,EAAE,CAAC,IAAI;gCACvB,IAAI,CAAC,iBAAiB,EAAE,CAAC,UAAU;kCACjC,QAAQ;;;;;;;;;;;;;;;;;;;yCAmBD,IAAI,CAAC,kBAAkB,EAAE;;;;;KAK7D,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,SAAwB;QACtD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;YACtC,IAAI,CAAC,wBAAwB,EAAE,CAAA;YAC/B,IAAI,CAAC,yBAAyB,EAAE,CAAA;QAClC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,YAAoB,EAAE;;QACtC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqDT;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;SAC7B,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM;YAAE,OAAM;QAE3B,IAAI,CAAC,OAAO,GAAG,CAAA,MAAA,QAAQ,CAAC,IAAI,0CAAE,OAAO,KAAK,EAAc,CAAA;QAExD,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED,KAAK,CAAC,wBAAwB;QAC5B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;SAUT;aACF,CAAC,CAAA;YAEF,IAAI,CAAC,qBAAqB,GAAG,QAAQ,CAAC,IAAI,CAAC,gCAAgC,IAAI,EAAE,CAAA;YACjF,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAA;QAC7E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,0CAA0C,EAAE,KAAK,CAAC,CAAA;YAChE,IAAI,CAAC,qBAAqB,GAAG,EAAE,CAAA;QACjC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;SAaT;aACF,CAAC,CAAA;YAEF,IAAI,CAAC,sBAAsB,GAAG,QAAQ,CAAC,IAAI,CAAC,gCAAgC,IAAI,EAAE,CAAA;YAClF,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAA;QAC9E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,4CAA4C,EAAE,KAAK,CAAC,CAAA;YAClE,IAAI,CAAC,sBAAsB,GAAG,EAAE,CAAA;QAClC,CAAC;IACH,CAAC;IAEO,kBAAkB;QACxB,2DAA2D;QAC3D,MAAM,eAAe,GAA2B;YAC9C,gBAAgB,EAAE,MAAM;YACxB,YAAY,EAAE,MAAM;YACpB,QAAQ,EAAE,MAAM;YAChB,WAAW,EAAE,MAAM;YACnB,UAAU,EAAE,MAAM;YAClB,eAAe,EAAE,MAAM;SACxB,CAAA;QAED,MAAM,IAAI,GASL,EAAE,CAAA;QAEP,yBAAyB;QACzB,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACzC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAA;YAE9D,IAAI,CAAC,IAAI,CAAC;gBACR,GAAG,EAAE,QAAQ,EAAE,mCAAmC;gBAClD,GAAG,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE;gBACrB,GAAG,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE;gBACrB,EAAE,EAAE,IAAI,CAAC,KAAK,GAAG,EAAE;gBACnB,EAAE,EAAE,IAAI,CAAC,KAAK,GAAG,EAAE;gBACnB,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE;gBACxB,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE;gBACtB,KAAK,EAAE,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,sBAAsB;aACrE,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACxD,CAAC;IAEO,mBAAmB,CAAC,OAAe;;QACzC,yCAAyC;QACzC,MAAM,QAAQ,GAAG,MAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,SAAS,0CAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,KAAK,OAAO,CAAC,CAAA;QAC5E,OAAO,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,KAAK,KAAI,CAAC,CAAA;IAC7B,CAAC;IAEO,iBAAiB;QACvB,2DAA2D;QAC3D,MAAM,eAAe,GAA2B;YAC9C,gBAAgB,EAAE,KAAK;YACvB,YAAY,EAAE,IAAI;YAClB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,IAAI;YACjB,UAAU,EAAE,IAAI;YAChB,eAAe,EAAE,IAAI;SACtB,CAAA;QAED,MAAM,IAAI,GAA4D,EAAE,CAAA;QACxE,MAAM,UAAU,GAAa,EAAE,CAAA;QAE/B,2EAA2E;QAC3E,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACzC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAA;YAC9D,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACnC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YAC3B,CAAC;YAED,uBAAuB;YACvB,IAAI,CAAC,IAAI,CAAC;gBACR,GAAG,EAAE,MAAM;gBACX,QAAQ;gBACR,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,gCAAgC;aACrE,CAAC,CAAA;YAEF,sEAAsE;YACtE,wDAAwD;YACxD,IAAI,CAAC,IAAI,CAAC;gBACR,GAAG,EAAE,QAAQ;gBACb,QAAQ;gBACR,KAAK,EAAE,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC;aAC9C,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,OAAO;YACL,IAAI;YACJ,UAAU,EACR,UAAU,CAAC,MAAM,GAAG,CAAC;gBACnB,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,CAAC;SAChF,CAAA;IACH,CAAC;IAEO,wBAAwB,CAAC,OAAgB;;QAC/C,kCAAkC;QAClC,MAAM,KAAK,GAAG,MAAA,IAAI,CAAC,qBAAqB,0CAAG,CAAC,CAAC,CAAA;QAE7C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,0BAA0B;YAC1B,MAAM,YAAY,GAAG,IAAI,CAAC,qBAAqB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;YACjG,IAAI,YAAY,EAAE,CAAC;gBACjB,OAAO;oBACL,GAAG,EAAE,YAAY,CAAC,MAAM;oBACxB,GAAG,EAAE,YAAY,CAAC,MAAM;oBACxB,IAAI,EAAE,CAAC,YAAY,CAAC,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;oBACnD,MAAM,EAAE,YAAY,CAAC,MAAM;oBAC3B,EAAE,EAAE,YAAY,CAAC,KAAK;oBACtB,EAAE,EAAE,YAAY,CAAC,KAAK;oBACtB,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;iBACxB,CAAA;YACH,CAAC;YAED,UAAU;YACV,OAAO;gBACL,GAAG,EAAE,CAAC;gBACN,GAAG,EAAE,GAAG;gBACR,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,EAAE;gBACV,EAAE,EAAE,EAAE;gBACN,EAAE,EAAE,EAAE;gBACN,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;aACxB,CAAA;QACH,CAAC;QAED,OAAO;YACL,GAAG,EAAE,KAAK,CAAC,MAAM,GAAG,EAAE;YACtB,GAAG,EAAE,KAAK,CAAC,MAAM,GAAG,EAAE;YACtB,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,UAAU;YACxD,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,EAAE;YACzB,EAAE,EAAE,KAAK,CAAC,KAAK,GAAG,EAAE;YACpB,EAAE,EAAE,KAAK,CAAC,KAAK,GAAG,EAAE;YACpB,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;SACxB,CAAA;IACH,CAAC;;AA3rBM,0BAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA6QF;CACF,AA/QY,CA+QZ;AAQgB;IAAhB,KAAK,EAAE;;oDAAyC;AAChC;IAAhB,KAAK,EAAE;;kEAA0C;AACjC;IAAhB,KAAK,EAAE;;mEAA2C;AA1RxC,mBAAmB;IAD/B,aAAa,CAAC,mBAAmB,CAAC;GACtB,mBAAmB,CA6rB/B","sourcesContent":["import { navigate, PageLifecycle, PageView } from '@operato/shell'\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\nimport { ScopedElementsMixin } from '@open-wc/scoped-elements'\nimport { Project, PROJECT_STATE, ProjectState } from './sv-project-list'\nimport { client } from '@operato/graphql'\nimport gql from 'graphql-tag'\nimport '../components/kpi-boxplot-chart'\nimport '../components/kpi-single-boxplot-chart'\nimport '../components/kpi-radar-chart'\n\n@customElement('sv-project-detail')\nexport class SvProjectDetailPage extends ScopedElementsMixin(PageView) {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n\n width: 100%;\n height: 100%;\n background-color: var(--md-sys-color-background, #f6f6f6);\n }\n\n /* content layout */\n div[content] {\n display: flex;\n gap: 35px;\n padding: 25px;\n }\n div[left] {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n }\n div[right] {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 0 1px;\n }\n\n /* card */\n .card {\n background: #ffffff;\n border-radius: 10px;\n padding: 15px;\n box-shadow: 3px 3px 3px 0 rgba(0, 0, 0, 0.1);\n }\n .card-title {\n display: flex;\n align-items: center;\n gap: 5px;\n color: #35618e;\n font-weight: 700;\n }\n .triangle {\n width: 0;\n height: 0;\n border-left: 6px solid transparent;\n border-right: 6px solid transparent;\n border-top: 8px solid #35618e;\n }\n\n /* left top info box */\n .bold {\n font-weight: bold;\n }\n .top-info {\n display: flex;\n gap: 10px;\n }\n .top-info img[pic] {\n width: 284px;\n height: 160px;\n object-fit: fill;\n background: #e5e7eb;\n }\n .top-info .info {\n display: flex;\n flex-direction: column;\n gap: 15px;\n padding: 10px 0;\n flex: 1;\n }\n .row {\n display: flex;\n align-items: center;\n gap: 10px;\n }\n .label {\n min-width: fit-content;\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n }\n .value {\n color: #333;\n font-size: 16px;\n }\n .status-value {\n color: #4cbb49;\n font-weight: 700;\n }\n\n .address {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n }\n\n .detail-grid {\n display: grid;\n grid-template-columns: 284px 1fr;\n gap: 15px 10px;\n padding: 5px 0;\n }\n .detail-grid .grid-item .label {\n width: auto;\n }\n\n /* left second card (requests) */\n .sub-desc {\n color: #35618e;\n font-size: 14px;\n letter-spacing: -0.05em;\n margin-bottom: 8px;\n }\n .list-block {\n display: flex;\n flex-direction: column;\n gap: 5px;\n padding: 5px 25px;\n }\n .list-title {\n color: #212529;\n font-weight: 700;\n font-size: 16px;\n letter-spacing: -0.05em;\n }\n .list-text {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n margin-left: 8px;\n }\n .list-text .warn {\n margin-left: 10px;\n color: #e13232;\n\n md-icon {\n font-size: 17px;\n width: 13px;\n height: 13px;\n }\n }\n\n /* right buttons */\n .button-line {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n }\n .ghost-btn {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 5px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.1);\n cursor: pointer;\n\n /* Material Symbols Outlined (stroke style) */\n md-icon {\n font-variation-settings:\n 'FILL' 0,\n 'wght' 300,\n 'GRAD' 0,\n 'opsz' 24;\n }\n }\n\n /* right KPI box */\n .kpi-box {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 15px;\n max-height: 420px;\n min-width: 560px;\n }\n .kpi-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n color: #35618e;\n font-weight: 700;\n font-size: 20px;\n letter-spacing: -0.05em;\n }\n .kpi-header div {\n display: flex;\n align-items: center;\n gap: 5px;\n }\n .kpi-desc {\n color: #35618e;\n font-size: 13px;\n letter-spacing: -0.05em;\n }\n .kpi-summary {\n display: flex;\n align-items: center;\n padding: 0 30px;\n max-height: 360px;\n gap: 20px;\n }\n .mini-boxplot {\n flex: 1;\n display: flex;\n flex-direction: column;\n height: 100%;\n position: relative;\n min-width: 120px;\n }\n .boxplot-area {\n flex: 1;\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n background: #ffffff;\n }\n\n .boxplot-point {\n display: flex;\n flex-direction: column;\n align-items: center;\n color: #35618e;\n }\n .boxplot-point .value {\n font-size: 42px;\n font-weight: 800;\n color: #35618e;\n border-radius: 6px;\n }\n .boxplot-point .label {\n font-size: 14px;\n font-weight: 300;\n width: auto;\n }\n\n /* spider chart placeholder */\n .spider-area {\n flex: 5;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 10px;\n height: 100%;\n }\n\n /* group distribution */\n .group-box .desc {\n color: #35618e;\n font-size: 13px;\n letter-spacing: -0.05em;\n margin-top: 4px;\n }\n .group-chart {\n height: 300px;\n margin-top: 25px;\n }\n\n kpi-single-boxplot-chart {\n flex: 1;\n }\n `\n ]\n\n get context() {\n return {\n title: this.project ? `프로젝트 상세 정보 - ${this.project?.name}` : '프로젝트 상세 정보'\n }\n }\n\n @state() private project: Project = {} as Project\n @state() private kpiComprehensiveStats: any[] = []\n @state() private kpiYComprehensiveStats: any[] = []\n\n render() {\n return html`\n <div content>\n <div left>\n <div class=\"card\">\n <div class=\"card-title\">\n <div>${this.project.name}</div>\n <div class=\"triangle\"></div>\n </div>\n\n <div class=\"top-info\" style=\"margin-top: 10px;\">\n <img pic src=${this.project.mainPhoto?.fullpath || '/assets/images/project-image.png'} alt=\"project\" />\n <div class=\"info\">\n <div class=\"row\">\n <div class=\"label\">• 주소</div>\n <div class=\"value\">${this.project.buildingComplex?.address}</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 공사기간</div>\n <div class=\"value\">${this.project.startDate} ~ ${this.project.endDate}</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 현장상태</div>\n <div class=\"value status-value\">${PROJECT_STATE[this.project.state]}</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 요청상세</div>\n <div class=\"value bold\">000</div>\n </div>\n <div class=\"row\">\n <div class=\"label\">• 알람</div>\n <div class=\"value\">000</div>\n </div>\n </div>\n </div>\n\n <div class=\"detail-grid\">\n <div class=\"grid-item row\">\n <div class=\"label\">• 공사비</div>\n <div class=\"value\">${this.project.buildingComplex?.constructionCost?.toLocaleString() || '-'} 원</div>\n </div>\n <div class=\"grid-item row\">\n <div class=\"label\">• 연면적</div>\n <div class=\"value\">${this.project.buildingComplex?.area?.toLocaleString() || '-'} ㎡</div>\n </div>\n <div class=\"grid-item row\">\n <div class=\"label\">• 용적률</div>\n <div class=\"value\">000 %</div>\n </div>\n <div class=\"grid-item row\">\n <div class=\"label\">• 투입인력</div>\n <div class=\"value\">000 명</div>\n </div>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-title\">\n <div>입력 대기 중</div>\n <div class=\"triangle\"></div>\n </div>\n <div class=\"sub-desc\">\n 현재 입력 기한이 도래했거나, 입력이 지연되고 있는 KPI 항목입니다. 해당 항목에 대한 데이터를 입력 해 주시기 바랍니다.\n </div>\n\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 대기 중 2건</div>\n <div class=\"list-text\"># 2033312-Y5.1 : 검수자재 불합격률</div>\n <div class=\"list-text\"># 2033312-Y5.1 : 품질시험 불합격 건수</div>\n </div>\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 기한 초과 1건</div>\n <div class=\"list-text\">\n # 2033312-Y5.2 : 검측 불합격률 <span class=\"warn\">2025.2.12 <md-icon>report</md-icon></span>\n </div>\n </div>\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 예정 4건</div>\n <div class=\"list-text\"># 2033320-Y4.32 : 연면적 대비 공기입력</div>\n <div class=\"list-text\"># 2012345-Y4.32 : 책임감리원의 일정성과 수준 평가</div>\n <div class=\"list-text\"># 2035641-Y5.22 : 설계 변경에 따른 공기 증감률</div>\n <div class=\"list-text\"># 2096412-Y5.31 : 현장별 일정 이탈 수준</div>\n </div>\n <div class=\"list-block\">\n <div class=\"list-title\">• 입력 기한 없음 3건</div>\n <div class=\"list-text\"># 2033320-Y4.32 : 연면적 대비 공기입력</div>\n <div class=\"list-text\"># 2012345-Y4.32 : 책임감리원의 일정성과 수준 평가</div>\n <div class=\"list-text\"># 2035641-Y5.22 : 설계 변경에 따른 공기 증감률</div>\n </div>\n </div>\n </div>\n\n <div right>\n ${html`\n <div class=\"button-line\">\n <div class=\"ghost-btn\" @click=${() => navigate(`project-update/${this.project.id}`)}>\n <md-icon slot=\"\">assignment</md-icon>\n <div>프로젝트 수정</div>\n </div>\n <div class=\"ghost-btn\" @click=${() => navigate(`project-complete/${this.project.id}`)}>\n <md-icon slot=\"\">assignment_turned_in</md-icon>\n <div>프로젝트 완공 처리</div>\n </div>\n </div>\n `}\n\n <div class=\"card kpi-box\">\n <div class=\"kpi-header\">\n <div>\n <div>종합 KPI</div>\n <div class=\"triangle\"></div>\n </div>\n <div class=\"kpi-desc\">프로젝트의 6개 성과를 종합한 종합 KPI포인트와 각 지표별 분포</div>\n </div>\n\n <div class=\"kpi-summary\">\n <div class=\"mini-boxplot\" title=\"boxplot placeholder\">\n <div class=\"boxplot-area\">\n <kpi-single-boxplot-chart .data=${this.getBoxPlotDataForProject(this.project)}></kpi-single-boxplot-chart>\n </div>\n\n <div class=\"boxplot-point\">\n <div class=\"value\">${this.project.kpi?.toFixed(1) ?? '-'}</div>\n <div class=\"label\">Point</div>\n </div>\n </div>\n\n <div class=\"spider-area\">\n <kpi-radar-chart\n .data=${this.getRadarChartData().data}\n .categories=${this.getRadarChartData().categories}\n .currentGroup=${'프로젝트성과'}\n ></kpi-radar-chart>\n </div>\n </div>\n </div>\n\n <div class=\"card group-box\">\n <div class=\"kpi-header\">\n <div>\n <div>그룹별 분포</div>\n <div class=\"triangle\"></div>\n </div>\n </div>\n <div class=\"desc\">\n Boxplot(박스플롯)은 각 카테고리별로 값의 분포(최소, 1사분위, 중앙값, 3사분위, 최대, 평균, 이상치 등)를 보여줍니다.\n 박스는 중앙 50% 구간, 수염은 전체 범위, 굵은 검정색 가로선은 중앙값(메디안), 초록색 원은 평균값(Mean)을 의미합니다.\n 이 프로젝트의 값은 진한 오렌지색 원으로 별도 강조되어 표시되며, 중앙값/평균과 다를 수 있습니다.\n </div>\n <div class=\"group-chart\">\n <kpi-boxplot-chart .data=${this.getYKpiBoxplotData()}></kpi-boxplot-chart>\n </div>\n </div>\n </div>\n </div>\n `\n }\n\n async pageUpdated(changes: any, lifecycle: PageLifecycle) {\n if (this.active) {\n this.initProject(lifecycle.resourceId)\n this.getKpiComprehensiveStats()\n this.getKpiYComprehensiveStats()\n }\n }\n\n async initProject(projectId: string = '') {\n const response = await client.query({\n query: gql`\n query Project($id: String!) {\n project(id: $id) {\n id\n name\n state\n startDate\n endDate\n projectType\n mainPhoto {\n fullpath\n }\n totalProgress\n weeklyProgress\n kpi\n kpiValues {\n kpiName\n value\n }\n inspPassRate\n robotProgressRate\n structuralSafetyRate\n robotCount\n buildingComplex {\n id\n address\n latitude\n longitude\n area\n clientCompany\n constructionCompany\n supervisoryCompany\n designCompany\n drawing {\n id\n name\n fullpath\n }\n constructionType\n constructionCost\n etc\n notice\n householdCount\n buildingCount\n virtualTourLink\n buildings {\n id\n name\n floorCount\n }\n }\n }\n }\n `,\n variables: { id: projectId }\n })\n\n if (response.errors) return\n\n this.project = response.data?.project || ({} as Project)\n\n this.updateContext()\n }\n\n async getKpiComprehensiveStats() {\n try {\n const response = await client.query({\n query: gql`\n query GetKpiZValueComprehensiveStats {\n totalKpiZValueComprehensiveStats {\n minVal\n q1Val\n medVal\n q3Val\n maxVal\n }\n }\n `\n })\n\n this.kpiComprehensiveStats = response.data.totalKpiZValueComprehensiveStats || []\n console.log('KPI Z-Value Comprehensive Stats:', this.kpiComprehensiveStats)\n } catch (error) {\n console.error('Failed to fetch KPI comprehensive stats:', error)\n this.kpiComprehensiveStats = []\n }\n }\n\n async getKpiYComprehensiveStats() {\n try {\n const response = await client.query({\n query: gql`\n query GetKpiYValueComprehensiveStats {\n totalKpiYValueComprehensiveStats {\n kpiName\n minVal\n q1Val\n medVal\n q3Val\n maxVal\n avgVal\n projectCount\n }\n }\n `\n })\n\n this.kpiYComprehensiveStats = response.data.totalKpiYValueComprehensiveStats || []\n console.log('KPI Y-Value Comprehensive Stats:', this.kpiYComprehensiveStats)\n } catch (error) {\n console.error('Failed to fetch KPI Y comprehensive stats:', error)\n this.kpiYComprehensiveStats = []\n }\n }\n\n private getYKpiBoxplotData() {\n // KPI 카테고리 매핑 (Y-category KPI names to display categories)\n const categoryMapping: Record<string, string> = {\n Y01_productivity: '생산성과',\n Y02_schedule: '일정성과',\n Y03_cost: '비용성과',\n Y04_quality: '품질성과',\n Y05_safety: '안전성과',\n Y06_environment: '환경성과'\n }\n\n const data: Array<{\n org: string\n min: number\n max: number\n q1: number\n q3: number\n median: number\n mean: number\n value: number\n }> = []\n\n // Y-KPI 카테고리별로 통합된 통계 생성\n this.kpiYComprehensiveStats.forEach(stat => {\n const category = categoryMapping[stat.kpiName] || stat.kpiName\n\n data.push({\n org: category, // org 필드 사용 (boxplot 차트가 기대하는 필드명)\n min: stat.minVal * 20,\n max: stat.maxVal * 20,\n q1: stat.q1Val * 20,\n q3: stat.q3Val * 20,\n median: stat.medVal * 20,\n mean: stat.avgVal * 20,\n value: this.getProjectYKpiValue(stat.kpiName) // 현재 프로젝트의 실제 Y-KPI 값\n })\n })\n\n return data.sort((a, b) => a.org.localeCompare(b.org))\n }\n\n private getProjectYKpiValue(kpiName: string): number {\n // 현재 프로젝트의 kpiValues 배열에서 해당 KPI 값을 찾습니다\n const kpiValue = this.project?.kpiValues?.find(kv => kv.kpiName === kpiName)\n return kpiValue?.value || 0\n }\n\n private getRadarChartData() {\n // KPI 카테고리 매핑 (Y-category KPI names to display categories)\n const categoryMapping: Record<string, string> = {\n Y01_productivity: '생산성',\n Y02_schedule: '일정',\n Y03_cost: '비용',\n Y04_quality: '품질',\n Y05_safety: '안전',\n Y06_environment: '환경'\n }\n\n const data: Array<{ org: string; category: string; value: number }> = []\n const categories: string[] = []\n\n // Create baseline data from comprehensive stats (using avgVal as baseline)\n this.kpiYComprehensiveStats.forEach(stat => {\n const category = categoryMapping[stat.kpiName] || stat.kpiName\n if (!categories.includes(category)) {\n categories.push(category)\n }\n\n // Add baseline average\n data.push({\n org: '기준평균',\n category,\n value: Math.round(stat.avgVal * 20) // Scale to match expected range\n })\n\n // Add current project performance (using project's individual values)\n // For now, using avgVal + some variation as placeholder\n data.push({\n org: '프로젝트성과',\n category,\n value: this.getProjectYKpiValue(stat.kpiName)\n })\n })\n\n return {\n data,\n categories:\n categories.length > 0\n ? categories\n : ['Y1. 일정성과', 'Y2. 비용성과', 'Y3. 품질성과', 'Y4. 안전성과', 'Y5. 환경성과', 'Y6. 생산성성과']\n }\n }\n\n private getBoxPlotDataForProject(project: Project) {\n // 프로젝트의 지역(geo_group)에 해당하는 통계 찾기\n const stats = this.kpiComprehensiveStats?.[0]\n\n if (!stats) {\n // 전체 통계의 평균값 사용 또는 기본값 반환\n const defaultStats = this.kpiComprehensiveStats.length > 0 ? this.kpiComprehensiveStats[0] : null\n if (defaultStats) {\n return {\n min: defaultStats.minVal,\n max: defaultStats.maxVal,\n mean: (defaultStats.q1Val + defaultStats.q3Val) / 2,\n median: defaultStats.medVal,\n q1: defaultStats.q1Val,\n q3: defaultStats.q3Val,\n value: project.kpi || 0\n }\n }\n\n // 완전한 기본값\n return {\n min: 0,\n max: 100,\n mean: 50,\n median: 50,\n q1: 25,\n q3: 75,\n value: project.kpi || 0\n }\n }\n\n return {\n min: stats.minVal * 20,\n max: stats.maxVal * 20,\n mean: ((stats.q1Val + stats.q3Val) / 2) * 20, // 대략적 평균값\n median: stats.medVal * 20,\n q1: stats.q1Val * 20,\n q3: stats.q3Val * 20,\n value: project.kpi || 0\n }\n }\n}\n"]}
|
|
@@ -34,6 +34,10 @@ export declare enum ProjectType {
|
|
|
34
34
|
DCSP = "DCSP",
|
|
35
35
|
DKPI = "DKPI"
|
|
36
36
|
}
|
|
37
|
+
export interface KpiValuesObject {
|
|
38
|
+
kpiName: string;
|
|
39
|
+
value: number;
|
|
40
|
+
}
|
|
37
41
|
export interface Project {
|
|
38
42
|
id?: string;
|
|
39
43
|
name: string;
|
|
@@ -45,6 +49,7 @@ export interface Project {
|
|
|
45
49
|
totalProgress?: number;
|
|
46
50
|
weeklyProgress?: number;
|
|
47
51
|
kpi?: number;
|
|
52
|
+
kpiValues?: KpiValuesObject[];
|
|
48
53
|
inspPassRate?: number;
|
|
49
54
|
robotProgressRate?: number;
|
|
50
55
|
structuralSafetyRate?: number;
|
|
@@ -144,7 +149,7 @@ export declare class SvProjectListPage extends SvProjectListPage_base {
|
|
|
144
149
|
private projectList;
|
|
145
150
|
private projectCount;
|
|
146
151
|
private currentPage;
|
|
147
|
-
private
|
|
152
|
+
private kpiComprehensiveStats;
|
|
148
153
|
private readonly pageLimit;
|
|
149
154
|
get totalPages(): number;
|
|
150
155
|
render(): import("lit-html").TemplateResult<1>;
|
|
@@ -153,7 +158,7 @@ export declare class SvProjectListPage extends SvProjectListPage_base {
|
|
|
153
158
|
private _onInputChange;
|
|
154
159
|
private _onKeypress;
|
|
155
160
|
private _changePage;
|
|
156
|
-
|
|
161
|
+
getKpiComprehensiveStats(): Promise<void>;
|
|
157
162
|
private _openCreateProjectPopup;
|
|
158
163
|
private getBoxPlotDataForProject;
|
|
159
164
|
}
|
|
@@ -49,7 +49,7 @@ let SvProjectListPage = class SvProjectListPage extends ScopedElementsMixin(Page
|
|
|
49
49
|
this.projectList = [];
|
|
50
50
|
this.projectCount = 0;
|
|
51
51
|
this.currentPage = 1;
|
|
52
|
-
this.
|
|
52
|
+
this.kpiComprehensiveStats = [];
|
|
53
53
|
this.pageLimit = 20;
|
|
54
54
|
}
|
|
55
55
|
get context() {
|
|
@@ -103,7 +103,7 @@ let SvProjectListPage = class SvProjectListPage extends ScopedElementsMixin(Page
|
|
|
103
103
|
return html `
|
|
104
104
|
<div tr @click=${() => navigate(`project-detail/${project.id}`)}>
|
|
105
105
|
<div td-pics>
|
|
106
|
-
<img pic src=${((_g = project.mainPhoto) === null || _g === void 0 ? void 0 : _g.fullpath) || '/assets/images/
|
|
106
|
+
<img pic src=${((_g = project.mainPhoto) === null || _g === void 0 ? void 0 : _g.fullpath) || '/assets/images/project-image.png'} alt="${project.name}" />
|
|
107
107
|
</div>
|
|
108
108
|
<div td-status>${PROJECT_STATE[project.state]}</div>
|
|
109
109
|
<div td-info>
|
|
@@ -133,7 +133,7 @@ let SvProjectListPage = class SvProjectListPage extends ScopedElementsMixin(Page
|
|
|
133
133
|
async pageUpdated(changes, lifecycle) {
|
|
134
134
|
if (this.active) {
|
|
135
135
|
this.getProjectList();
|
|
136
|
-
this.
|
|
136
|
+
this.getKpiComprehensiveStats();
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
async getProjectList() {
|
|
@@ -203,12 +203,12 @@ let SvProjectListPage = class SvProjectListPage extends ScopedElementsMixin(Page
|
|
|
203
203
|
this.getProjectList();
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
async
|
|
206
|
+
async getKpiComprehensiveStats() {
|
|
207
207
|
try {
|
|
208
208
|
const response = await client.query({
|
|
209
209
|
query: gql `
|
|
210
|
-
query
|
|
211
|
-
|
|
210
|
+
query GetKpiZValueComprehensiveStats {
|
|
211
|
+
totalKpiZValueComprehensiveStats {
|
|
212
212
|
minVal
|
|
213
213
|
q1Val
|
|
214
214
|
medVal
|
|
@@ -218,12 +218,12 @@ let SvProjectListPage = class SvProjectListPage extends ScopedElementsMixin(Page
|
|
|
218
218
|
}
|
|
219
219
|
`
|
|
220
220
|
});
|
|
221
|
-
this.
|
|
222
|
-
console.log('KPI Z-Value
|
|
221
|
+
this.kpiComprehensiveStats = response.data.totalKpiZValueComprehensiveStats || [];
|
|
222
|
+
console.log('KPI Z-Value Comprehensive Stats:', this.kpiComprehensiveStats);
|
|
223
223
|
}
|
|
224
224
|
catch (error) {
|
|
225
|
-
console.error('Failed to fetch KPI
|
|
226
|
-
this.
|
|
225
|
+
console.error('Failed to fetch KPI comprehensive stats:', error);
|
|
226
|
+
this.kpiComprehensiveStats = [];
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
_openCreateProjectPopup() {
|
|
@@ -236,10 +236,10 @@ let SvProjectListPage = class SvProjectListPage extends ScopedElementsMixin(Page
|
|
|
236
236
|
getBoxPlotDataForProject(project) {
|
|
237
237
|
var _a;
|
|
238
238
|
// 프로젝트의 지역(geo_group)에 해당하는 통계 찾기
|
|
239
|
-
const stats = (_a = this.
|
|
239
|
+
const stats = (_a = this.kpiComprehensiveStats) === null || _a === void 0 ? void 0 : _a[0];
|
|
240
240
|
if (!stats) {
|
|
241
241
|
// 전체 통계의 평균값 사용 또는 기본값 반환
|
|
242
|
-
const defaultStats = this.
|
|
242
|
+
const defaultStats = this.kpiComprehensiveStats.length > 0 ? this.kpiComprehensiveStats[0] : null;
|
|
243
243
|
if (defaultStats) {
|
|
244
244
|
return {
|
|
245
245
|
min: defaultStats.minVal,
|
|
@@ -481,7 +481,7 @@ __decorate([
|
|
|
481
481
|
__decorate([
|
|
482
482
|
state(),
|
|
483
483
|
__metadata("design:type", Array)
|
|
484
|
-
], SvProjectListPage.prototype, "
|
|
484
|
+
], SvProjectListPage.prototype, "kpiComprehensiveStats", void 0);
|
|
485
485
|
SvProjectListPage = __decorate([
|
|
486
486
|
customElement('sv-project-list')
|
|
487
487
|
], SvProjectListPage);
|