@dssp/dkpi 1.0.32 → 1.0.33

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.
@@ -4,6 +4,7 @@ import { customElement, property, state } from 'lit/decorators.js';
4
4
  import { getProject, updateProjectCompleteStep3, getKpiMetrics, getKpiMetricValues } from '../../shared/complete-api';
5
5
  import { fetchSentimentGrades, matchSentimentBand, sentimentBandsOf, SENTIMENT_METRICS } from '../../shared/sentiment-index';
6
6
  import { notify } from '@operato/layout';
7
+ import { ProjectState } from '../sv-project-list';
7
8
  import { hasPrivilege } from '@things-factory/auth-base/dist-client';
8
9
  let SvProjectCompleteTab3Upload = class SvProjectCompleteTab3Upload extends LitElement {
9
10
  constructor() {
@@ -15,6 +16,11 @@ let SvProjectCompleteTab3Upload = class SvProjectCompleteTab3Upload extends LitE
15
16
  /** kpi:sentiment — Step3 보고서 첨부/저장 권한 */
16
17
  this.canSave = false;
17
18
  }
19
+ /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 입력·저장은 하지 않는다. */
20
+ get isCompleted() {
21
+ var _a;
22
+ return ((_a = this.project) === null || _a === void 0 ? void 0 : _a.state) === ProjectState.COMPLETED;
23
+ }
18
24
  async connectedCallback() {
19
25
  super.connectedCallback();
20
26
  this.canSave = await hasPrivilege({
@@ -43,15 +49,17 @@ let SvProjectCompleteTab3Upload = class SvProjectCompleteTab3Upload extends LitE
43
49
  <div class="attachment-row">
44
50
  <md-icon class="file-icon">picture_as_pdf</md-icon>
45
51
  <div class="attachment-name" title="${(_a = this._getCurrentFile()) === null || _a === void 0 ? void 0 : _a.name}">${(_b = this._getCurrentFile()) === null || _b === void 0 ? void 0 : _b.name}</div>
46
- <md-icon class="delete-icon" @click=${this._removeFile}>delete</md-icon>
52
+ ${this.isCompleted ? '' : html `<md-icon class="delete-icon" @click=${this._removeFile}>delete</md-icon>`}
47
53
  </div>
48
54
  </div>
49
55
  `
50
- : html `
51
- <div class="upload-controls">
52
- <ox-input-file accept="application/pdf,.pdf" hide-filelist @change=${this._onFileSelect}> </ox-input-file>
53
- </div>
54
- `}
56
+ : this.isCompleted
57
+ ? html `<div class="empty-msg">업로드된 의견서가 없습니다.</div>`
58
+ : html `
59
+ <div class="upload-controls">
60
+ <ox-input-file accept="application/pdf,.pdf" hide-filelist @change=${this._onFileSelect}> </ox-input-file>
61
+ </div>
62
+ `}
55
63
  ${this.slpaData.length > 0
56
64
  ? html `
57
65
  <div class="slpa-results">
@@ -88,16 +96,18 @@ let SvProjectCompleteTab3Upload = class SvProjectCompleteTab3Upload extends LitE
88
96
  `
89
97
  : ''}
90
98
 
91
- <div class="button-line">
92
- <div class="ghost-btn " @click=${this._reset}>초기화</div>
93
- <div
94
- class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
95
- title=${this.canSave ? '' : 'kpi:sentiment 권한 필요'}
96
- @click=${() => this.canSave && this._save()}
97
- >
98
- 저장
99
- </div>
100
- </div>
99
+ ${this.isCompleted
100
+ ? html `<div class="locked-note">🔒 완료된 프로젝트 — 입력된 값만 볼 수 있습니다.</div>`
101
+ : html `<div class="button-line">
102
+ <div class="ghost-btn " @click=${this._reset}>초기화</div>
103
+ <div
104
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
105
+ title=${this.canSave ? '' : 'kpi:sentiment 권한 필요'}
106
+ @click=${() => this.canSave && this._save()}
107
+ >
108
+ 저장
109
+ </div>
110
+ </div>`}
101
111
  </div>
102
112
  `;
103
113
  }
@@ -115,6 +125,8 @@ let SvProjectCompleteTab3Upload = class SvProjectCompleteTab3Upload extends LitE
115
125
  }
116
126
  async _save() {
117
127
  var _a;
128
+ if (this.isCompleted)
129
+ return;
118
130
  // 대기 중인 파일이 있으면 업로드
119
131
  if (this.pendingFile) {
120
132
  await this._uploadFile(this.pendingFile);
@@ -378,6 +390,20 @@ SvProjectCompleteTab3Upload.styles = [
378
390
  color: #1976d2;
379
391
  font-weight: 700;
380
392
  }
393
+
394
+ .locked-note {
395
+ margin-top: 16px;
396
+ text-align: right;
397
+ font-size: 13px;
398
+ color: #6c757d;
399
+ }
400
+
401
+ .empty-msg {
402
+ text-align: center;
403
+ color: #999;
404
+ padding: 40px 0;
405
+ font-size: 14px;
406
+ }
381
407
  `
382
408
  ];
383
409
  __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"pc-tab3-upload.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab3-upload.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAGlE,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AACrH,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AAG7D,IAAM,2BAA2B,GAAjC,MAAM,2BAA4B,SAAQ,UAAU;IAApD;;QA+LuB,YAAO,GAAQ,EAAE,CAAA;QACpC,eAAU,GAAQ,EAAE,CAAA;QACZ,gBAAW,GAAgB,IAAI,CAAA;QACvC,aAAQ,GAAU,EAAE,CAAA;QAC7B,yCAAyC;QAChC,YAAO,GAAG,KAAK,CAAA;IA4L1B,CAAC;IA1LC,KAAK,CAAC,iBAAiB;QACrB,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,CAAC,OAAO,GAAG,MAAM,YAAY,CAAC;YAChC,QAAQ,EAAE,KAAK;YACf,SAAS,EAAE,WAAW;YACtB,kBAAkB,EAAE,IAAI;YACxB,gBAAgB,EAAE,IAAI;SACvB,CAAC,CAAA;IACJ,CAAC;IAED,MAAM;;QACJ,OAAO,IAAI,CAAA;;;;;;;;;;;UAWL,IAAI,CAAC,eAAe,EAAE;YACtB,CAAC,CAAC,IAAI,CAAA;;;;wDAIwC,MAAA,IAAI,CAAC,eAAe,EAAE,0CAAE,IAAI,KAAK,MAAA,IAAI,CAAC,eAAe,EAAE,0CAAE,IAAI;wDAC7D,IAAI,CAAC,WAAW;;;aAG3D;YACH,CAAC,CAAC,IAAI,CAAA;;qFAEqE,IAAI,CAAC,aAAa;;aAE1F;UACH,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACxB,CAAC,CAAC,IAAI,CAAA;;;kBAGE,IAAI,CAAC,QAAQ,CAAC,GAAG,CACjB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAA;;gDAEkB,IAAI,CAAC,KAAK;;mDAEP,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,EAAE;0BACjE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,6BAA6B,IAAI,CAAC,OAAO,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE;;;sBAGrF,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBACrB,CAAC,CAAC,IAAI,CAAA;;;;;;8BAME,IAAI,CAAC,KAAK,CAAC,GAAG,CACd,CAAC,IAAS,EAAE,EAAE;;oBAAC,OAAA,IAAI,CAAA;4CACL,CAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,KAAK,MAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;wCACvD,IAAI,CAAC,KAAK;wCACV,IAAI,CAAC,MAAM;;+BAEpB,CAAA;iBAAA,CACF;;yBAEJ;gBACH,CAAC,CAAC,EAAE;mBACP,CACF;;aAEJ;YACH,CAAC,CAAC,EAAE;;;2CAG6B,IAAI,CAAC,MAAM;;yCAEb,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU;oBACnD,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qBAAqB;qBACxC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE;;;;;;KAMlD,CAAA;IACH,CAAC;IAED,UAAU,CAAC,iBAAmC;;QAC5C,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAA;QAEnC,yCAAyC;QACzC,IAAI,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,KAAI,MAAA,IAAI,CAAC,OAAO,0CAAE,EAAE,CAAA,EAAE,CAAC;YACzD,IAAI,CAAC,eAAe,EAAE,CAAA;QACxB,CAAC;IACH,CAAC;IAEO,eAAe;;QACrB,OAAO,IAAI,CAAC,WAAW,IAAI,CAAC,CAAA,MAAA,IAAI,CAAC,UAAU,0CAAE,EAAE,EAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC3E,CAAC;IAEO,KAAK,CAAC,KAAK;;QACjB,oBAAoB;QACpB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACxC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACzB,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,CAAA,MAAA,IAAI,CAAC,UAAU,0CAAE,EAAE,CAAA,EAAE,CAAC;YACrD,MAAM,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAA;QACnC,CAAC;IACH,CAAC;IAEO,WAAW;QACjB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,UAAU,GAAG,EAAE,CAAA;QACpB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAA,CAAC,iBAAiB;IACtC,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,CAAc;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,MAAgB,CAAA;QAChC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,IAAiB;;QACzC,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,CAAA;QAC5F,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACrB,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,8BAA8B,CAAA;YAC7D,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAA;YAC1B,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAA;YAE/B,kCAAkC;YAClC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAA;QAC5B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa;;QACzB,gBAAgB;QAChB,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,CAAA;QAE1D,yBAAyB;QACzB,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QAE9E,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,gDAAgD;YAChD,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBAClD,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC;gBACvD,oBAAoB,CAAC,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC;aACzC,CAAC,CAAA;YAEF,qCAAqC;YACrC,IAAI,CAAC,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;gBACvC,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,EAAE,CAAC,CAAA;gBACpF,MAAM,KAAK,GAAG,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,KAAK,KAAI,CAAC,CAAA;gBAErC,oDAAoD;gBACpD,iCAAiC;gBACjC,MAAM,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;gBAChD,MAAM,YAAY,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;gBAEtE,OAAO;oBACL,KAAK,EAAE,CAAA,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,KAAK,KAAI,MAAM,CAAC,IAAI;oBACtC,KAAK;oBACL,IAAI,EAAE,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,IAAI,KAAI,MAAM,CAAC,IAAI,IAAI,EAAE;oBAC5C,OAAO,EAAE,kBAAkB,CAAC,KAAK,EAAE,YAAY,CAAC;oBAChD,KAAK,EAAE,gBAAgB,CAAC,YAAY,CAAC;iBACtC,CAAA;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAA,CAAC,iBAAiB;QACpC,IAAI,CAAC,eAAe,EAAE,CAAA;IACxB,CAAC;IAEO,KAAK,CAAC,eAAe;;QAC3B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,cAAc,CAAA;QAExC,mCAAmC;QACnC,IAAI,MAAA,IAAI,CAAC,UAAU,0CAAE,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,aAAa,EAAE,CAAA;QAC5B,CAAC;IACH,CAAC;;AA9XM,kCAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0LF;CACF,AA5LY,CA4LZ;AAE2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;4DAAkB;AACpC;IAAR,KAAK,EAAE;;+DAAqB;AACZ;IAAhB,KAAK,EAAE;;gEAAwC;AACvC;IAAR,KAAK,EAAE;;6DAAqB;AAEpB;IAAR,KAAK,EAAE;;4DAAgB;AApMb,2BAA2B;IADvC,aAAa,CAAC,mBAAmB,CAAC;GACtB,2BAA2B,CAgYvC","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { gql } from '@apollo/client'\nimport { getProject, updateProjectCompleteStep3, getKpiMetrics, getKpiMetricValues } from '../../shared/complete-api'\nimport {\n fetchSentimentGrades,\n matchSentimentBand,\n sentimentBandsOf,\n SENTIMENT_METRICS\n} from '../../shared/sentiment-index'\nimport { notify } from '@operato/layout'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\n@customElement('sv-pc-tab3-upload')\nexport class SvProjectCompleteTab3Upload extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n .title {\n color: #212529;\n font-size: 13px;\n font-weight: 400;\n line-height: 24px;\n text-align: center;\n }\n\n .rows {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 8px 6px;\n }\n\n .upload-controls {\n display: flex;\n gap: 8px;\n justify-content: center;\n margin-bottom: 16px;\n\n ox-input-file {\n flex: 1;\n max-width: 500px;\n height: 210px;\n }\n }\n\n .attachment-list {\n display: flex;\n justify-content: center;\n margin-top: 8px;\n }\n\n .attachment-row {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n padding: 24px 20px;\n border: 2px solid #e3f2fd;\n border-radius: 12px;\n background: linear-gradient(135deg, #f8fbff 0%, #e3f2fd 100%);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n min-width: 300px;\n max-width: 400px;\n }\n\n .file-icon {\n font-size: 40px;\n color: #d32f2f;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n height: 48px;\n }\n\n .attachment-name {\n font-size: 16px;\n font-weight: 500;\n color: #1976d2;\n text-align: center;\n word-break: break-word;\n line-height: 1.4;\n }\n\n .delete-icon {\n cursor: pointer;\n color: #757575;\n background: rgba(117, 117, 117, 0.1);\n border-radius: 50%;\n padding: 8px;\n transition: all 0.2s ease;\n }\n\n .delete-icon:hover {\n color: #e57373;\n background: rgba(229, 115, 115, 0.15);\n transform: scale(1.1);\n }\n\n .button-line {\n display: flex;\n justify-content: center;\n gap: 10px;\n margin-top: 16px;\n }\n .ghost-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n cursor: pointer;\n }\n .ghost-btn.secondary {\n background: #24be7b;\n }\n .ghost-btn.disabled {\n opacity: 0.45;\n cursor: not-allowed;\n }\n\n .slpa-results {\n margin-top: 20px;\n padding: 16px;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n background: #f8f9fa;\n }\n\n .slpa-title {\n font-size: 16px;\n font-weight: 600;\n color: #35618e;\n margin-bottom: 12px;\n }\n\n .slpa-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 0;\n border-bottom: 1px solid #e0e0e0;\n }\n\n .slpa-item:last-child {\n border-bottom: none;\n }\n\n .slpa-label {\n font-weight: 500;\n color: #333;\n }\n\n .slpa-readout {\n display: flex;\n align-items: baseline;\n gap: 10px;\n }\n\n .slpa-value {\n font-weight: 600;\n color: #1976d2;\n }\n\n .slpa-result {\n font-weight: 700;\n color: #35618e;\n }\n\n /* 지수 → 5단계 환산 기준. 회의 자료의 \"지수 범위 / 결과\" 표를 그대로 옮긴 것. */\n .slpa-legend {\n margin: 6px 0 10px auto;\n border-collapse: collapse;\n font-size: 12px;\n color: #555;\n }\n\n .slpa-legend th,\n .slpa-legend td {\n padding: 3px 12px;\n border: 1px solid #e0e0e0;\n text-align: center;\n font-weight: 400;\n }\n\n .slpa-legend th {\n background: #eef2f7;\n color: #35618e;\n font-weight: 600;\n }\n\n .slpa-legend tr.current td {\n background: #e3f2fd;\n color: #1976d2;\n font-weight: 700;\n }\n `\n ]\n\n @property({ type: Object }) project: any = {}\n @state() attachment: any = {}\n @state() private pendingFile: File | null = null\n @state() slpaData: any[] = []\n /** kpi:sentiment — Step3 보고서 첨부/저장 권한 */\n @state() canSave = false\n\n async connectedCallback() {\n super.connectedCallback()\n this.canSave = await hasPrivilege({\n category: 'kpi',\n privilege: 'sentiment',\n domainOwnerGranted: true,\n superUserGranted: true\n })\n }\n\n render() {\n return html`\n <div class=\"title\">\n <div>\n 감리 최종 감리보고서의 종합의견서(PDF 형식)를 시스템에 업로드해 주세요.\n <br />\n 보고서에 포함된 텍스트와 평가 내용을 기반으로 기계 판독 및 AI 기반 프로젝트 평가가 자동으로 진행되므로, 반드시 최종\n 확정된 원본 파일을 제출해 주시기 바랍니다.\n </div>\n </div>\n\n <div class=\"rows\">\n ${this._getCurrentFile()\n ? html`\n <div class=\"attachment-list\">\n <div class=\"attachment-row\">\n <md-icon class=\"file-icon\">picture_as_pdf</md-icon>\n <div class=\"attachment-name\" title=\"${this._getCurrentFile()?.name}\">${this._getCurrentFile()?.name}</div>\n <md-icon class=\"delete-icon\" @click=${this._removeFile}>delete</md-icon>\n </div>\n </div>\n `\n : html`\n <div class=\"upload-controls\">\n <ox-input-file accept=\"application/pdf,.pdf\" hide-filelist @change=${this._onFileSelect}> </ox-input-file>\n </div>\n `}\n ${this.slpaData.length > 0\n ? html`\n <div class=\"slpa-results\">\n <div class=\"slpa-title\">AI 분석 결과 (SL-PA 관련 항목)</div>\n ${this.slpaData.map(\n item => html`\n <div class=\"slpa-item\">\n <div class=\"slpa-label\">${item.label}</div>\n <div class=\"slpa-readout\">\n <span class=\"slpa-value\">${item.value.toFixed(2)} ${item.unit || ''}</span>\n ${item.matched ? html`<span class=\"slpa-result\">${item.matched.result}</span>` : ''}\n </div>\n </div>\n ${item.bands.length > 0\n ? html`\n <table class=\"slpa-legend\">\n <tr>\n <th>지수 범위</th>\n <th>결과</th>\n </tr>\n ${item.bands.map(\n (band: any) => html`\n <tr class=${item.matched?.score === band.score ? 'current' : ''}>\n <td>${band.range}</td>\n <td>${band.result}</td>\n </tr>\n `\n )}\n </table>\n `\n : ''}\n `\n )}\n </div>\n `\n : ''}\n\n <div class=\"button-line\">\n <div class=\"ghost-btn \" @click=${this._reset}>초기화</div>\n <div\n class=\"ghost-btn secondary ${this.canSave ? '' : 'disabled'}\"\n title=${this.canSave ? '' : 'kpi:sentiment 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>\n </div>\n `\n }\n\n willUpdate(changedProperties: Map<string, any>) {\n super.willUpdate(changedProperties)\n\n // project가 변경되고, project.id가 존재하면 데이터 로드\n if (changedProperties.has('project') && this.project?.id) {\n this._getProjectData()\n }\n }\n\n private _getCurrentFile() {\n return this.pendingFile || (this.attachment?.id ? this.attachment : null)\n }\n\n private async _save() {\n // 대기 중인 파일이 있으면 업로드\n if (this.pendingFile) {\n await this._uploadFile(this.pendingFile)\n this.pendingFile = null\n } else if (!this.pendingFile && !this.attachment?.id) {\n notify({ message: '파일은 필수입니다.' })\n }\n }\n\n private _removeFile() {\n this.pendingFile = null\n this.attachment = {}\n this.slpaData = [] // SL-PA 데이터도 초기화\n }\n\n private async _onFileSelect(e: CustomEvent) {\n const files = e.detail as File[]\n if (files.length > 0) {\n this.pendingFile = files[0]\n }\n }\n\n private async _uploadFile(file: File | null) {\n const response = await updateProjectCompleteStep3(file, this.project.id, this.project?.code)\n if (!response.errors) {\n const uploaded = response.data.updateKpiMetricValuesSentiment\n this.attachment = uploaded\n notify({ message: '저장되었습니다.' })\n\n // PDF 업로드 후 SL-PA 관련 데이터를 가져와서 표시\n await this._loadSlpaData()\n }\n }\n\n private async _loadSlpaData() {\n // KPI 메트릭들 가져오기\n const kpiMetrics = await getKpiMetrics(this.project?.code)\n\n // \"SL-PA\"가 포함된 메트릭들만 필터링\n const slpaMetrics = kpiMetrics.filter(metric => metric.name.includes('SL-PA'))\n\n if (slpaMetrics.length > 0) {\n // 해당 프로젝트의 KPI 값들과, 지수 → 5단계 환산에 쓸 구간표를 함께 가져온다\n const [kpiMetricValues, grades] = await Promise.all([\n getKpiMetricValues(this.project.id, this.project?.code),\n fetchSentimentGrades(this.project?.code)\n ])\n\n // SL-PA 메트릭들과 해당 값들을 매칭하여 표시용 데이터 생성\n this.slpaData = slpaMetrics.map(metric => {\n const metricValue = kpiMetricValues.find((item: any) => item.metricId === metric.id)\n const value = metricValue?.value || 0\n\n // 체감지수로 정의된 metric 만 화면 이름과 등급을 갖는다. 그 밖의 SL-PA 항목은\n // 환산 기준이 없으므로 이름과 값 그대로 (기존 동작).\n const sentiment = SENTIMENT_METRICS[metric.name]\n const metricGrades = sentiment ? grades[sentiment.kpiCode] : undefined\n\n return {\n label: sentiment?.label || metric.name,\n value,\n unit: metricValue?.unit || metric.unit || '',\n matched: matchSentimentBand(value, metricGrades),\n bands: sentimentBandsOf(metricGrades)\n }\n })\n }\n }\n\n private _reset() {\n this.pendingFile = null\n this.slpaData = [] // SL-PA 데이터도 초기화\n this._getProjectData()\n }\n\n private async _getProjectData() {\n const project = await getProject(this.project.id)\n this.attachment = project.completeReport\n\n // 기존에 PDF가 업로드되어 있다면 SL-PA 데이터도 로드\n if (this.attachment?.id) {\n await this._loadSlpaData()\n }\n }\n}\n"]}
1
+ {"version":3,"file":"pc-tab3-upload.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab3-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { gql } from '@apollo/client'\nimport { getProject, updateProjectCompleteStep3, getKpiMetrics, getKpiMetricValues } from '../../shared/complete-api'\nimport {\n fetchSentimentGrades,\n matchSentimentBand,\n sentimentBandsOf,\n SENTIMENT_METRICS\n} from '../../shared/sentiment-index'\nimport { notify } from '@operato/layout'\nimport { ProjectState } from '../sv-project-list'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\n@customElement('sv-pc-tab3-upload')\nexport class SvProjectCompleteTab3Upload extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n .title {\n color: #212529;\n font-size: 13px;\n font-weight: 400;\n line-height: 24px;\n text-align: center;\n }\n\n .rows {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 8px 6px;\n }\n\n .upload-controls {\n display: flex;\n gap: 8px;\n justify-content: center;\n margin-bottom: 16px;\n\n ox-input-file {\n flex: 1;\n max-width: 500px;\n height: 210px;\n }\n }\n\n .attachment-list {\n display: flex;\n justify-content: center;\n margin-top: 8px;\n }\n\n .attachment-row {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n padding: 24px 20px;\n border: 2px solid #e3f2fd;\n border-radius: 12px;\n background: linear-gradient(135deg, #f8fbff 0%, #e3f2fd 100%);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n min-width: 300px;\n max-width: 400px;\n }\n\n .file-icon {\n font-size: 40px;\n color: #d32f2f;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n height: 48px;\n }\n\n .attachment-name {\n font-size: 16px;\n font-weight: 500;\n color: #1976d2;\n text-align: center;\n word-break: break-word;\n line-height: 1.4;\n }\n\n .delete-icon {\n cursor: pointer;\n color: #757575;\n background: rgba(117, 117, 117, 0.1);\n border-radius: 50%;\n padding: 8px;\n transition: all 0.2s ease;\n }\n\n .delete-icon:hover {\n color: #e57373;\n background: rgba(229, 115, 115, 0.15);\n transform: scale(1.1);\n }\n\n .button-line {\n display: flex;\n justify-content: center;\n gap: 10px;\n margin-top: 16px;\n }\n .ghost-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n cursor: pointer;\n }\n .ghost-btn.secondary {\n background: #24be7b;\n }\n .ghost-btn.disabled {\n opacity: 0.45;\n cursor: not-allowed;\n }\n\n .slpa-results {\n margin-top: 20px;\n padding: 16px;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n background: #f8f9fa;\n }\n\n .slpa-title {\n font-size: 16px;\n font-weight: 600;\n color: #35618e;\n margin-bottom: 12px;\n }\n\n .slpa-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 0;\n border-bottom: 1px solid #e0e0e0;\n }\n\n .slpa-item:last-child {\n border-bottom: none;\n }\n\n .slpa-label {\n font-weight: 500;\n color: #333;\n }\n\n .slpa-readout {\n display: flex;\n align-items: baseline;\n gap: 10px;\n }\n\n .slpa-value {\n font-weight: 600;\n color: #1976d2;\n }\n\n .slpa-result {\n font-weight: 700;\n color: #35618e;\n }\n\n /* 지수 → 5단계 환산 기준. 회의 자료의 \"지수 범위 / 결과\" 표를 그대로 옮긴 것. */\n .slpa-legend {\n margin: 6px 0 10px auto;\n border-collapse: collapse;\n font-size: 12px;\n color: #555;\n }\n\n .slpa-legend th,\n .slpa-legend td {\n padding: 3px 12px;\n border: 1px solid #e0e0e0;\n text-align: center;\n font-weight: 400;\n }\n\n .slpa-legend th {\n background: #eef2f7;\n color: #35618e;\n font-weight: 600;\n }\n\n .slpa-legend tr.current td {\n background: #e3f2fd;\n color: #1976d2;\n font-weight: 700;\n }\n\n .locked-note {\n margin-top: 16px;\n text-align: right;\n font-size: 13px;\n color: #6c757d;\n }\n\n .empty-msg {\n text-align: center;\n color: #999;\n padding: 40px 0;\n font-size: 14px;\n }\n `\n ]\n\n @property({ type: Object }) project: any = {}\n @state() attachment: any = {}\n @state() private pendingFile: File | null = null\n @state() slpaData: any[] = []\n /** kpi:sentiment — Step3 보고서 첨부/저장 권한 */\n @state() canSave = false\n\n /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 입력·저장은 하지 않는다. */\n private get isCompleted(): boolean {\n return this.project?.state === ProjectState.COMPLETED\n }\n\n async connectedCallback() {\n super.connectedCallback()\n this.canSave = await hasPrivilege({\n category: 'kpi',\n privilege: 'sentiment',\n domainOwnerGranted: true,\n superUserGranted: true\n })\n }\n\n render() {\n return html`\n <div class=\"title\">\n <div>\n 감리 최종 감리보고서의 종합의견서(PDF 형식)를 시스템에 업로드해 주세요.\n <br />\n 보고서에 포함된 텍스트와 평가 내용을 기반으로 기계 판독 및 AI 기반 프로젝트 평가가 자동으로 진행되므로, 반드시 최종\n 확정된 원본 파일을 제출해 주시기 바랍니다.\n </div>\n </div>\n\n <div class=\"rows\">\n ${this._getCurrentFile()\n ? html`\n <div class=\"attachment-list\">\n <div class=\"attachment-row\">\n <md-icon class=\"file-icon\">picture_as_pdf</md-icon>\n <div class=\"attachment-name\" title=\"${this._getCurrentFile()?.name}\">${this._getCurrentFile()?.name}</div>\n ${this.isCompleted ? '' : html`<md-icon class=\"delete-icon\" @click=${this._removeFile}>delete</md-icon>`}\n </div>\n </div>\n `\n : this.isCompleted\n ? html`<div class=\"empty-msg\">업로드된 의견서가 없습니다.</div>`\n : html`\n <div class=\"upload-controls\">\n <ox-input-file accept=\"application/pdf,.pdf\" hide-filelist @change=${this._onFileSelect}> </ox-input-file>\n </div>\n `}\n ${this.slpaData.length > 0\n ? html`\n <div class=\"slpa-results\">\n <div class=\"slpa-title\">AI 분석 결과 (SL-PA 관련 항목)</div>\n ${this.slpaData.map(\n item => html`\n <div class=\"slpa-item\">\n <div class=\"slpa-label\">${item.label}</div>\n <div class=\"slpa-readout\">\n <span class=\"slpa-value\">${item.value.toFixed(2)} ${item.unit || ''}</span>\n ${item.matched ? html`<span class=\"slpa-result\">${item.matched.result}</span>` : ''}\n </div>\n </div>\n ${item.bands.length > 0\n ? html`\n <table class=\"slpa-legend\">\n <tr>\n <th>지수 범위</th>\n <th>결과</th>\n </tr>\n ${item.bands.map(\n (band: any) => html`\n <tr class=${item.matched?.score === band.score ? 'current' : ''}>\n <td>${band.range}</td>\n <td>${band.result}</td>\n </tr>\n `\n )}\n </table>\n `\n : ''}\n `\n )}\n </div>\n `\n : ''}\n\n ${this.isCompleted\n ? html`<div class=\"locked-note\">🔒 완료된 프로젝트 — 입력된 값만 볼 수 있습니다.</div>`\n : html`<div class=\"button-line\">\n <div class=\"ghost-btn \" @click=${this._reset}>초기화</div>\n <div\n class=\"ghost-btn secondary ${this.canSave ? '' : 'disabled'}\"\n title=${this.canSave ? '' : 'kpi:sentiment 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>`}\n </div>\n `\n }\n\n willUpdate(changedProperties: Map<string, any>) {\n super.willUpdate(changedProperties)\n\n // project가 변경되고, project.id가 존재하면 데이터 로드\n if (changedProperties.has('project') && this.project?.id) {\n this._getProjectData()\n }\n }\n\n private _getCurrentFile() {\n return this.pendingFile || (this.attachment?.id ? this.attachment : null)\n }\n\n private async _save() {\n if (this.isCompleted) return\n // 대기 중인 파일이 있으면 업로드\n if (this.pendingFile) {\n await this._uploadFile(this.pendingFile)\n this.pendingFile = null\n } else if (!this.pendingFile && !this.attachment?.id) {\n notify({ message: '파일은 필수입니다.' })\n }\n }\n\n private _removeFile() {\n this.pendingFile = null\n this.attachment = {}\n this.slpaData = [] // SL-PA 데이터도 초기화\n }\n\n private async _onFileSelect(e: CustomEvent) {\n const files = e.detail as File[]\n if (files.length > 0) {\n this.pendingFile = files[0]\n }\n }\n\n private async _uploadFile(file: File | null) {\n const response = await updateProjectCompleteStep3(file, this.project.id, this.project?.code)\n if (!response.errors) {\n const uploaded = response.data.updateKpiMetricValuesSentiment\n this.attachment = uploaded\n notify({ message: '저장되었습니다.' })\n\n // PDF 업로드 후 SL-PA 관련 데이터를 가져와서 표시\n await this._loadSlpaData()\n }\n }\n\n private async _loadSlpaData() {\n // KPI 메트릭들 가져오기\n const kpiMetrics = await getKpiMetrics(this.project?.code)\n\n // \"SL-PA\"가 포함된 메트릭들만 필터링\n const slpaMetrics = kpiMetrics.filter(metric => metric.name.includes('SL-PA'))\n\n if (slpaMetrics.length > 0) {\n // 해당 프로젝트의 KPI 값들과, 지수 → 5단계 환산에 쓸 구간표를 함께 가져온다\n const [kpiMetricValues, grades] = await Promise.all([\n getKpiMetricValues(this.project.id, this.project?.code),\n fetchSentimentGrades(this.project?.code)\n ])\n\n // SL-PA 메트릭들과 해당 값들을 매칭하여 표시용 데이터 생성\n this.slpaData = slpaMetrics.map(metric => {\n const metricValue = kpiMetricValues.find((item: any) => item.metricId === metric.id)\n const value = metricValue?.value || 0\n\n // 체감지수로 정의된 metric 만 화면 이름과 등급을 갖는다. 그 밖의 SL-PA 항목은\n // 환산 기준이 없으므로 이름과 값 그대로 (기존 동작).\n const sentiment = SENTIMENT_METRICS[metric.name]\n const metricGrades = sentiment ? grades[sentiment.kpiCode] : undefined\n\n return {\n label: sentiment?.label || metric.name,\n value,\n unit: metricValue?.unit || metric.unit || '',\n matched: matchSentimentBand(value, metricGrades),\n bands: sentimentBandsOf(metricGrades)\n }\n })\n }\n }\n\n private _reset() {\n this.pendingFile = null\n this.slpaData = [] // SL-PA 데이터도 초기화\n this._getProjectData()\n }\n\n private async _getProjectData() {\n const project = await getProject(this.project.id)\n this.attachment = project.completeReport\n\n // 기존에 PDF가 업로드되어 있다면 SL-PA 데이터도 로드\n if (this.attachment?.id) {\n await this._loadSlpaData()\n }\n }\n}\n"]}
@@ -31,8 +31,9 @@ export declare class SvProjectCompleteTab4Monthly extends LitElement {
31
31
  private _isCellPending;
32
32
  /** metric 컬럼이 전 row 통틀어 한 번도 값이 없으면 헤더 강조. */
33
33
  private _isMetricNeverEntered;
34
- /** 종료(COMPLETED) 프로젝트 — 신규 월 추가 금지, 기존 월만 수정 가능. */
34
+ /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 추가·수정·삭제는 하지 않는다. */
35
35
  private get isCompleted();
36
+ private get editable();
36
37
  private _addMonth;
37
38
  private _removeMonth;
38
39
  /**
@@ -60,7 +60,7 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
60
60
 
61
61
  ${this.isCompleted
62
62
  ? html `<div class="toolbar">
63
- <span class="locked-note">🔒 종료된 프로젝트 — 월 추가 불가, 기존 월 데이터만 수정할 수 있습니다.</span>
63
+ <span class="locked-note">🔒 완료된 프로젝트 — 입력된 값만 볼 수 있습니다.</span>
64
64
  </div>`
65
65
  : html `<div class="toolbar">
66
66
  <select
@@ -83,7 +83,9 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
83
83
  월별 metric (periodType=MONTH) 이 등록되어 있지 않습니다. KPI 관리자에서 먼저 등록해주세요.
84
84
  </div>`
85
85
  : this.monthRows.length === 0
86
- ? html `<div class="empty-msg">등록된 월별 데이터가 없습니다. 위에서 월을 추가해주세요.</div>`
86
+ ? html `<div class="empty-msg">
87
+ ${this.isCompleted ? '입력된 월별 데이터가 없습니다.' : '등록된 월별 데이터가 없습니다. 위에서 월을 추가해주세요.'}
88
+ </div>`
87
89
  : html `
88
90
  <div class="grid-wrapper">
89
91
  <table>
@@ -115,7 +117,7 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
115
117
  <kpi-metric-input
116
118
  .metric=${this._withInputHint(m)}
117
119
  .value=${(_a = row.values[m.id]) !== null && _a !== void 0 ? _a : null}
118
- ?disabled=${!this.canSave}
120
+ ?disabled=${!this.editable}
119
121
  @metric-input-change=${(e) => this._setCell(rowIdx, m.id, e.detail.value)}
120
122
  ></kpi-metric-input>
121
123
  </td>
@@ -131,7 +133,9 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
131
133
  : html `<span class="status-saved">저장됨</span>`}
132
134
  </td>
133
135
  <td>
134
- <span class="delete-btn" title="삭제" @click=${() => this._removeMonth(rowIdx)}>✕</span>
136
+ ${this.isCompleted
137
+ ? ''
138
+ : html `<span class="delete-btn" title="삭제" @click=${() => this._removeMonth(rowIdx)}>✕</span>`}
135
139
  </td>
136
140
  </tr>
137
141
  `)}
@@ -140,16 +144,18 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
140
144
  </div>
141
145
  `}
142
146
 
143
- <div class="button-line">
144
- <div class="ghost-btn" @click=${this._reset}>초기화</div>
145
- <div
146
- class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
147
- title=${this.canSave ? '' : 'kpi:input 권한 필요'}
148
- @click=${() => this.canSave && this._save()}
149
- >
150
- 저장
151
- </div>
152
- </div>
147
+ ${this.isCompleted
148
+ ? ''
149
+ : html `<div class="button-line">
150
+ <div class="ghost-btn" @click=${this._reset}>초기화</div>
151
+ <div
152
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
153
+ title=${this.canSave ? '' : 'kpi:input 권한 필요'}
154
+ @click=${() => this.canSave && this._save()}
155
+ >
156
+ 저장
157
+ </div>
158
+ </div>`}
153
159
  `;
154
160
  }
155
161
  willUpdate(changedProperties) {
@@ -257,7 +263,8 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
257
263
  }
258
264
  // project.startDate ~ 현재월 사이 누락된 월 placeholder row 자동 추가.
259
265
  // 사용자가 값을 입력하지 않으면 dirty 가 안 되므로 저장 시 무시되어 spurious patch 없음.
260
- for (const ym of this._generateExpectedMonths()) {
266
+ // 완료된 프로젝트는 입력할 수 없으므로 빈 월을 만들지 않는다 — 입력된 월만 보인다.
267
+ for (const ym of this.isCompleted ? [] : this._generateExpectedMonths()) {
261
268
  if (!byMonth.has(ym)) {
262
269
  byMonth.set(ym, {
263
270
  workDate: ym,
@@ -313,17 +320,17 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
313
320
  return v !== undefined && v !== null && v !== '';
314
321
  });
315
322
  }
316
- /** 종료(COMPLETED) 프로젝트 — 신규 월 추가 금지, 기존 월만 수정 가능. */
323
+ /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 추가·수정·삭제는 하지 않는다. */
317
324
  get isCompleted() {
318
325
  var _a;
319
326
  return ((_a = this.project) === null || _a === void 0 ? void 0 : _a.state) === ProjectState.COMPLETED;
320
327
  }
328
+ get editable() {
329
+ return this.canSave && !this.isCompleted;
330
+ }
321
331
  _addMonth() {
322
- // 종료된 프로젝트는 새 월을 추가할 수 없다 (기존 월 데이터 수정만 허용).
323
- if (this.isCompleted) {
324
- notify({ message: '종료된 프로젝트는 월을 추가할 수 없습니다. 기존 월 데이터만 수정할 수 있습니다.' });
332
+ if (this.isCompleted)
325
333
  return;
326
- }
327
334
  const workDate = `${this.addYear}-${String(this.addMonth).padStart(2, '0')}`;
328
335
  // 현재월 row 는 추가 금지 — 운영 원칙상 이번달 데이터는 입력 대상 아님.
329
336
  const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM');
@@ -347,6 +354,8 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
347
354
  }
348
355
  async _removeMonth(rowIdx) {
349
356
  var _a;
357
+ if (this.isCompleted)
358
+ return;
350
359
  const row = this.monthRows[rowIdx];
351
360
  // 신규 row (DB 미반영) — 메모리에서만 제거
352
361
  if (row.isNew) {
@@ -420,6 +429,8 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
420
429
  */
421
430
  async _save() {
422
431
  var _a, _b;
432
+ if (this.isCompleted)
433
+ return;
423
434
  const patches = [];
424
435
  for (const row of this.monthRows) {
425
436
  if (!row.dirty)
@@ -1 +1 @@
1
- {"version":3,"file":"pc-tab4-monthly.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab4-monthly.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { gql } from '@apollo/client'\nimport { notify } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport moment from 'moment-timezone'\nimport { tenantHeaders } from '@dssp/project/dist-client/shared/domain-context'\nimport { ProjectState } from '../sv-project-list'\nimport { syncKpiToProjectInfo } from '../../shared/kpi-project-sync'\nimport '@things-factory/kpi/dist-client/viewparts/kpi-metric-input'\n\n/**\n * 프로젝트 완공 처리 탭 4 — 월별 KPI 데이터 그리드.\n *\n * 데이터 source = `KpiMetric` (periodType=MONTH) + `KpiMetricValue` (그 metric 의\n * 월별 값들). 이전 버전 (DataSet/DataSample) 폐기. KpiMetricValue 의 unique\n * 인덱스 (domain, metric, valueDate, org) 가 (도메인, 프로젝트, 메트릭, 월) 단일 row\n * 정책을 보장.\n *\n * 단방향 흐름:\n * - 탭1/탭2 의 metric 저장 (valueDate=YYYY-MM-01) → 그 월 row 가 여기 자동 노출\n * - 여기서 cell 수정 → 그 metric × 그 월 row 만 update (탭1/탭2 의 \"현재값\" 영향 없음 —\n * 탭1/탭2 는 KpiMetricValueProvider 가 latest 또는 valueDate-based lookup 으로 동작)\n */\n\n/** rating 셀렉트로 표시할 metric 인지 판단 — 이름에 '평가' 또는 '이탈' 포함되면 1~5 rating. */\nfunction isRating(metricName: string): boolean {\n return /평가|이탈/.test(metricName || '')\n}\n\n@customElement('sv-pc-tab4-monthly')\nexport class SvProjectCompleteTab4Monthly extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n .title {\n color: #212529;\n font-size: 13px;\n font-weight: 400;\n line-height: 24px;\n text-align: center;\n margin-bottom: 8px;\n }\n\n .toolbar {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n margin-bottom: 12px;\n }\n .toolbar select {\n padding: 5px 8px;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 5px;\n font-size: 14px;\n color: #212529;\n }\n .toolbar .add-btn {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 5px 10px;\n background: #35618e;\n color: #fff;\n border-radius: 5px;\n cursor: pointer;\n font-size: 13px;\n }\n .toolbar .locked-note {\n font-size: 13px;\n color: #6c757d;\n }\n\n .grid-wrapper {\n overflow-x: auto;\n padding: 0 6px;\n }\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n thead th {\n background: #f3f3fa;\n border-top: 2px solid #0c4da2;\n color: #212529;\n text-align: center;\n padding: 8px 6px;\n white-space: nowrap;\n font-weight: 500;\n }\n tbody td {\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n text-align: center;\n padding: 6px 4px;\n vertical-align: middle;\n }\n tbody tr:hover {\n background: #f8fafc;\n }\n .month-cell {\n color: #35618e;\n font-weight: 600;\n white-space: nowrap;\n }\n .month-cell.current {\n color: #16a085;\n }\n\n input[type='number'] {\n width: 70px;\n padding: 4px 6px;\n border: 1px solid rgba(0, 0, 0, 0.12);\n border-radius: 4px;\n text-align: center;\n font-size: 13px;\n }\n input[type='number']:focus {\n outline: none;\n border-color: #35618e;\n }\n\n .rating-select {\n padding: 4px 6px;\n border: 1px solid rgba(0, 0, 0, 0.12);\n border-radius: 4px;\n font-size: 13px;\n text-align: center;\n }\n\n .status-saved {\n color: #16a085;\n font-size: 12px;\n }\n .status-unsaved {\n color: #e67e22;\n font-size: 12px;\n }\n .status-new {\n color: #3498db;\n font-size: 12px;\n }\n .status-pending {\n color: #e74c3c;\n font-size: 12px;\n font-weight: 600;\n }\n\n /* 미입력 셀 — 과월/현재월 row 에서 값이 비어있을 때 강조 */\n td.pending input[type='number'],\n td.pending .rating-select {\n border-color: #e74c3c;\n background-color: #fff5f5;\n }\n /* 한 번도 입력된 적 없는 metric 컬럼 — 헤더 강조 */\n th.pending {\n background: #fdecec;\n color: #c0392b;\n }\n\n .button-line {\n display: flex;\n justify-content: center;\n gap: 10px;\n margin-top: 16px;\n }\n .ghost-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n cursor: pointer;\n }\n .ghost-btn.secondary {\n background: #24be7b;\n }\n .ghost-btn.disabled {\n opacity: 0.45;\n cursor: not-allowed;\n }\n\n .delete-btn {\n cursor: pointer;\n color: #999;\n font-size: 18px;\n }\n .delete-btn:hover {\n color: #e74c3c;\n }\n\n .empty-msg {\n text-align: center;\n color: #999;\n padding: 40px 0;\n font-size: 14px;\n }\n `\n ]\n\n @property({ type: Object }) project: any = {}\n\n /** 월별 metric 목록 (periodType=MONTH). KpiMetric admin 에 등록된 것 기준. */\n @state() monthlyMetrics: any[] = []\n\n /** monthRows: { workDate:'YYYY-MM', values:{[metricId]:value}, originalValues, dirty }[] */\n @state() monthRows: any[] = []\n\n @state() addYear: number = new Date().getFullYear()\n @state() addMonth: number = new Date().getMonth() + 1\n /** kpi:input — Step4 월별 데이터 저장 권한 (cumulative 와 같은 권한) */\n @state() canSave = false\n\n async connectedCallback() {\n super.connectedCallback()\n this.canSave = await hasPrivilege({\n category: 'kpi',\n privilege: 'input',\n domainOwnerGranted: true,\n superUserGranted: true\n })\n }\n\n render() {\n const years = this._getYearRange()\n\n return html`\n <div class=\"title\">\n 프로젝트 수행 기간 동안의 월별 데이터를 입력합니다.<br />\n 공정률, 감리자 정성 평가(1~5) 등의 항목을 월 단위로 기록하여 성과 추세 분석에 활용합니다.\n </div>\n\n ${this.isCompleted\n ? html`<div class=\"toolbar\">\n <span class=\"locked-note\">🔒 종료된 프로젝트 — 월 추가 불가, 기존 월 데이터만 수정할 수 있습니다.</span>\n </div>`\n : html`<div class=\"toolbar\">\n <select\n .value=${String(this.addYear)}\n @change=${(e: Event) => (this.addYear = Number((e.target as HTMLSelectElement).value))}\n >\n ${years.map(y => html`<option value=${y} ?selected=${y === this.addYear}>${y}년</option>`)}\n </select>\n <select\n .value=${String(this.addMonth)}\n @change=${(e: Event) => (this.addMonth = Number((e.target as HTMLSelectElement).value))}\n >\n ${[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(\n m => html`<option value=${m} ?selected=${m === this.addMonth}>${m}월</option>`\n )}\n </select>\n <div class=\"add-btn\" @click=${this._addMonth}>+ 월 추가</div>\n </div>`}\n\n ${this.monthlyMetrics.length === 0\n ? html`<div class=\"empty-msg\">\n 월별 metric (periodType=MONTH) 이 등록되어 있지 않습니다. KPI 관리자에서 먼저 등록해주세요.\n </div>`\n : this.monthRows.length === 0\n ? html`<div class=\"empty-msg\">등록된 월별 데이터가 없습니다. 위에서 월을 추가해주세요.</div>`\n : html`\n <div class=\"grid-wrapper\">\n <table>\n <thead>\n <tr>\n <th>월</th>\n ${this.monthlyMetrics.map(\n m => html`\n <th\n class=${this._isMetricNeverEntered(m.id) ? 'pending' : ''}\n title=${this._isMetricNeverEntered(m.id) ? '한 번도 입력된 적 없는 metric' : ''}\n >\n ${m.name}${m.unit ? ` (${m.unit})` : ''}\n </th>\n `\n )}\n <th>상태</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n ${this.monthRows.map(\n (row, rowIdx) => html`\n <tr>\n <td class=\"month-cell ${this._isCurrentMonth(row.workDate) ? 'current' : ''}\">${row.workDate}</td>\n ${this.monthlyMetrics.map(m => {\n const pending = this._isCellPending(row, m.id)\n // 위젯은 metric.meta.input 선언으로 결정(이름추측 isRating 제거).\n return html`\n <td class=${pending ? 'pending' : ''}>\n <kpi-metric-input\n .metric=${this._withInputHint(m)}\n .value=${row.values[m.id] ?? null}\n ?disabled=${!this.canSave}\n @metric-input-change=${(e: CustomEvent) =>\n this._setCell(rowIdx, m.id, e.detail.value)}\n ></kpi-metric-input>\n </td>\n `\n })}\n <td>\n ${row.dirty\n ? html`<span class=\"status-unsaved\">수정됨</span>`\n : row.isAuto\n ? html`<span class=\"status-pending\">미입력</span>`\n : row.isNew\n ? html`<span class=\"status-new\">신규</span>`\n : html`<span class=\"status-saved\">저장됨</span>`}\n </td>\n <td>\n <span class=\"delete-btn\" title=\"삭제\" @click=${() => this._removeMonth(rowIdx)}>✕</span>\n </td>\n </tr>\n `\n )}\n </tbody>\n </table>\n </div>\n `}\n\n <div class=\"button-line\">\n <div class=\"ghost-btn\" @click=${this._reset}>초기화</div>\n <div\n class=\"ghost-btn secondary ${this.canSave ? '' : 'disabled'}\"\n title=${this.canSave ? '' : 'kpi:input 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>\n `\n }\n\n willUpdate(changedProperties: Map<string, any>) {\n super.willUpdate(changedProperties)\n if (changedProperties.has('project') && this.project?.id) {\n this._loadData()\n }\n }\n\n private _getYearRange(): number[] {\n const startYear = this.project?.startDate ? new Date(this.project.startDate).getFullYear() : new Date().getFullYear() - 2\n const endYear = this.project?.endDate ? new Date(this.project.endDate).getFullYear() + 1 : new Date().getFullYear() + 1\n const years: number[] = []\n for (let y = startYear; y <= endYear; y++) years.push(y)\n return years\n }\n\n private _isCurrentMonth(workDate: string): boolean {\n return workDate === moment().tz('Asia/Seoul').format('YYYY-MM')\n }\n\n /**\n * 월별 metric 정의 + 그 프로젝트의 월별 KpiMetricValue 들을 조회해 그리드 row 구성.\n *\n * 1) KpiMetric where periodType=MONTH → monthlyMetrics\n * 2) KpiMetricValue where org=projectId → 월별로 그룹핑하여 monthRows\n */\n private async _loadData() {\n try {\n // 1) 월별 metric 목록\n const metricsResp = await client.query({\n query: gql`\n query KpiMetrics {\n kpiMetrics {\n items {\n id\n name\n unit\n periodType\n rank\n meta\n collectType\n }\n }\n }\n `,\n context: { headers: tenantHeaders(this.project?.code) }\n })\n const all = metricsResp.data?.kpiMetrics?.items || []\n // periodType=MONTH 만, rank 오름차순(미지정 null 은 뒤로, 동률은 기존 순서 유지).\n this.monthlyMetrics = all\n .filter((m: any) => m.periodType === 'MONTH')\n .map((m: any, i: number) => ({ m, i }))\n .sort((a: any, b: any) => {\n const ra = a.m.rank ?? Number.POSITIVE_INFINITY\n const rb = b.m.rank ?? Number.POSITIVE_INFINITY\n return ra !== rb ? ra - rb : a.i - b.i\n })\n .map((x: any) => x.m)\n\n if (this.monthlyMetrics.length === 0) {\n this.monthRows = []\n return\n }\n\n // 2) 프로젝트의 KpiMetricValue 들 (org=projectId). 월별 metric 만 client 측에서 필터.\n const monthlyMetricIds = new Set(this.monthlyMetrics.map((m: any) => m.id))\n const valuesResp = await client.query({\n query: gql`\n query KpiMetricValues($filters: [Filter!]) {\n kpiMetricValues(filters: $filters) {\n items {\n id\n value\n unit\n periodType\n valueDate\n metricId\n }\n }\n }\n `,\n variables: { filters: [{ name: 'org', operator: 'eq', value: this.project.id }] },\n context: { headers: tenantHeaders(this.project?.code) }\n })\n const allValues = valuesResp.data?.kpiMetricValues?.items || []\n // periodType='MONTH' 만 그리드에 — 비월별 row 가 valueDate prefix 로 잘못 묶이지 않도록.\n const monthlyValues = allValues.filter((v: any) => monthlyMetricIds.has(v.metricId) && v.periodType === 'MONTH')\n\n // 월별 그룹핑 — valueDate YYYY-MM-DD 의 앞 7자리. id 도 보존 (삭제 시 사용).\n // 현재월(=this month) row 는 운영 원칙상 표시 대상 아님 → 아예 bucket 제외.\n const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM')\n const byMonth = new Map<string, any>()\n for (const v of monthlyValues) {\n const ym = (v.valueDate || '').slice(0, 7)\n if (!ym) continue\n if (ym === currentYm) continue // 현재월 record 무시 (DB 에 우연히 있더라도)\n if (!byMonth.has(ym)) byMonth.set(ym, { workDate: ym, values: {}, originalValues: {}, valueIds: {}, dirty: false })\n const row = byMonth.get(ym)\n row.values[v.metricId] = v.value\n row.originalValues[v.metricId] = v.value\n row.valueIds[v.metricId] = v.id\n }\n // project.startDate ~ 현재월 사이 누락된 월 placeholder row 자동 추가.\n // 사용자가 값을 입력하지 않으면 dirty 가 안 되므로 저장 시 무시되어 spurious patch 없음.\n for (const ym of this._generateExpectedMonths()) {\n if (!byMonth.has(ym)) {\n byMonth.set(ym, {\n workDate: ym,\n values: {},\n originalValues: {},\n valueIds: {},\n dirty: false,\n isAuto: true\n })\n }\n }\n\n this.monthRows = Array.from(byMonth.values()).sort((a, b) => b.workDate.localeCompare(a.workDate))\n } catch (e) {\n console.error('Failed to load monthly data:', e)\n this.monthRows = []\n }\n }\n\n /** project.startDate (YYYY-MM) ~ **전월** (YYYY-MM) 까지 매월 문자열 배열.\n * 현재월은 의도적으로 제외 — 운영 원칙상 \"이번달 데이터는 아직 입력 대상이 아님\". */\n private _generateExpectedMonths(): string[] {\n const startStr = (this.project?.startDate || '').slice(0, 7)\n if (!startStr || !/^\\d{4}-\\d{2}$/.test(startStr)) return []\n const tz = 'Asia/Seoul'\n let cur = moment.tz(startStr, 'YYYY-MM', tz)\n const end = moment.tz(moment().tz(tz).subtract(1, 'month').format('YYYY-MM'), 'YYYY-MM', tz)\n if (!cur.isValid() || cur.isAfter(end)) return []\n const months: string[] = []\n while (cur.isSameOrBefore(end)) {\n months.push(cur.format('YYYY-MM'))\n cur = cur.add(1, 'month')\n }\n return months\n }\n\n /** 셀 미입력 여부 — 값 없음 AND 그 월이 **전월** (직전 한 달) 인 경우만 pending.\n * 과월 전체가 아니라 전월 한 달에 한해서만 강조 — 사용자 입력 흐름(이번 달에 지난달 데이터)과 일치. */\n private _isCellPending(row: any, metricId: string): boolean {\n const v = row.values?.[metricId]\n if (v !== undefined && v !== null && v !== '') return false\n const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM')\n return row.workDate === lastMonth\n }\n\n /** metric 컬럼이 전 row 통틀어 한 번도 값이 없으면 헤더 강조. */\n private _isMetricNeverEntered(metricId: string): boolean {\n return !this.monthRows.some(r => {\n const v = r.values?.[metricId]\n return v !== undefined && v !== null && v !== ''\n })\n }\n\n /** 종료(COMPLETED) 프로젝트 — 신규 월 추가 금지, 기존 월만 수정 가능. */\n private get isCompleted(): boolean {\n return this.project?.state === ProjectState.COMPLETED\n }\n\n private _addMonth() {\n // 종료된 프로젝트는 새 월을 추가할 수 없다 (기존 월 데이터 수정만 허용).\n if (this.isCompleted) {\n notify({ message: '종료된 프로젝트는 월을 추가할 수 없습니다. 기존 월 데이터만 수정할 수 있습니다.' })\n return\n }\n\n const workDate = `${this.addYear}-${String(this.addMonth).padStart(2, '0')}`\n\n // 현재월 row 는 추가 금지 — 운영 원칙상 이번달 데이터는 입력 대상 아님.\n const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM')\n if (workDate === currentYm) {\n notify({ message: '현재월(이번 달) 데이터는 입력 대상이 아닙니다.' })\n return\n }\n // 미래월도 차단\n if (workDate > currentYm) {\n notify({ message: '미래월은 입력 대상이 아닙니다.' })\n return\n }\n\n if (this.monthRows.some(r => r.workDate === workDate)) {\n notify({ message: `${workDate}은 이미 존재합니다.` })\n return\n }\n\n const newRow = { workDate, values: {}, originalValues: {}, valueIds: {}, dirty: true, isNew: true }\n const rows = [...this.monthRows, newRow]\n rows.sort((a, b) => b.workDate.localeCompare(a.workDate))\n this.monthRows = rows\n }\n\n private async _removeMonth(rowIdx: number) {\n const row = this.monthRows[rowIdx]\n\n // 신규 row (DB 미반영) — 메모리에서만 제거\n if (row.isNew) {\n this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx)\n return\n }\n\n // 저장된 row — 그 월의 모든 KpiMetricValue id 들을 한 번에 삭제.\n // 권한: things-factory 의 deleteKpiMetricValues 는 category=\"kpi\", privilege=\"mutation\"\n // (superUser/도메인 owner 자동 통과, 일반 사용자는 별도 grant 필요).\n const ids = Object.values(row.valueIds || {}).filter(Boolean) as string[]\n if (ids.length === 0) {\n this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx)\n return\n }\n\n const ok = await OxPrompt.open({\n title: `${row.workDate} 의 월별 데이터를 삭제하시겠습니까?`,\n confirmButton: { text: '삭제' },\n cancelButton: { text: '취소' }\n })\n if (!ok) return\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation DeleteKpiMetricValues($ids: [String!]!) {\n deleteKpiMetricValues(ids: $ids)\n }\n `,\n variables: { ids },\n context: { headers: tenantHeaders(this.project?.code) }\n })\n if (response.errors) {\n throw new Error(response.errors.map((e: any) => e.message).join('\\n'))\n }\n notify({ message: `${row.workDate} 월별 데이터 ${ids.length}건 삭제되었습니다.` })\n await this._loadData()\n } catch (e) {\n console.error('Failed to delete monthly values:', e)\n notify({ message: '삭제 중 오류가 발생했습니다. 권한을 확인해주세요.' })\n }\n }\n\n /**\n * metric 입력 힌트 결정: DB 선언(meta.input) 우선. 없으면 전환용 파생(DB meta 미도입 동안).\n * 향후 metric.meta.input 이 채워지면 이 파생 분기(및 isRating)는 제거한다.\n */\n private _withInputHint(m: any) {\n // 유효 힌트: DB 선언(meta.input) 우선, 없으면 전환용 파생.\n const input: any = m?.meta?.input\n ? m.meta.input\n : isRating(m?.name)\n ? { widget: 'rating', max: 5, step: 1 }\n : { widget: 'number', step: 1, min: 0 }\n\n // 월별 그리드는 조밀해 별표 시인성이 떨어지므로, rating 은 1~N 셀렉터로 \"표현\"만 오버라이드.\n // (metric 설정=별표 는 그대로. 넓은 완료평가 화면은 별표 유지)\n if (input.widget === 'rating') {\n const max = input.max ?? 5\n const options = Array.from({ length: max }, (_, i) => ({ value: i + 1, label: String(i + 1) }))\n return { ...m, meta: { ...(m?.meta || {}), input: { widget: 'select', options } } }\n }\n\n return { ...m, meta: { ...(m?.meta || {}), input } }\n }\n\n /** <kpi-metric-input> 이 방출하는 (이미 타입된) 값을 그대로 저장. */\n private _setCell(rowIdx: number, metricId: string, value: any) {\n this.monthRows = this.monthRows.map((row, i) =>\n i !== rowIdx ? row : { ...row, values: { ...row.values, [metricId]: value }, dirty: true }\n )\n }\n\n /**\n * 저장 — dirty row 들 안의 변경된 cell 들을 KpiMetricValuePatch 로 모아\n * updateKpiMetricValuesCumulative 한 번 호출 (backend upsert 가 unique 조합 처리).\n */\n private async _save() {\n const patches: any[] = []\n for (const row of this.monthRows) {\n if (!row.dirty) continue\n for (const m of this.monthlyMetrics) {\n const value = row.values[m.id]\n const original = row.originalValues[m.id]\n if (value === undefined || value === null) continue // 빈 값은 skip (기존 유지)\n if (value === original) continue // 변화 없으면 skip\n patches.push({\n metricId: m.id,\n value,\n unit: m.unit || '',\n org: this.project.id,\n periodType: 'MONTH',\n valueDate: `${row.workDate}-01`\n })\n }\n }\n\n if (patches.length === 0) {\n notify({ message: '변경된 데이터가 없습니다.' })\n return\n }\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateKpiMetricValuesCumulative($patches: [KpiMetricValuePatch!]!) {\n updateKpiMetricValuesCumulative(patches: $patches) {\n id\n }\n }\n `,\n variables: { patches },\n context: { headers: tenantHeaders(this.project?.code) }\n })\n if (response.errors) {\n throw new Error(response.errors.map((e: any) => e.message).join('\\n'))\n }\n notify({ message: `${patches.length}건 저장되었습니다.` })\n\n // 월별 → 기본정보(BuildingComplex) 역동기 (BC→월별 방향과 대칭).\n // MONTH 양방향 필드(총투입인력·총설계변경 등)를 전월 값 기준으로 BC 에 반영.\n // syncKpiToProjectInfo 의 findExistingRow 가 MONTH 는 전월 row 를 선택하므로\n // monthRows 를 KpiMetricValue 형태로 평탄화해 전달한다.\n try {\n const kmvs = this.monthRows.flatMap((row: any) =>\n this.monthlyMetrics.map((m: any) => ({\n metricId: m.id,\n value: row.values[m.id],\n periodType: 'MONTH',\n valueDate: `${row.workDate}-01`,\n unit: m.unit || '',\n org: this.project.id\n }))\n )\n await syncKpiToProjectInfo(this.project, this.monthlyMetrics, kmvs, this.project?.code)\n } catch (e) {\n console.warn('[월별→프로젝트정보] BuildingComplex 동기 실패', e)\n }\n\n await this._loadData()\n } catch (e) {\n console.error('Failed to save monthly values:', e)\n notify({ message: '저장 중 오류가 발생했습니다.' })\n }\n }\n\n private _reset() {\n this._loadData()\n }\n}\n"]}
1
+ {"version":3,"file":"pc-tab4-monthly.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab4-monthly.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AACpE,OAAO,MAAM,MAAM,iBAAiB,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,iDAAiD,CAAA;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AACpE,OAAO,4DAA4D,CAAA;AAEnE;;;;;;;;;;;;GAYG;AAEH,uEAAuE;AACvE,SAAS,QAAQ,CAAC,UAAkB;IAClC,OAAO,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAA;AACvC,CAAC;AAGM,IAAM,4BAA4B,GAAlC,MAAM,4BAA6B,SAAQ,UAAU;IAArD;;QA8KuB,YAAO,GAAQ,EAAE,CAAA;QAE7C,mEAAmE;QAC1D,mBAAc,GAAU,EAAE,CAAA;QAEnC,4FAA4F;QACnF,cAAS,GAAU,EAAE,CAAA;QAErB,YAAO,GAAW,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QAC1C,aAAQ,GAAW,IAAI,IAAI,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;QACrD,0DAA0D;QACjD,YAAO,GAAG,KAAK,CAAA;IA6d1B,CAAC;IA3dC,KAAK,CAAC,iBAAiB;QACrB,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,CAAC,OAAO,GAAG,MAAM,YAAY,CAAC;YAChC,QAAQ,EAAE,KAAK;YACf,SAAS,EAAE,OAAO;YAClB,kBAAkB,EAAE,IAAI;YACxB,gBAAgB,EAAE,IAAI;SACvB,CAAC,CAAA;IACJ,CAAC;IAED,MAAM;QACJ,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAA;QAElC,OAAO,IAAI,CAAA;;;;;;QAMP,IAAI,CAAC,WAAW;YAChB,CAAC,CAAC,IAAI,CAAA;;iBAEG;YACT,CAAC,CAAC,IAAI,CAAA;;uBAES,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;wBACnB,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAE,CAAC,CAAC,MAA4B,CAAC,KAAK,CAAC,CAAC;;gBAEpF,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC;;;uBAGhF,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;wBACpB,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAE,CAAC,CAAC,MAA4B,CAAC,KAAK,CAAC,CAAC;;gBAErF,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,CAC3C,CAAC,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,CAC9E;;0CAE2B,IAAI,CAAC,SAAS;iBACvC;;QAET,IAAI,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC;YAChC,CAAC,CAAC,IAAI,CAAA;;iBAEG;YACT,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;gBAC3B,CAAC,CAAC,IAAI,CAAA;gBACA,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,kCAAkC;mBACxE;gBACT,CAAC,CAAC,IAAI,CAAA;;;;;;wBAMQ,IAAI,CAAC,cAAc,CAAC,GAAG,CACvB,CAAC,CAAC,EAAE,CAAC,IAAI,CAAA;;oCAEG,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;oCACjD,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE;;8BAEpE,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE;;yBAE1C,CACF;;;;;;sBAMD,IAAI,CAAC,SAAS,CAAC,GAAG,CAClB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAA;;kDAEO,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,QAAQ;4BAC1F,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;;oBAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;oBAC9C,mDAAmD;oBACnD,OAAO,IAAI,CAAA;0CACG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;4CAEtB,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;2CACvB,MAAA,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,mCAAI,IAAI;8CACrB,CAAC,IAAI,CAAC,QAAQ;yDACH,CAAC,CAAc,EAAE,EAAE,CACxC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;;6BAGlD,CAAA;gBACH,CAAC,CAAC;;8BAEE,GAAG,CAAC,KAAK;oBACT,CAAC,CAAC,IAAI,CAAA,yCAAyC;oBAC/C,CAAC,CAAC,GAAG,CAAC,MAAM;wBACV,CAAC,CAAC,IAAI,CAAA,yCAAyC;wBAC/C,CAAC,CAAC,GAAG,CAAC,KAAK;4BACT,CAAC,CAAC,IAAI,CAAA,oCAAoC;4BAC1C,CAAC,CAAC,IAAI,CAAA,uCAAuC;;;8BAGjD,IAAI,CAAC,WAAW;oBAChB,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,IAAI,CAAA,8CAA8C,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW;;;uBAGrG,CACF;;;;aAIR;;QAEL,IAAI,CAAC,WAAW;YAChB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,IAAI,CAAA;4CAC8B,IAAI,CAAC,MAAM;;2CAEZ,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU;sBACnD,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,iBAAiB;uBACpC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE;;;;iBAIxC;KACZ,CAAA;IACH,CAAC;IAED,UAAU,CAAC,iBAAmC;;QAC5C,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAA;QACnC,IAAI,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,KAAI,MAAA,IAAI,CAAC,OAAO,0CAAE,EAAE,CAAA,EAAE,CAAC;YACzD,IAAI,CAAC,SAAS,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IAEO,aAAa;;QACnB,MAAM,SAAS,GAAG,CAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,SAAS,EAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;QACzH,MAAM,OAAO,GAAG,CAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,OAAO,EAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,CAAA;QACvH,MAAM,KAAK,GAAa,EAAE,CAAA;QAC1B,KAAK,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACxD,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,eAAe,CAAC,QAAgB;QACtC,OAAO,QAAQ,KAAK,MAAM,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACjE,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,SAAS;;QACrB,IAAI,CAAC;YACH,kBAAkB;YAClB,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBACrC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;SAcT;gBACD,OAAO,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,EAAE;aACxD,CAAC,CAAA;YACF,MAAM,GAAG,GAAG,CAAA,MAAA,MAAA,WAAW,CAAC,IAAI,0CAAE,UAAU,0CAAE,KAAK,KAAI,EAAE,CAAA;YACrD,8DAA8D;YAC9D,IAAI,CAAC,cAAc,GAAG,GAAG;iBACtB,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC;iBAC5C,GAAG,CAAC,CAAC,CAAM,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;iBACtC,IAAI,CAAC,CAAC,CAAM,EAAE,CAAM,EAAE,EAAE;;gBACvB,MAAM,EAAE,GAAG,MAAA,CAAC,CAAC,CAAC,CAAC,IAAI,mCAAI,MAAM,CAAC,iBAAiB,CAAA;gBAC/C,MAAM,EAAE,GAAG,MAAA,CAAC,CAAC,CAAC,CAAC,IAAI,mCAAI,MAAM,CAAC,iBAAiB,CAAA;gBAC/C,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACxC,CAAC,CAAC;iBACD,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAEvB,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;gBACnB,OAAM;YACR,CAAC;YAED,wEAAwE;YACxE,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAC3E,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBACpC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;SAaT;gBACD,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE;gBACjF,OAAO,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,EAAE;aACxD,CAAC,CAAA;YACF,MAAM,SAAS,GAAG,CAAA,MAAA,MAAA,UAAU,CAAC,IAAI,0CAAE,eAAe,0CAAE,KAAK,KAAI,EAAE,CAAA;YAC/D,uEAAuE;YACvE,MAAM,aAAa,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAA;YAEhH,4DAA4D;YAC5D,yDAAyD;YACzD,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;YAC7D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAe,CAAA;YACtC,KAAK,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;gBAC9B,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC1C,IAAI,CAAC,EAAE;oBAAE,SAAQ;gBACjB,IAAI,EAAE,KAAK,SAAS;oBAAE,SAAQ,CAAC,gCAAgC;gBAC/D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;gBACnH,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAC3B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;gBAChC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;gBACxC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAA;YACjC,CAAC;YACD,0DAA0D;YAC1D,8DAA8D;YAC9D,kDAAkD;YAClD,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,EAAE,EAAE,CAAC;gBACxE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;oBACrB,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE;wBACd,QAAQ,EAAE,EAAE;wBACZ,MAAM,EAAE,EAAE;wBACV,cAAc,EAAE,EAAE;wBAClB,QAAQ,EAAE,EAAE;wBACZ,KAAK,EAAE,KAAK;wBACZ,MAAM,EAAE,IAAI;qBACb,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;YAED,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;QACpG,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,CAAC,CAAC,CAAA;YAChD,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;QACrB,CAAC;IACH,CAAC;IAED;0DACsD;IAC9C,uBAAuB;;QAC7B,MAAM,QAAQ,GAAG,CAAC,CAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,SAAS,KAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5D,IAAI,CAAC,QAAQ,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAA;QAC3D,MAAM,EAAE,GAAG,YAAY,CAAA;QACvB,IAAI,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;QAC5C,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;QAC5F,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,CAAA;QACjD,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,OAAO,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;YAClC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;QAC3B,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC;IAED;qEACiE;IACzD,cAAc,CAAC,GAAQ,EAAE,QAAgB;;QAC/C,MAAM,CAAC,GAAG,MAAA,GAAG,CAAC,MAAM,0CAAG,QAAQ,CAAC,CAAA;QAChC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,KAAK,CAAA;QAC3D,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAClF,OAAO,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAA;IACnC,CAAC;IAED,8CAA8C;IACtC,qBAAqB,CAAC,QAAgB;QAC5C,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;;YAC9B,MAAM,CAAC,GAAG,MAAA,CAAC,CAAC,MAAM,0CAAG,QAAQ,CAAC,CAAA;YAC9B,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAA;QAClD,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,yDAAyD;IACzD,IAAY,WAAW;;QACrB,OAAO,CAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,KAAK,MAAK,YAAY,CAAC,SAAS,CAAA;IACvD,CAAC;IAED,IAAY,QAAQ;QAClB,OAAO,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAA;IAC1C,CAAC;IAEO,SAAS;QACf,IAAI,IAAI,CAAC,WAAW;YAAE,OAAM;QAE5B,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;QAE5E,8CAA8C;QAC9C,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAC7D,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,CAAA;YAClD,OAAM;QACR,CAAC;QACD,UAAU;QACV,IAAI,QAAQ,GAAG,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAA;YACxC,OAAM;QACR,CAAC;QAED,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC;YACtD,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,QAAQ,aAAa,EAAE,CAAC,CAAA;YAC7C,OAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;QACnG,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;QACxC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;QACzD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;IACvB,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,MAAc;;QACvC,IAAI,IAAI,CAAC,WAAW;YAAE,OAAM;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QAElC,8BAA8B;QAC9B,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAA;YAC9D,OAAM;QACR,CAAC;QAED,kDAAkD;QAClD,oFAAoF;QACpF,oDAAoD;QACpD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAa,CAAA;QACzE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAA;YAC9D,OAAM;QACR,CAAC;QAED,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YAC7B,KAAK,EAAE,GAAG,GAAG,CAAC,QAAQ,sBAAsB;YAC5C,aAAa,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;YAC7B,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAA;QACF,IAAI,CAAC,EAAE;YAAE,OAAM;QAEf,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CAAA;;;;SAIZ;gBACD,SAAS,EAAE,EAAE,GAAG,EAAE;gBAClB,OAAO,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,EAAE;aACxD,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;YACxE,CAAC;YACD,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,QAAQ,WAAW,GAAG,CAAC,MAAM,YAAY,EAAE,CAAC,CAAA;YACrE,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,CAAC,CAAC,CAAA;YACpD,MAAM,CAAC,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC,CAAA;QACrD,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,cAAc,CAAC,CAAM;;QAC3B,2CAA2C;QAC3C,MAAM,KAAK,GAAQ,CAAA,MAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,IAAI,0CAAE,KAAK;YAC/B,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK;YACd,CAAC,CAAC,QAAQ,CAAC,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,IAAI,CAAC;gBACnB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;gBACvC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAA;QAEzC,4DAA4D;QAC5D,0CAA0C;QAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,MAAA,KAAK,CAAC,GAAG,mCAAI,CAAC,CAAA;YAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAC/F,uCAAY,CAAC,KAAE,IAAI,kCAAO,CAAC,CAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,IAAI,KAAI,EAAE,CAAC,KAAE,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAI;QACrF,CAAC;QAED,uCAAY,CAAC,KAAE,IAAI,kCAAO,CAAC,CAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,IAAI,KAAI,EAAE,CAAC,KAAE,KAAK,OAAI;IACtD,CAAC;IAED,oDAAoD;IAC5C,QAAQ,CAAC,MAAc,EAAE,QAAgB,EAAE,KAAU;QAC3D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAC7C,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,iCAAM,GAAG,KAAE,MAAM,kCAAO,GAAG,CAAC,MAAM,KAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,KAAI,KAAK,EAAE,IAAI,GAAE,CAC3F,CAAA;IACH,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,KAAK;;QACjB,IAAI,IAAI,CAAC,WAAW;YAAE,OAAM;QAC5B,MAAM,OAAO,GAAU,EAAE,CAAA;QACzB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,KAAK;gBAAE,SAAQ;YACxB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;gBACpC,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAC9B,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;oBAAE,SAAQ,CAAC,oBAAoB;gBACxE,IAAI,KAAK,KAAK,QAAQ;oBAAE,SAAQ,CAAC,cAAc;gBAC/C,OAAO,CAAC,IAAI,CAAC;oBACX,QAAQ,EAAE,CAAC,CAAC,EAAE;oBACd,KAAK;oBACL,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;oBAClB,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE;oBACpB,UAAU,EAAE,OAAO;oBACnB,SAAS,EAAE,GAAG,GAAG,CAAC,QAAQ,KAAK;iBAChC,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAA;YACrC,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;SAMZ;gBACD,SAAS,EAAE,EAAE,OAAO,EAAE;gBACtB,OAAO,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,EAAE;aACxD,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;YACxE,CAAC;YACD,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,YAAY,EAAE,CAAC,CAAA;YAElD,iDAAiD;YACjD,kDAAkD;YAClD,kEAAkE;YAClE,4CAA4C;YAC5C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,GAAQ,EAAE,EAAE,CAC/C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;oBACnC,QAAQ,EAAE,CAAC,CAAC,EAAE;oBACd,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;oBACvB,UAAU,EAAE,OAAO;oBACnB,SAAS,EAAE,GAAG,GAAG,CAAC,QAAQ,KAAK;oBAC/B,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;oBAClB,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE;iBACrB,CAAC,CAAC,CACJ,CAAA;gBACD,MAAM,oBAAoB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,IAAI,CAAC,CAAA;YACzF,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,CAAC,CAAC,CAAA;YACtD,CAAC;YAED,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,CAAC,CAAC,CAAA;YAClD,MAAM,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAA;QACzC,CAAC;IACH,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,SAAS,EAAE,CAAA;IAClB,CAAC;;AAppBM,mCAAM,GAAG;IACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyKF;CACF,AA3KY,CA2KZ;AAE2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;6DAAkB;AAGpC;IAAR,KAAK,EAAE;;oEAA2B;AAG1B;IAAR,KAAK,EAAE;;+DAAsB;AAErB;IAAR,KAAK,EAAE;;6DAA2C;AAC1C;IAAR,KAAK,EAAE;;8DAA6C;AAE5C;IAAR,KAAK,EAAE;;6DAAgB;AAzLb,4BAA4B;IADxC,aAAa,CAAC,oBAAoB,CAAC;GACvB,4BAA4B,CAspBxC","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { gql } from '@apollo/client'\nimport { notify } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport moment from 'moment-timezone'\nimport { tenantHeaders } from '@dssp/project/dist-client/shared/domain-context'\nimport { ProjectState } from '../sv-project-list'\nimport { syncKpiToProjectInfo } from '../../shared/kpi-project-sync'\nimport '@things-factory/kpi/dist-client/viewparts/kpi-metric-input'\n\n/**\n * 프로젝트 완공 처리 탭 4 — 월별 KPI 데이터 그리드.\n *\n * 데이터 source = `KpiMetric` (periodType=MONTH) + `KpiMetricValue` (그 metric 의\n * 월별 값들). 이전 버전 (DataSet/DataSample) 폐기. KpiMetricValue 의 unique\n * 인덱스 (domain, metric, valueDate, org) 가 (도메인, 프로젝트, 메트릭, 월) 단일 row\n * 정책을 보장.\n *\n * 단방향 흐름:\n * - 탭1/탭2 의 metric 저장 (valueDate=YYYY-MM-01) → 그 월 row 가 여기 자동 노출\n * - 여기서 cell 수정 → 그 metric × 그 월 row 만 update (탭1/탭2 의 \"현재값\" 영향 없음 —\n * 탭1/탭2 는 KpiMetricValueProvider 가 latest 또는 valueDate-based lookup 으로 동작)\n */\n\n/** rating 셀렉트로 표시할 metric 인지 판단 — 이름에 '평가' 또는 '이탈' 포함되면 1~5 rating. */\nfunction isRating(metricName: string): boolean {\n return /평가|이탈/.test(metricName || '')\n}\n\n@customElement('sv-pc-tab4-monthly')\nexport class SvProjectCompleteTab4Monthly extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n }\n .title {\n color: #212529;\n font-size: 13px;\n font-weight: 400;\n line-height: 24px;\n text-align: center;\n margin-bottom: 8px;\n }\n\n .toolbar {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n margin-bottom: 12px;\n }\n .toolbar select {\n padding: 5px 8px;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 5px;\n font-size: 14px;\n color: #212529;\n }\n .toolbar .add-btn {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 5px 10px;\n background: #35618e;\n color: #fff;\n border-radius: 5px;\n cursor: pointer;\n font-size: 13px;\n }\n .toolbar .locked-note {\n font-size: 13px;\n color: #6c757d;\n }\n\n .grid-wrapper {\n overflow-x: auto;\n padding: 0 6px;\n }\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n thead th {\n background: #f3f3fa;\n border-top: 2px solid #0c4da2;\n color: #212529;\n text-align: center;\n padding: 8px 6px;\n white-space: nowrap;\n font-weight: 500;\n }\n tbody td {\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n text-align: center;\n padding: 6px 4px;\n vertical-align: middle;\n }\n tbody tr:hover {\n background: #f8fafc;\n }\n .month-cell {\n color: #35618e;\n font-weight: 600;\n white-space: nowrap;\n }\n .month-cell.current {\n color: #16a085;\n }\n\n input[type='number'] {\n width: 70px;\n padding: 4px 6px;\n border: 1px solid rgba(0, 0, 0, 0.12);\n border-radius: 4px;\n text-align: center;\n font-size: 13px;\n }\n input[type='number']:focus {\n outline: none;\n border-color: #35618e;\n }\n\n .rating-select {\n padding: 4px 6px;\n border: 1px solid rgba(0, 0, 0, 0.12);\n border-radius: 4px;\n font-size: 13px;\n text-align: center;\n }\n\n .status-saved {\n color: #16a085;\n font-size: 12px;\n }\n .status-unsaved {\n color: #e67e22;\n font-size: 12px;\n }\n .status-new {\n color: #3498db;\n font-size: 12px;\n }\n .status-pending {\n color: #e74c3c;\n font-size: 12px;\n font-weight: 600;\n }\n\n /* 미입력 셀 — 과월/현재월 row 에서 값이 비어있을 때 강조 */\n td.pending input[type='number'],\n td.pending .rating-select {\n border-color: #e74c3c;\n background-color: #fff5f5;\n }\n /* 한 번도 입력된 적 없는 metric 컬럼 — 헤더 강조 */\n th.pending {\n background: #fdecec;\n color: #c0392b;\n }\n\n .button-line {\n display: flex;\n justify-content: center;\n gap: 10px;\n margin-top: 16px;\n }\n .ghost-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n background: #35618e;\n color: #ffffff;\n border-radius: 5px;\n cursor: pointer;\n }\n .ghost-btn.secondary {\n background: #24be7b;\n }\n .ghost-btn.disabled {\n opacity: 0.45;\n cursor: not-allowed;\n }\n\n .delete-btn {\n cursor: pointer;\n color: #999;\n font-size: 18px;\n }\n .delete-btn:hover {\n color: #e74c3c;\n }\n\n .empty-msg {\n text-align: center;\n color: #999;\n padding: 40px 0;\n font-size: 14px;\n }\n `\n ]\n\n @property({ type: Object }) project: any = {}\n\n /** 월별 metric 목록 (periodType=MONTH). KpiMetric admin 에 등록된 것 기준. */\n @state() monthlyMetrics: any[] = []\n\n /** monthRows: { workDate:'YYYY-MM', values:{[metricId]:value}, originalValues, dirty }[] */\n @state() monthRows: any[] = []\n\n @state() addYear: number = new Date().getFullYear()\n @state() addMonth: number = new Date().getMonth() + 1\n /** kpi:input — Step4 월별 데이터 저장 권한 (cumulative 와 같은 권한) */\n @state() canSave = false\n\n async connectedCallback() {\n super.connectedCallback()\n this.canSave = await hasPrivilege({\n category: 'kpi',\n privilege: 'input',\n domainOwnerGranted: true,\n superUserGranted: true\n })\n }\n\n render() {\n const years = this._getYearRange()\n\n return html`\n <div class=\"title\">\n 프로젝트 수행 기간 동안의 월별 데이터를 입력합니다.<br />\n 공정률, 감리자 정성 평가(1~5) 등의 항목을 월 단위로 기록하여 성과 추세 분석에 활용합니다.\n </div>\n\n ${this.isCompleted\n ? html`<div class=\"toolbar\">\n <span class=\"locked-note\">🔒 완료된 프로젝트 — 입력된 값만 볼 수 있습니다.</span>\n </div>`\n : html`<div class=\"toolbar\">\n <select\n .value=${String(this.addYear)}\n @change=${(e: Event) => (this.addYear = Number((e.target as HTMLSelectElement).value))}\n >\n ${years.map(y => html`<option value=${y} ?selected=${y === this.addYear}>${y}년</option>`)}\n </select>\n <select\n .value=${String(this.addMonth)}\n @change=${(e: Event) => (this.addMonth = Number((e.target as HTMLSelectElement).value))}\n >\n ${[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(\n m => html`<option value=${m} ?selected=${m === this.addMonth}>${m}월</option>`\n )}\n </select>\n <div class=\"add-btn\" @click=${this._addMonth}>+ 월 추가</div>\n </div>`}\n\n ${this.monthlyMetrics.length === 0\n ? html`<div class=\"empty-msg\">\n 월별 metric (periodType=MONTH) 이 등록되어 있지 않습니다. KPI 관리자에서 먼저 등록해주세요.\n </div>`\n : this.monthRows.length === 0\n ? html`<div class=\"empty-msg\">\n ${this.isCompleted ? '입력된 월별 데이터가 없습니다.' : '등록된 월별 데이터가 없습니다. 위에서 월을 추가해주세요.'}\n </div>`\n : html`\n <div class=\"grid-wrapper\">\n <table>\n <thead>\n <tr>\n <th>월</th>\n ${this.monthlyMetrics.map(\n m => html`\n <th\n class=${this._isMetricNeverEntered(m.id) ? 'pending' : ''}\n title=${this._isMetricNeverEntered(m.id) ? '한 번도 입력된 적 없는 metric' : ''}\n >\n ${m.name}${m.unit ? ` (${m.unit})` : ''}\n </th>\n `\n )}\n <th>상태</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n ${this.monthRows.map(\n (row, rowIdx) => html`\n <tr>\n <td class=\"month-cell ${this._isCurrentMonth(row.workDate) ? 'current' : ''}\">${row.workDate}</td>\n ${this.monthlyMetrics.map(m => {\n const pending = this._isCellPending(row, m.id)\n // 위젯은 metric.meta.input 선언으로 결정(이름추측 isRating 제거).\n return html`\n <td class=${pending ? 'pending' : ''}>\n <kpi-metric-input\n .metric=${this._withInputHint(m)}\n .value=${row.values[m.id] ?? null}\n ?disabled=${!this.editable}\n @metric-input-change=${(e: CustomEvent) =>\n this._setCell(rowIdx, m.id, e.detail.value)}\n ></kpi-metric-input>\n </td>\n `\n })}\n <td>\n ${row.dirty\n ? html`<span class=\"status-unsaved\">수정됨</span>`\n : row.isAuto\n ? html`<span class=\"status-pending\">미입력</span>`\n : row.isNew\n ? html`<span class=\"status-new\">신규</span>`\n : html`<span class=\"status-saved\">저장됨</span>`}\n </td>\n <td>\n ${this.isCompleted\n ? ''\n : html`<span class=\"delete-btn\" title=\"삭제\" @click=${() => this._removeMonth(rowIdx)}>✕</span>`}\n </td>\n </tr>\n `\n )}\n </tbody>\n </table>\n </div>\n `}\n\n ${this.isCompleted\n ? ''\n : html`<div class=\"button-line\">\n <div class=\"ghost-btn\" @click=${this._reset}>초기화</div>\n <div\n class=\"ghost-btn secondary ${this.canSave ? '' : 'disabled'}\"\n title=${this.canSave ? '' : 'kpi:input 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>`}\n `\n }\n\n willUpdate(changedProperties: Map<string, any>) {\n super.willUpdate(changedProperties)\n if (changedProperties.has('project') && this.project?.id) {\n this._loadData()\n }\n }\n\n private _getYearRange(): number[] {\n const startYear = this.project?.startDate ? new Date(this.project.startDate).getFullYear() : new Date().getFullYear() - 2\n const endYear = this.project?.endDate ? new Date(this.project.endDate).getFullYear() + 1 : new Date().getFullYear() + 1\n const years: number[] = []\n for (let y = startYear; y <= endYear; y++) years.push(y)\n return years\n }\n\n private _isCurrentMonth(workDate: string): boolean {\n return workDate === moment().tz('Asia/Seoul').format('YYYY-MM')\n }\n\n /**\n * 월별 metric 정의 + 그 프로젝트의 월별 KpiMetricValue 들을 조회해 그리드 row 구성.\n *\n * 1) KpiMetric where periodType=MONTH → monthlyMetrics\n * 2) KpiMetricValue where org=projectId → 월별로 그룹핑하여 monthRows\n */\n private async _loadData() {\n try {\n // 1) 월별 metric 목록\n const metricsResp = await client.query({\n query: gql`\n query KpiMetrics {\n kpiMetrics {\n items {\n id\n name\n unit\n periodType\n rank\n meta\n collectType\n }\n }\n }\n `,\n context: { headers: tenantHeaders(this.project?.code) }\n })\n const all = metricsResp.data?.kpiMetrics?.items || []\n // periodType=MONTH 만, rank 오름차순(미지정 null 은 뒤로, 동률은 기존 순서 유지).\n this.monthlyMetrics = all\n .filter((m: any) => m.periodType === 'MONTH')\n .map((m: any, i: number) => ({ m, i }))\n .sort((a: any, b: any) => {\n const ra = a.m.rank ?? Number.POSITIVE_INFINITY\n const rb = b.m.rank ?? Number.POSITIVE_INFINITY\n return ra !== rb ? ra - rb : a.i - b.i\n })\n .map((x: any) => x.m)\n\n if (this.monthlyMetrics.length === 0) {\n this.monthRows = []\n return\n }\n\n // 2) 프로젝트의 KpiMetricValue 들 (org=projectId). 월별 metric 만 client 측에서 필터.\n const monthlyMetricIds = new Set(this.monthlyMetrics.map((m: any) => m.id))\n const valuesResp = await client.query({\n query: gql`\n query KpiMetricValues($filters: [Filter!]) {\n kpiMetricValues(filters: $filters) {\n items {\n id\n value\n unit\n periodType\n valueDate\n metricId\n }\n }\n }\n `,\n variables: { filters: [{ name: 'org', operator: 'eq', value: this.project.id }] },\n context: { headers: tenantHeaders(this.project?.code) }\n })\n const allValues = valuesResp.data?.kpiMetricValues?.items || []\n // periodType='MONTH' 만 그리드에 — 비월별 row 가 valueDate prefix 로 잘못 묶이지 않도록.\n const monthlyValues = allValues.filter((v: any) => monthlyMetricIds.has(v.metricId) && v.periodType === 'MONTH')\n\n // 월별 그룹핑 — valueDate YYYY-MM-DD 의 앞 7자리. id 도 보존 (삭제 시 사용).\n // 현재월(=this month) row 는 운영 원칙상 표시 대상 아님 → 아예 bucket 제외.\n const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM')\n const byMonth = new Map<string, any>()\n for (const v of monthlyValues) {\n const ym = (v.valueDate || '').slice(0, 7)\n if (!ym) continue\n if (ym === currentYm) continue // 현재월 record 무시 (DB 에 우연히 있더라도)\n if (!byMonth.has(ym)) byMonth.set(ym, { workDate: ym, values: {}, originalValues: {}, valueIds: {}, dirty: false })\n const row = byMonth.get(ym)\n row.values[v.metricId] = v.value\n row.originalValues[v.metricId] = v.value\n row.valueIds[v.metricId] = v.id\n }\n // project.startDate ~ 현재월 사이 누락된 월 placeholder row 자동 추가.\n // 사용자가 값을 입력하지 않으면 dirty 가 안 되므로 저장 시 무시되어 spurious patch 없음.\n // 완료된 프로젝트는 입력할 수 없으므로 빈 월을 만들지 않는다 — 입력된 월만 보인다.\n for (const ym of this.isCompleted ? [] : this._generateExpectedMonths()) {\n if (!byMonth.has(ym)) {\n byMonth.set(ym, {\n workDate: ym,\n values: {},\n originalValues: {},\n valueIds: {},\n dirty: false,\n isAuto: true\n })\n }\n }\n\n this.monthRows = Array.from(byMonth.values()).sort((a, b) => b.workDate.localeCompare(a.workDate))\n } catch (e) {\n console.error('Failed to load monthly data:', e)\n this.monthRows = []\n }\n }\n\n /** project.startDate (YYYY-MM) ~ **전월** (YYYY-MM) 까지 매월 문자열 배열.\n * 현재월은 의도적으로 제외 — 운영 원칙상 \"이번달 데이터는 아직 입력 대상이 아님\". */\n private _generateExpectedMonths(): string[] {\n const startStr = (this.project?.startDate || '').slice(0, 7)\n if (!startStr || !/^\\d{4}-\\d{2}$/.test(startStr)) return []\n const tz = 'Asia/Seoul'\n let cur = moment.tz(startStr, 'YYYY-MM', tz)\n const end = moment.tz(moment().tz(tz).subtract(1, 'month').format('YYYY-MM'), 'YYYY-MM', tz)\n if (!cur.isValid() || cur.isAfter(end)) return []\n const months: string[] = []\n while (cur.isSameOrBefore(end)) {\n months.push(cur.format('YYYY-MM'))\n cur = cur.add(1, 'month')\n }\n return months\n }\n\n /** 셀 미입력 여부 — 값 없음 AND 그 월이 **전월** (직전 한 달) 인 경우만 pending.\n * 과월 전체가 아니라 전월 한 달에 한해서만 강조 — 사용자 입력 흐름(이번 달에 지난달 데이터)과 일치. */\n private _isCellPending(row: any, metricId: string): boolean {\n const v = row.values?.[metricId]\n if (v !== undefined && v !== null && v !== '') return false\n const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM')\n return row.workDate === lastMonth\n }\n\n /** metric 컬럼이 전 row 통틀어 한 번도 값이 없으면 헤더 강조. */\n private _isMetricNeverEntered(metricId: string): boolean {\n return !this.monthRows.some(r => {\n const v = r.values?.[metricId]\n return v !== undefined && v !== null && v !== ''\n })\n }\n\n /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 추가·수정·삭제는 하지 않는다. */\n private get isCompleted(): boolean {\n return this.project?.state === ProjectState.COMPLETED\n }\n\n private get editable(): boolean {\n return this.canSave && !this.isCompleted\n }\n\n private _addMonth() {\n if (this.isCompleted) return\n\n const workDate = `${this.addYear}-${String(this.addMonth).padStart(2, '0')}`\n\n // 현재월 row 는 추가 금지 — 운영 원칙상 이번달 데이터는 입력 대상 아님.\n const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM')\n if (workDate === currentYm) {\n notify({ message: '현재월(이번 달) 데이터는 입력 대상이 아닙니다.' })\n return\n }\n // 미래월도 차단\n if (workDate > currentYm) {\n notify({ message: '미래월은 입력 대상이 아닙니다.' })\n return\n }\n\n if (this.monthRows.some(r => r.workDate === workDate)) {\n notify({ message: `${workDate}은 이미 존재합니다.` })\n return\n }\n\n const newRow = { workDate, values: {}, originalValues: {}, valueIds: {}, dirty: true, isNew: true }\n const rows = [...this.monthRows, newRow]\n rows.sort((a, b) => b.workDate.localeCompare(a.workDate))\n this.monthRows = rows\n }\n\n private async _removeMonth(rowIdx: number) {\n if (this.isCompleted) return\n const row = this.monthRows[rowIdx]\n\n // 신규 row (DB 미반영) — 메모리에서만 제거\n if (row.isNew) {\n this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx)\n return\n }\n\n // 저장된 row — 그 월의 모든 KpiMetricValue id 들을 한 번에 삭제.\n // 권한: things-factory 의 deleteKpiMetricValues 는 category=\"kpi\", privilege=\"mutation\"\n // (superUser/도메인 owner 자동 통과, 일반 사용자는 별도 grant 필요).\n const ids = Object.values(row.valueIds || {}).filter(Boolean) as string[]\n if (ids.length === 0) {\n this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx)\n return\n }\n\n const ok = await OxPrompt.open({\n title: `${row.workDate} 의 월별 데이터를 삭제하시겠습니까?`,\n confirmButton: { text: '삭제' },\n cancelButton: { text: '취소' }\n })\n if (!ok) return\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation DeleteKpiMetricValues($ids: [String!]!) {\n deleteKpiMetricValues(ids: $ids)\n }\n `,\n variables: { ids },\n context: { headers: tenantHeaders(this.project?.code) }\n })\n if (response.errors) {\n throw new Error(response.errors.map((e: any) => e.message).join('\\n'))\n }\n notify({ message: `${row.workDate} 월별 데이터 ${ids.length}건 삭제되었습니다.` })\n await this._loadData()\n } catch (e) {\n console.error('Failed to delete monthly values:', e)\n notify({ message: '삭제 중 오류가 발생했습니다. 권한을 확인해주세요.' })\n }\n }\n\n /**\n * metric 입력 힌트 결정: DB 선언(meta.input) 우선. 없으면 전환용 파생(DB meta 미도입 동안).\n * 향후 metric.meta.input 이 채워지면 이 파생 분기(및 isRating)는 제거한다.\n */\n private _withInputHint(m: any) {\n // 유효 힌트: DB 선언(meta.input) 우선, 없으면 전환용 파생.\n const input: any = m?.meta?.input\n ? m.meta.input\n : isRating(m?.name)\n ? { widget: 'rating', max: 5, step: 1 }\n : { widget: 'number', step: 1, min: 0 }\n\n // 월별 그리드는 조밀해 별표 시인성이 떨어지므로, rating 은 1~N 셀렉터로 \"표현\"만 오버라이드.\n // (metric 설정=별표 는 그대로. 넓은 완료평가 화면은 별표 유지)\n if (input.widget === 'rating') {\n const max = input.max ?? 5\n const options = Array.from({ length: max }, (_, i) => ({ value: i + 1, label: String(i + 1) }))\n return { ...m, meta: { ...(m?.meta || {}), input: { widget: 'select', options } } }\n }\n\n return { ...m, meta: { ...(m?.meta || {}), input } }\n }\n\n /** <kpi-metric-input> 이 방출하는 (이미 타입된) 값을 그대로 저장. */\n private _setCell(rowIdx: number, metricId: string, value: any) {\n this.monthRows = this.monthRows.map((row, i) =>\n i !== rowIdx ? row : { ...row, values: { ...row.values, [metricId]: value }, dirty: true }\n )\n }\n\n /**\n * 저장 — dirty row 들 안의 변경된 cell 들을 KpiMetricValuePatch 로 모아\n * updateKpiMetricValuesCumulative 한 번 호출 (backend upsert 가 unique 조합 처리).\n */\n private async _save() {\n if (this.isCompleted) return\n const patches: any[] = []\n for (const row of this.monthRows) {\n if (!row.dirty) continue\n for (const m of this.monthlyMetrics) {\n const value = row.values[m.id]\n const original = row.originalValues[m.id]\n if (value === undefined || value === null) continue // 빈 값은 skip (기존 유지)\n if (value === original) continue // 변화 없으면 skip\n patches.push({\n metricId: m.id,\n value,\n unit: m.unit || '',\n org: this.project.id,\n periodType: 'MONTH',\n valueDate: `${row.workDate}-01`\n })\n }\n }\n\n if (patches.length === 0) {\n notify({ message: '변경된 데이터가 없습니다.' })\n return\n }\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateKpiMetricValuesCumulative($patches: [KpiMetricValuePatch!]!) {\n updateKpiMetricValuesCumulative(patches: $patches) {\n id\n }\n }\n `,\n variables: { patches },\n context: { headers: tenantHeaders(this.project?.code) }\n })\n if (response.errors) {\n throw new Error(response.errors.map((e: any) => e.message).join('\\n'))\n }\n notify({ message: `${patches.length}건 저장되었습니다.` })\n\n // 월별 → 기본정보(BuildingComplex) 역동기 (BC→월별 방향과 대칭).\n // MONTH 양방향 필드(총투입인력·총설계변경 등)를 전월 값 기준으로 BC 에 반영.\n // syncKpiToProjectInfo 의 findExistingRow 가 MONTH 는 전월 row 를 선택하므로\n // monthRows 를 KpiMetricValue 형태로 평탄화해 전달한다.\n try {\n const kmvs = this.monthRows.flatMap((row: any) =>\n this.monthlyMetrics.map((m: any) => ({\n metricId: m.id,\n value: row.values[m.id],\n periodType: 'MONTH',\n valueDate: `${row.workDate}-01`,\n unit: m.unit || '',\n org: this.project.id\n }))\n )\n await syncKpiToProjectInfo(this.project, this.monthlyMetrics, kmvs, this.project?.code)\n } catch (e) {\n console.warn('[월별→프로젝트정보] BuildingComplex 동기 실패', e)\n }\n\n await this._loadData()\n } catch (e) {\n console.error('Failed to save monthly values:', e)\n notify({ message: '저장 중 오류가 발생했습니다.' })\n }\n }\n\n private _reset() {\n this._loadData()\n }\n}\n"]}
@@ -1,10 +1,10 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import { navigate, PageView } from '@operato/shell';
3
3
  import { ScrollbarStyles } from '@operato/styles';
4
- import { css, html } from 'lit';
4
+ import { css, html, nothing } from 'lit';
5
5
  import { customElement, state } from 'lit/decorators.js';
6
6
  import { ScopedElementsMixin } from '@open-wc/scoped-elements';
7
- import { PROJECT_STATE } from './sv-project-list';
7
+ import { PROJECT_STATE, ProjectState } from './sv-project-list';
8
8
  import { client } from '@operato/graphql';
9
9
  import gql from 'graphql-tag';
10
10
  import moment from 'moment-timezone';
@@ -171,32 +171,39 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
171
171
  <div>KPI 데이타입력</div>
172
172
  </div>
173
173
  </div>
174
- <div class="sub-desc">
175
- 이 프로젝트에 아직 입력되지 않은 KPI 항목입니다. 해당 항목에 대한 데이터를 입력 해 주시기 바랍니다.
176
- </div>
174
+ ${
175
+ /* 완료된 공사에는 더 입력할 KPI 가 없다 — 월 단위 지표를 "지난달 값" 으로 판정하면
176
+ 준공 뒤 모든 달이 미입력으로 잡힌다. 목록 자체를 보이지 않는다. */
177
+ this.project.state === ProjectState.COMPLETED
178
+ ? nothing
179
+ : html `
180
+ <div class="sub-desc">
181
+ 이 프로젝트에 아직 입력되지 않은 KPI 항목입니다. 해당 항목에 대한 데이터를 입력 해 주시기 바랍니다.
182
+ </div>
177
183
 
178
- <div class="list-block">
179
- <div class="list-title">• 미입력 ${this.pendingMetrics.length}건</div>
180
- ${this.pendingMetrics.length === 0
181
- ? html `<div class="list-text">모든 항목이 입력되었습니다.</div>`
182
- : this.pendingMetrics.map(m => html `
183
- <div
184
- class="list-text"
185
- style="cursor: pointer;"
186
- @click=${async () => {
187
- if (isProjectTypeDomain()) {
188
- navigate('project-complete');
189
- return;
190
- }
191
- const id = this.project.id || (await this._resolveProjectIdFromDomain());
192
- if (id)
193
- navigate(`project-complete/${id}`);
194
- }}
195
- >
196
- # ${m.name}
197
- </div>
198
- `)}
199
- </div>
184
+ <div class="list-block">
185
+ <div class="list-title">• 미입력 ${this.pendingMetrics.length}건</div>
186
+ ${this.pendingMetrics.length === 0
187
+ ? html `<div class="list-text">모든 항목이 입력되었습니다.</div>`
188
+ : this.pendingMetrics.map(m => html `
189
+ <div
190
+ class="list-text"
191
+ style="cursor: pointer;"
192
+ @click=${async () => {
193
+ if (isProjectTypeDomain()) {
194
+ navigate('project-complete');
195
+ return;
196
+ }
197
+ const id = this.project.id || (await this._resolveProjectIdFromDomain());
198
+ if (id)
199
+ navigate(`project-complete/${id}`);
200
+ }}
201
+ >
202
+ # ${m.name}
203
+ </div>
204
+ `)}
205
+ </div>
206
+ `}
200
207
  </div>
201
208
  </div>
202
209