@dssp/dkpi 1.0.30 → 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.
Files changed (24) hide show
  1. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +3 -10
  2. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +63 -79
  3. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
  4. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +2 -0
  5. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +28 -11
  6. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
  7. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +2 -0
  8. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +42 -16
  9. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
  10. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +2 -1
  11. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +31 -20
  12. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -1
  13. package/dist-client/pages/sv-project-detail.js +34 -27
  14. package/dist-client/pages/sv-project-detail.js.map +1 -1
  15. package/dist-client/pages/sv-project-update.d.ts +2 -3
  16. package/dist-client/pages/sv-project-update.js +156 -17
  17. package/dist-client/pages/sv-project-update.js.map +1 -1
  18. package/dist-client/tsconfig.tsbuildinfo +1 -1
  19. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +6 -1
  20. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +12 -15
  21. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  22. package/dist-server/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +3 -3
  24. package/schema.graphql +1932 -125
@@ -6,6 +6,7 @@ import moment from 'moment-timezone';
6
6
  import { notify } from '@operato/layout';
7
7
  import { hasPrivilege } from '@things-factory/auth-base/dist-client';
8
8
  import '@things-factory/kpi/dist-client/viewparts/kpi-metric-input';
9
+ import { ProjectState } from '../sv-project-list';
9
10
  let SvProjectCompleteTab2Rating = class SvProjectCompleteTab2Rating extends LitElement {
10
11
  constructor() {
11
12
  super(...arguments);
@@ -15,6 +16,11 @@ let SvProjectCompleteTab2Rating = class SvProjectCompleteTab2Rating extends LitE
15
16
  /** kpi:assessment — Step2 평가 저장 권한 */
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
  render() {
19
25
  // 전월 (last month) YYYY-MM. 월별 metric 의 "전월 데이터 입력" 검사 기준.
20
26
  const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
@@ -57,7 +63,7 @@ let SvProjectCompleteTab2Rating = class SvProjectCompleteTab2Rating extends LitE
57
63
  <kpi-metric-input
58
64
  .metric=${this._withRatingHint(metric)}
59
65
  .value=${(_b = kpiMetricValue === null || kpiMetricValue === void 0 ? void 0 : kpiMetricValue.value) !== null && _b !== void 0 ? _b : null}
60
- ?disabled=${!this.canSave}
66
+ ?disabled=${!this.canSave || this.isCompleted}
61
67
  @metric-input-change=${(e) => this._setRating(metric.id, e.detail.value)}
62
68
  ></kpi-metric-input>
63
69
  </div>
@@ -65,16 +71,18 @@ let SvProjectCompleteTab2Rating = class SvProjectCompleteTab2Rating extends LitE
65
71
  `;
66
72
  })}
67
73
 
68
- <div class="button-line">
69
- <div class="ghost-btn" @click=${this._reset}>초기화</div>
70
- <div
71
- class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
72
- title=${this.canSave ? '' : 'kpi:assessment 권한 필요'}
73
- @click=${() => this.canSave && this._save()}
74
- >
75
- 저장
76
- </div>
77
- </div>
74
+ ${this.isCompleted
75
+ ? html `<div class="locked-note">🔒 완료된 프로젝트 — 입력된 값만 볼 수 있습니다.</div>`
76
+ : html `<div class="button-line">
77
+ <div class="ghost-btn" @click=${this._reset}>초기화</div>
78
+ <div
79
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
80
+ title=${this.canSave ? '' : 'kpi:assessment 권한 필요'}
81
+ @click=${() => this.canSave && this._save()}
82
+ >
83
+ 저장
84
+ </div>
85
+ </div>`}
78
86
  </div>
79
87
  `;
80
88
  }
@@ -176,6 +184,8 @@ let SvProjectCompleteTab2Rating = class SvProjectCompleteTab2Rating extends LitE
176
184
  }
177
185
  async _save() {
178
186
  var _a;
187
+ if (this.isCompleted)
188
+ return;
179
189
  const response = await updateProjectCompleteStep2(this.kpiMetricValues, (_a = this.project) === null || _a === void 0 ? void 0 : _a.code);
180
190
  if (!response.errors) {
181
191
  notify({ message: '저장되었습니다.' });
@@ -319,6 +329,13 @@ SvProjectCompleteTab2Rating.styles = [
319
329
  opacity: 0.45;
320
330
  cursor: not-allowed;
321
331
  }
332
+
333
+ .locked-note {
334
+ margin-top: 16px;
335
+ text-align: right;
336
+ font-size: 13px;
337
+ color: #6c757d;
338
+ }
322
339
  `
323
340
  ];
324
341
  __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"pc-tab2-rating.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab2-rating.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { getKpiCategories, getKpiMetrics, getKpiMetricValues, updateProjectCompleteStep2 } from '../../shared/complete-api'\nimport moment from 'moment-timezone'\nimport { notify } from '@operato/layout'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport '@things-factory/kpi/dist-client/viewparts/kpi-metric-input'\n\n@customElement('sv-pc-tab2-rating')\nexport class SvProjectCompleteTab2Rating 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 padding: 8px 6px;\n }\n .row.header {\n min-height: 35px;\n background: #f3f3fa;\n border-top: 2px #0c4da2 solid;\n grid-template-columns: 300px 1fr 1fr;\n padding: 0px 25px;\n\n .header-label {\n color: #212529;\n text-align: center;\n }\n }\n .row {\n display: grid;\n grid-template-columns: 300px 1fr 1fr;\n gap: 6px 10px;\n padding: 8px 25px;\n align-items: center;\n border-bottom: 1px rgba(0, 0, 0, 0.1) solid;\n\n .cell {\n display: flex;\n align-items: center;\n justify-content: center;\n }\n }\n .label {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n white-space: nowrap;\n display: flex;\n justify-content: center;\n }\n .stars {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n }\n .star-wrap {\n position: relative;\n width: 28px;\n height: 28px;\n display: inline-block;\n }\n .star-base,\n .star-fill {\n position: absolute;\n top: 0;\n left: 0;\n font-size: 28px;\n line-height: 28px;\n user-select: none;\n }\n .star-base {\n color: #d0d7e2;\n }\n .star-fill {\n color: #ffb400;\n overflow: hidden;\n width: 0%;\n }\n .star-wrap {\n cursor: pointer;\n }\n .score {\n color: #212529;\n text-align: right;\n }\n .unit {\n text-align: center;\n color: #212529;\n }\n .plus {\n color: #e13232;\n font-weight: 700;\n }\n .minus {\n color: #1e88e5;\n font-weight: 700;\n }\n /* 미입력 — kpiMetricValue 가 비어있는 metric 행 */\n .label.pending::before {\n content: '⚠';\n color: #e74c3c;\n margin-right: 4px;\n }\n .stars.pending .star-base {\n color: #e74c3c;\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 ]\n\n @property({ type: Object }) project: any = {}\n @state() kpiMetricValues: any = []\n @state() kpiMetrics: any = []\n /** kpi:assessment — Step2 평가 저장 권한 */\n @state() canSave = false\n\n render() {\n // 전월 (last month) YYYY-MM. 월별 metric 의 \"전월 데이터 입력\" 검사 기준.\n const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM')\n\n return html`\n <div class=\"title\">\n <div>\n 당초 계획 대비 실제 진행 과정에서 변동된 공사비, 공기(공사기간), 면적, 기타 주요 항목을 현실에 맞게 수정·입력합니다.\n <br />이 정보는 성과 분석, KPI 평가, 통계 산출 등에 기준값으로 사용되므로, 가능한 한 실제 값 기준으로 정확히\n 입력해주시기 바랍니다.\n </div>\n </div>\n\n <div class=\"rows\">\n <div class=\"row header\">\n <div class=\"header-label\">성과영역</div>\n <div class=\"header-label\">현재 평가값</div>\n <div class=\"header-label\">완료 평가</div>\n </div>\n\n ${this.kpiMetrics.map((metric, idx) => {\n const isMonthly = metric.periodType === 'MONTH'\n // MONTH 는 '월별 데이터' 탭의 최신월과 양방향 동기화되도록 그 metric 의 실제 최신월을\n // 읽는다 (데이터 없으면 전월로 폴백). 종료 프로젝트는 최종월이 곧 최신월.\n const targetMonth = isMonthly ? this._latestMonthOf(metric.id) || lastMonth : ''\n const kpiMetricValue = isMonthly\n ? this.kpiMetricValues.find(\n (item: any) =>\n item.metricId === metric.id && item.periodType === 'MONTH' && (item.valueDate || '').startsWith(targetMonth)\n ) || {}\n : this.kpiMetricValues.find((item: any) => item.metricId === metric.id && item.periodType === metric.periodType) || {}\n const isPending = kpiMetricValue.value === undefined || kpiMetricValue.value === null\n\n return html`\n <div class=\"row\">\n <div\n class=\"label ${isPending ? 'pending' : ''}\"\n title=${isPending ? (isMonthly ? '전월 데이터 입력 필요' : '아직 평가되지 않은 항목') : ''}\n >\n • ${metric.name}\n </div>\n <div class=\"cell\">${kpiMetricValue?.value ?? '-'}</div>\n <div class=\"stars ${isPending ? 'pending' : ''}\">\n <kpi-metric-input\n .metric=${this._withRatingHint(metric)}\n .value=${kpiMetricValue?.value ?? null}\n ?disabled=${!this.canSave}\n @metric-input-change=${(e: CustomEvent) => this._setRating(metric.id, e.detail.value)}\n ></kpi-metric-input>\n </div>\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:assessment 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>\n </div>\n `\n }\n\n async connectedCallback() {\n super.connectedCallback()\n this.canSave = await hasPrivilege({\n category: 'kpi',\n privilege: 'assessment',\n domainOwnerGranted: true,\n superUserGranted: true\n })\n if (this.project?.id) {\n this._getInitData()\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._getInitData()\n }\n }\n\n private async _getInitData() {\n const kpiMetrics = await getKpiMetrics(this.project?.code)\n // 정성 평가 항목만 — 순서는 getKpiMetrics 가 rank 기준으로 이미 정렬.\n this.kpiMetrics = kpiMetrics.filter(item => item.name.includes('정성 평가')) || []\n this.kpiMetricValues = await getKpiMetricValues(this.project.id, this.project?.code)\n }\n\n /** 그 metric 의 월별(MONTH) 데이터 중 가장 최신 월(YYYY-MM). 없으면 null.\n * '완료 평가' 는 이 최신월을 읽고/쓰므로 '월별 데이터' 탭 최신월과 양방향 동기화된다. */\n private _latestMonthOf(metricId: string): string | null {\n const months = (this.kpiMetricValues || [])\n .filter((i: any) => i.metricId === metricId && i.periodType === 'MONTH' && i.valueDate)\n .map((i: any) => (i.valueDate as string).slice(0, 7))\n if (!months.length) return null\n return months.sort((a: string, b: string) => b.localeCompare(a))[0]\n }\n\n /** 완료평가 탭 — 모든 metric 을 1~5 별점으로. DB meta.input 이 있으면 우선. */\n private _withRatingHint(metric: any) {\n if (metric?.meta?.input) return metric\n return { ...metric, meta: { ...(metric?.meta || {}), input: { widget: 'rating', max: 5, step: 1 } } }\n }\n\n private _setRating(metricId: string, score5: number) {\n // score5: 1~5 정수 (별 전체 클릭).\n const metric = this.kpiMetrics.find((m: any) => m.id === metricId)\n if (!metric) return\n\n const today = moment().tz('Asia/Seoul')\n const todayYmd = today.format('YYYY-MM-DD')\n const periodType = metric.periodType\n let updated = [...this.kpiMetricValues]\n\n if (periodType === 'MONTH') {\n // '월별 데이터' 탭의 최신월과 같은 레코드를 수정해야 양방향 동기화가 된다.\n // 최신월이 없으면(데이터 없음) 전월로 폴백해 신규 생성.\n const targetYm = this._latestMonthOf(metricId) || today.clone().subtract(1, 'month').format('YYYY-MM')\n const target1 = `${targetYm}-01`\n const idx = updated.findIndex(\n (i: any) => i.metricId === metricId && i.periodType === 'MONTH' && (i.valueDate || '').startsWith(targetYm)\n )\n if (idx !== -1) {\n updated[idx] = { ...updated[idx], value: score5 }\n } else {\n updated.push({\n id: crypto.randomUUID(),\n value: score5,\n metricId,\n unit: metric.unit || '',\n org: this.project.id,\n periodType: 'MONTH',\n valueDate: target1\n })\n }\n } else {\n // ALLTIME / DAY / 기타 — metric.periodType 그대로 단일 row.\n const idx = updated.findIndex((i: any) => i.metricId === metricId && i.periodType === periodType)\n if (idx !== -1) {\n updated[idx] = { ...updated[idx], value: score5 }\n } else {\n updated.push({\n id: crypto.randomUUID(),\n value: score5,\n metricId,\n unit: metric.unit || '',\n org: this.project.id,\n periodType,\n valueDate: todayYmd\n })\n }\n }\n\n this.kpiMetricValues = updated\n this.dispatchEvent(new CustomEvent('complete-data-change', { detail: { tab: 2, data: updated } }))\n }\n\n private async _save() {\n const response = await updateProjectCompleteStep2(this.kpiMetricValues, this.project?.code)\n if (!response.errors) {\n notify({ message: '저장되었습니다.' })\n }\n }\n\n private _reset() {\n this._getInitData()\n }\n}\n"]}
1
+ {"version":3,"file":"pc-tab2-rating.js","sourceRoot":"","sources":["../../../client/pages/project-complete-tabs/pc-tab2-rating.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { getKpiCategories, getKpiMetrics, getKpiMetricValues, updateProjectCompleteStep2 } from '../../shared/complete-api'\nimport moment from 'moment-timezone'\nimport { notify } from '@operato/layout'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport '@things-factory/kpi/dist-client/viewparts/kpi-metric-input'\nimport { ProjectState } from '../sv-project-list'\n\n@customElement('sv-pc-tab2-rating')\nexport class SvProjectCompleteTab2Rating 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 padding: 8px 6px;\n }\n .row.header {\n min-height: 35px;\n background: #f3f3fa;\n border-top: 2px #0c4da2 solid;\n grid-template-columns: 300px 1fr 1fr;\n padding: 0px 25px;\n\n .header-label {\n color: #212529;\n text-align: center;\n }\n }\n .row {\n display: grid;\n grid-template-columns: 300px 1fr 1fr;\n gap: 6px 10px;\n padding: 8px 25px;\n align-items: center;\n border-bottom: 1px rgba(0, 0, 0, 0.1) solid;\n\n .cell {\n display: flex;\n align-items: center;\n justify-content: center;\n }\n }\n .label {\n color: #35618e;\n font-size: 16px;\n letter-spacing: -0.05em;\n white-space: nowrap;\n display: flex;\n justify-content: center;\n }\n .stars {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n }\n .star-wrap {\n position: relative;\n width: 28px;\n height: 28px;\n display: inline-block;\n }\n .star-base,\n .star-fill {\n position: absolute;\n top: 0;\n left: 0;\n font-size: 28px;\n line-height: 28px;\n user-select: none;\n }\n .star-base {\n color: #d0d7e2;\n }\n .star-fill {\n color: #ffb400;\n overflow: hidden;\n width: 0%;\n }\n .star-wrap {\n cursor: pointer;\n }\n .score {\n color: #212529;\n text-align: right;\n }\n .unit {\n text-align: center;\n color: #212529;\n }\n .plus {\n color: #e13232;\n font-weight: 700;\n }\n .minus {\n color: #1e88e5;\n font-weight: 700;\n }\n /* 미입력 — kpiMetricValue 가 비어있는 metric 행 */\n .label.pending::before {\n content: '⚠';\n color: #e74c3c;\n margin-right: 4px;\n }\n .stars.pending .star-base {\n color: #e74c3c;\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 .locked-note {\n margin-top: 16px;\n text-align: right;\n font-size: 13px;\n color: #6c757d;\n }\n `\n ]\n\n @property({ type: Object }) project: any = {}\n @state() kpiMetricValues: any = []\n @state() kpiMetrics: any = []\n /** kpi:assessment — Step2 평가 저장 권한 */\n @state() canSave = false\n\n /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 입력·저장은 하지 않는다. */\n private get isCompleted(): boolean {\n return this.project?.state === ProjectState.COMPLETED\n }\n\n render() {\n // 전월 (last month) YYYY-MM. 월별 metric 의 \"전월 데이터 입력\" 검사 기준.\n const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM')\n\n return html`\n <div class=\"title\">\n <div>\n 당초 계획 대비 실제 진행 과정에서 변동된 공사비, 공기(공사기간), 면적, 기타 주요 항목을 현실에 맞게 수정·입력합니다.\n <br />이 정보는 성과 분석, KPI 평가, 통계 산출 등에 기준값으로 사용되므로, 가능한 한 실제 값 기준으로 정확히\n 입력해주시기 바랍니다.\n </div>\n </div>\n\n <div class=\"rows\">\n <div class=\"row header\">\n <div class=\"header-label\">성과영역</div>\n <div class=\"header-label\">현재 평가값</div>\n <div class=\"header-label\">완료 평가</div>\n </div>\n\n ${this.kpiMetrics.map((metric, idx) => {\n const isMonthly = metric.periodType === 'MONTH'\n // MONTH 는 '월별 데이터' 탭의 최신월과 양방향 동기화되도록 그 metric 의 실제 최신월을\n // 읽는다 (데이터 없으면 전월로 폴백). 종료 프로젝트는 최종월이 곧 최신월.\n const targetMonth = isMonthly ? this._latestMonthOf(metric.id) || lastMonth : ''\n const kpiMetricValue = isMonthly\n ? this.kpiMetricValues.find(\n (item: any) =>\n item.metricId === metric.id && item.periodType === 'MONTH' && (item.valueDate || '').startsWith(targetMonth)\n ) || {}\n : this.kpiMetricValues.find((item: any) => item.metricId === metric.id && item.periodType === metric.periodType) || {}\n const isPending = kpiMetricValue.value === undefined || kpiMetricValue.value === null\n\n return html`\n <div class=\"row\">\n <div\n class=\"label ${isPending ? 'pending' : ''}\"\n title=${isPending ? (isMonthly ? '전월 데이터 입력 필요' : '아직 평가되지 않은 항목') : ''}\n >\n • ${metric.name}\n </div>\n <div class=\"cell\">${kpiMetricValue?.value ?? '-'}</div>\n <div class=\"stars ${isPending ? 'pending' : ''}\">\n <kpi-metric-input\n .metric=${this._withRatingHint(metric)}\n .value=${kpiMetricValue?.value ?? null}\n ?disabled=${!this.canSave || this.isCompleted}\n @metric-input-change=${(e: CustomEvent) => this._setRating(metric.id, e.detail.value)}\n ></kpi-metric-input>\n </div>\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:assessment 권한 필요'}\n @click=${() => this.canSave && this._save()}\n >\n 저장\n </div>\n </div>`}\n </div>\n `\n }\n\n async connectedCallback() {\n super.connectedCallback()\n this.canSave = await hasPrivilege({\n category: 'kpi',\n privilege: 'assessment',\n domainOwnerGranted: true,\n superUserGranted: true\n })\n if (this.project?.id) {\n this._getInitData()\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._getInitData()\n }\n }\n\n private async _getInitData() {\n const kpiMetrics = await getKpiMetrics(this.project?.code)\n // 정성 평가 항목만 — 순서는 getKpiMetrics 가 rank 기준으로 이미 정렬.\n this.kpiMetrics = kpiMetrics.filter(item => item.name.includes('정성 평가')) || []\n this.kpiMetricValues = await getKpiMetricValues(this.project.id, this.project?.code)\n }\n\n /** 그 metric 의 월별(MONTH) 데이터 중 가장 최신 월(YYYY-MM). 없으면 null.\n * '완료 평가' 는 이 최신월을 읽고/쓰므로 '월별 데이터' 탭 최신월과 양방향 동기화된다. */\n private _latestMonthOf(metricId: string): string | null {\n const months = (this.kpiMetricValues || [])\n .filter((i: any) => i.metricId === metricId && i.periodType === 'MONTH' && i.valueDate)\n .map((i: any) => (i.valueDate as string).slice(0, 7))\n if (!months.length) return null\n return months.sort((a: string, b: string) => b.localeCompare(a))[0]\n }\n\n /** 완료평가 탭 — 모든 metric 을 1~5 별점으로. DB meta.input 이 있으면 우선. */\n private _withRatingHint(metric: any) {\n if (metric?.meta?.input) return metric\n return { ...metric, meta: { ...(metric?.meta || {}), input: { widget: 'rating', max: 5, step: 1 } } }\n }\n\n private _setRating(metricId: string, score5: number) {\n // score5: 1~5 정수 (별 전체 클릭).\n const metric = this.kpiMetrics.find((m: any) => m.id === metricId)\n if (!metric) return\n\n const today = moment().tz('Asia/Seoul')\n const todayYmd = today.format('YYYY-MM-DD')\n const periodType = metric.periodType\n let updated = [...this.kpiMetricValues]\n\n if (periodType === 'MONTH') {\n // '월별 데이터' 탭의 최신월과 같은 레코드를 수정해야 양방향 동기화가 된다.\n // 최신월이 없으면(데이터 없음) 전월로 폴백해 신규 생성.\n const targetYm = this._latestMonthOf(metricId) || today.clone().subtract(1, 'month').format('YYYY-MM')\n const target1 = `${targetYm}-01`\n const idx = updated.findIndex(\n (i: any) => i.metricId === metricId && i.periodType === 'MONTH' && (i.valueDate || '').startsWith(targetYm)\n )\n if (idx !== -1) {\n updated[idx] = { ...updated[idx], value: score5 }\n } else {\n updated.push({\n id: crypto.randomUUID(),\n value: score5,\n metricId,\n unit: metric.unit || '',\n org: this.project.id,\n periodType: 'MONTH',\n valueDate: target1\n })\n }\n } else {\n // ALLTIME / DAY / 기타 — metric.periodType 그대로 단일 row.\n const idx = updated.findIndex((i: any) => i.metricId === metricId && i.periodType === periodType)\n if (idx !== -1) {\n updated[idx] = { ...updated[idx], value: score5 }\n } else {\n updated.push({\n id: crypto.randomUUID(),\n value: score5,\n metricId,\n unit: metric.unit || '',\n org: this.project.id,\n periodType,\n valueDate: todayYmd\n })\n }\n }\n\n this.kpiMetricValues = updated\n this.dispatchEvent(new CustomEvent('complete-data-change', { detail: { tab: 2, data: updated } }))\n }\n\n private async _save() {\n if (this.isCompleted) return\n const response = await updateProjectCompleteStep2(this.kpiMetricValues, this.project?.code)\n if (!response.errors) {\n notify({ message: '저장되었습니다.' })\n }\n }\n\n private _reset() {\n this._getInitData()\n }\n}\n"]}
@@ -7,6 +7,8 @@ export declare class SvProjectCompleteTab3Upload extends LitElement {
7
7
  slpaData: any[];
8
8
  /** kpi:sentiment — Step3 보고서 첨부/저장 권한 */
9
9
  canSave: boolean;
10
+ /** 완료(COMPLETED) 프로젝트 — 입력된 값은 보이지만 입력·저장은 하지 않는다. */
11
+ private get isCompleted();
10
12
  connectedCallback(): Promise<void>;
11
13
  render(): import("lit-html").TemplateResult<1>;
12
14
  willUpdate(changedProperties: Map<string, any>): void;
@@ -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":"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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 { 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)