@dssp/dkpi 1.0.0-alpha.96 → 1.0.0-alpha.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +10 -1
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
- package/dist-client/pages/sv-project-update.js +10 -0
- package/dist-client/pages/sv-project-update.js.map +1 -1
- package/dist-client/shared/complete-api.d.ts +6 -0
- package/dist-client/shared/complete-api.js +18 -0
- package/dist-client/shared/complete-api.js.map +1 -1
- package/dist-client/shared/kpi-project-sync.d.ts +15 -0
- package/dist-client/shared/kpi-project-sync.js +127 -0
- package/dist-client/shared/kpi-project-sync.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -128,6 +128,24 @@ export const getKpiCategories = async (projectCode) => {
|
|
|
128
128
|
}
|
|
129
129
|
return response.data.kpiCategories;
|
|
130
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* BuildingComplex 컬럼 부분 업데이트. updateBuildingComplex 는 기존 row 를 load 후
|
|
133
|
+
* patch 를 merge 저장하므로 (building-complex-mutation.ts) 지정 컬럼만 안전하게 갱신
|
|
134
|
+
* — updateProject 처럼 buildings 재생성 side-effect 가 없다.
|
|
135
|
+
*/
|
|
136
|
+
export const updateBuildingComplexFields = async (id, patch, projectCode) => {
|
|
137
|
+
return await client.mutate({
|
|
138
|
+
mutation: gql `
|
|
139
|
+
mutation UpdateBuildingComplex($id: String!, $patch: BuildingComplexPatch!) {
|
|
140
|
+
updateBuildingComplex(id: $id, patch: $patch) {
|
|
141
|
+
id
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
variables: { id, patch },
|
|
146
|
+
context: { headers: tenantHeaders(projectCode) }
|
|
147
|
+
});
|
|
148
|
+
};
|
|
131
149
|
export const updateProjectCompleteStep1 = async (patches, projectCode) => {
|
|
132
150
|
const response = await client.mutate({
|
|
133
151
|
mutation: gql `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"complete-api.js","sourceRoot":"","sources":["../../client/shared/complete-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { gql } from '@apollo/client'\nimport { client } from '@operato/graphql'\nimport { tenantHeaders } from '@dssp/project/dist-client/shared/domain-context'\n\nexport const getProject = async (projectId: string, projectCode?: string) => {\n const response = await client.query({\n query: gql`\n query Project($id: String!) {\n project(id: $id) {\n id\n code\n name\n startDate\n endDate\n state\n\n completeReport {\n id\n name\n fullpath\n }\n\n buildingComplex {\n id\n coverageRatio\n floorAreaRatio\n workerCount\n designChangeCount\n constructionCost\n area\n }\n }\n }\n `,\n variables: { id: projectId },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return {}\n }\n\n return response.data.project\n}\n\nexport const getKpiMetrics = async (projectCode?: string) => {\n const response = await client.query({\n query: gql`\n query KpiMetrics {\n kpiMetrics {\n items {\n id\n name\n unit\n periodType\n description\n active\n rank\n }\n }\n }\n `,\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return []\n }\n\n // KpiMetric 자체의 active 만 통과 — admin 에서 active=false 로 비활성화한 metric 은 자동 제외.\n // rank 오름차순 정렬 (미지정=null 은 뒤로). rank 가 같으면 기존(서버) 순서 유지.\n const items = (response.data.kpiMetrics.items || []).filter((m: any) => m.active !== false)\n return items\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\nexport const getKpiMetricValues = async (projectId: string, projectCode?: string) => {\n const filters = [{ name: 'org', operator: 'eq', value: projectId }]\n\n const response = 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: {\n filters\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiMetricValues.items\n}\n\nexport const getKpiCategories = async (projectCode?: string) => {\n const response = await client.query({\n query: gql`\n query {\n kpiCategories: kpisLevel1 {\n id\n name\n description\n active\n kpis: children {\n id\n name\n description\n formula\n active\n grades\n }\n }\n }\n `,\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiCategories\n}\n\nexport const updateProjectCompleteStep1 = async (patches: any, projectCode?: string) => {\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: {\n patches\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\nexport const updateProjectCompleteStep2 = async (patches: any, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateKpiMetricValuesAssessment($patches: [KpiMetricValuePatch!]!) {\n updateKpiMetricValuesAssessment(patches: $patches) {\n id\n }\n }\n `,\n variables: {\n patches\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\nexport const updateProjectCompleteStep3 = async (file: any, projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateKpiMetricValuesSentiment($attachment: NewAttachment!) {\n updateKpiMetricValuesSentiment(attachment: $attachment) {\n id\n name\n fullpath\n }\n }\n `,\n variables: {\n attachment: {\n file,\n refBy: projectId,\n refType: 'ProjectCompleteReport'\n }\n },\n context: {\n hasUpload: true,\n headers: tenantHeaders(projectCode)\n }\n })\n\n return response\n}\n\n/**\n * UI \"자동 수집\" 버튼 전용 — 세움터/올바로/키스콘 시나리오를 순차 호출 후\n * 시스템별 성공/실패 결과를 반환. 시나리오는 도메인 EnvVar 만으로 동작하므로\n * 파라미터 없음. 시나리오 내부 step 이 KPI Value 를 DB 에 적재하므로 호출 후\n * KPI Metric Values 를 다시 조회해 form 을 갱신하는 것이 정상 흐름.\n */\nexport interface ExternalDataCollectionResult {\n source: string // 'saeumteo' | 'allbaro' | 'kiscon'\n label: string // '세움터' | '올바로' | '키스콘'\n ok: boolean\n message: string\n /**\n * 시나리오 result 객체. projectKey: value 맵.\n * - 세움터: { area, floorAreaRatio, upperFloorCount, lowerFloorCount,\n * structureType, siteType }\n * - 올바로: { totalConstructionWasteAmount }\n * - 키스콘: { constructionPeriod, constructionCost }\n *\n * siteType (건물 용도, SiteType enum 값 — 예: APARTMENT_COMPLEX) 와\n * structureType (구조형태) 는 KPI metric 이 아니라 BuildingComplex 의 기본\n * 속성으로 사용. KISCON 의 \"공사유형(신설/증축)\" 은 SiteType 도메인과 다르므로\n * 매핑 대상 아님.\n *\n * 없는 키는 omit. 시나리오 실패 시 null/undefined.\n */\n data?: Record<string, number | string | null>\n}\n\nexport const collectProjectExternalData = async (\n projectId: string,\n projectCode?: string\n): Promise<ExternalDataCollectionResult[]> => {\n const response = await client.mutate({\n mutation: gql`\n mutation CollectProjectExternalData($projectId: String!) {\n collectProjectExternalData(projectId: $projectId) {\n source\n label\n ok\n message\n data\n }\n }\n `,\n variables: { projectId },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n // 전체 mutation 자체 실패 — UI 가 시스템별 fallback 결과 구성\n throw new Error(response.errors.map((e: any) => e.message).join('\\n'))\n }\n\n return response.data.collectProjectExternalData\n}\n\nexport const updateProjectCompleteFinalize = async (projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation FinalizeProjectWithKpiRecalculation($projectId: String!) {\n finalizeProjectWithKpiRecalculation(projectId: $projectId)\n }\n `,\n variables: {\n projectId\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\n/** 완료(COMPLETED) 프로젝트를 진행중(ONGOING) 으로 되돌림 (완료처리 토글의 해제). */\nexport const updateProjectCompleteUnfinalize = async (projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateProject($project: ProjectPatch!) {\n updateProject(project: $project) {\n id\n state\n }\n }\n `,\n variables: { project: { id: projectId, state: 'ONGOING' } },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\nexport const recalculateProjectKpi = async (projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation RecalculateProjectKpi($projectId: String!) {\n recalculateProjectKpi(projectId: $projectId)\n }\n `,\n variables: {\n projectId\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n"]}
|
|
1
|
+
{"version":3,"file":"complete-api.js","sourceRoot":"","sources":["../../client/shared/complete-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { gql } from '@apollo/client'\nimport { client } from '@operato/graphql'\nimport { tenantHeaders } from '@dssp/project/dist-client/shared/domain-context'\n\nexport const getProject = async (projectId: string, projectCode?: string) => {\n const response = await client.query({\n query: gql`\n query Project($id: String!) {\n project(id: $id) {\n id\n code\n name\n startDate\n endDate\n state\n\n completeReport {\n id\n name\n fullpath\n }\n\n buildingComplex {\n id\n coverageRatio\n floorAreaRatio\n workerCount\n designChangeCount\n constructionCost\n area\n }\n }\n }\n `,\n variables: { id: projectId },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return {}\n }\n\n return response.data.project\n}\n\nexport const getKpiMetrics = async (projectCode?: string) => {\n const response = await client.query({\n query: gql`\n query KpiMetrics {\n kpiMetrics {\n items {\n id\n name\n unit\n periodType\n description\n active\n rank\n }\n }\n }\n `,\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return []\n }\n\n // KpiMetric 자체의 active 만 통과 — admin 에서 active=false 로 비활성화한 metric 은 자동 제외.\n // rank 오름차순 정렬 (미지정=null 은 뒤로). rank 가 같으면 기존(서버) 순서 유지.\n const items = (response.data.kpiMetrics.items || []).filter((m: any) => m.active !== false)\n return items\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\nexport const getKpiMetricValues = async (projectId: string, projectCode?: string) => {\n const filters = [{ name: 'org', operator: 'eq', value: projectId }]\n\n const response = 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: {\n filters\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiMetricValues.items\n}\n\nexport const getKpiCategories = async (projectCode?: string) => {\n const response = await client.query({\n query: gql`\n query {\n kpiCategories: kpisLevel1 {\n id\n name\n description\n active\n kpis: children {\n id\n name\n description\n formula\n active\n grades\n }\n }\n }\n `,\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiCategories\n}\n\n/**\n * BuildingComplex 컬럼 부분 업데이트. updateBuildingComplex 는 기존 row 를 load 후\n * patch 를 merge 저장하므로 (building-complex-mutation.ts) 지정 컬럼만 안전하게 갱신\n * — updateProject 처럼 buildings 재생성 side-effect 가 없다.\n */\nexport const updateBuildingComplexFields = async (id: string, patch: any, projectCode?: string) => {\n return await client.mutate({\n mutation: gql`\n mutation UpdateBuildingComplex($id: String!, $patch: BuildingComplexPatch!) {\n updateBuildingComplex(id: $id, patch: $patch) {\n id\n }\n }\n `,\n variables: { id, patch },\n context: { headers: tenantHeaders(projectCode) }\n })\n}\n\nexport const updateProjectCompleteStep1 = async (patches: any, projectCode?: string) => {\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: {\n patches\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\nexport const updateProjectCompleteStep2 = async (patches: any, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateKpiMetricValuesAssessment($patches: [KpiMetricValuePatch!]!) {\n updateKpiMetricValuesAssessment(patches: $patches) {\n id\n }\n }\n `,\n variables: {\n patches\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\nexport const updateProjectCompleteStep3 = async (file: any, projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateKpiMetricValuesSentiment($attachment: NewAttachment!) {\n updateKpiMetricValuesSentiment(attachment: $attachment) {\n id\n name\n fullpath\n }\n }\n `,\n variables: {\n attachment: {\n file,\n refBy: projectId,\n refType: 'ProjectCompleteReport'\n }\n },\n context: {\n hasUpload: true,\n headers: tenantHeaders(projectCode)\n }\n })\n\n return response\n}\n\n/**\n * UI \"자동 수집\" 버튼 전용 — 세움터/올바로/키스콘 시나리오를 순차 호출 후\n * 시스템별 성공/실패 결과를 반환. 시나리오는 도메인 EnvVar 만으로 동작하므로\n * 파라미터 없음. 시나리오 내부 step 이 KPI Value 를 DB 에 적재하므로 호출 후\n * KPI Metric Values 를 다시 조회해 form 을 갱신하는 것이 정상 흐름.\n */\nexport interface ExternalDataCollectionResult {\n source: string // 'saeumteo' | 'allbaro' | 'kiscon'\n label: string // '세움터' | '올바로' | '키스콘'\n ok: boolean\n message: string\n /**\n * 시나리오 result 객체. projectKey: value 맵.\n * - 세움터: { area, floorAreaRatio, upperFloorCount, lowerFloorCount,\n * structureType, siteType }\n * - 올바로: { totalConstructionWasteAmount }\n * - 키스콘: { constructionPeriod, constructionCost }\n *\n * siteType (건물 용도, SiteType enum 값 — 예: APARTMENT_COMPLEX) 와\n * structureType (구조형태) 는 KPI metric 이 아니라 BuildingComplex 의 기본\n * 속성으로 사용. KISCON 의 \"공사유형(신설/증축)\" 은 SiteType 도메인과 다르므로\n * 매핑 대상 아님.\n *\n * 없는 키는 omit. 시나리오 실패 시 null/undefined.\n */\n data?: Record<string, number | string | null>\n}\n\nexport const collectProjectExternalData = async (\n projectId: string,\n projectCode?: string\n): Promise<ExternalDataCollectionResult[]> => {\n const response = await client.mutate({\n mutation: gql`\n mutation CollectProjectExternalData($projectId: String!) {\n collectProjectExternalData(projectId: $projectId) {\n source\n label\n ok\n message\n data\n }\n }\n `,\n variables: { projectId },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n if (response.errors) {\n // 전체 mutation 자체 실패 — UI 가 시스템별 fallback 결과 구성\n throw new Error(response.errors.map((e: any) => e.message).join('\\n'))\n }\n\n return response.data.collectProjectExternalData\n}\n\nexport const updateProjectCompleteFinalize = async (projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation FinalizeProjectWithKpiRecalculation($projectId: String!) {\n finalizeProjectWithKpiRecalculation(projectId: $projectId)\n }\n `,\n variables: {\n projectId\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\n/** 완료(COMPLETED) 프로젝트를 진행중(ONGOING) 으로 되돌림 (완료처리 토글의 해제). */\nexport const updateProjectCompleteUnfinalize = async (projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation UpdateProject($project: ProjectPatch!) {\n updateProject(project: $project) {\n id\n state\n }\n }\n `,\n variables: { project: { id: projectId, state: 'ONGOING' } },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n\nexport const recalculateProjectKpi = async (projectId: string, projectCode?: string) => {\n const response = await client.mutate({\n mutation: gql`\n mutation RecalculateProjectKpi($projectId: String!) {\n recalculateProjectKpi(projectId: $projectId)\n }\n `,\n variables: {\n projectId\n },\n context: { headers: tenantHeaders(projectCode) }\n })\n\n return response\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** BuildingComplex 실컬럼 보유 → 프로젝트정보 ↔ KPI 양방향 동기 대상. */
|
|
2
|
+
export declare const BIDIRECTIONAL_BC_KEYS: readonly ["workerCount", "area", "floorAreaRatio", "designChangeCount"];
|
|
3
|
+
/**
|
|
4
|
+
* 프로젝트 정보관리 저장 → KPI 기준정보 반영.
|
|
5
|
+
* - 4개 양방향 필드: BuildingComplex 값 → KpiMetricValue
|
|
6
|
+
* - constructionPeriod: startDate/endDate → ceil(기간/30) 개월 (일방향)
|
|
7
|
+
* 대상 metric 이 kpi-admin 에서 비활성/삭제/개명됐으면 매칭 실패 → 조용히 skip.
|
|
8
|
+
*/
|
|
9
|
+
export declare function syncProjectInfoToKpi(project: any, projectCode?: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* KPI 정보입력 저장 → 프로젝트 정보(BuildingComplex) 반영. (양방향 4필드만)
|
|
12
|
+
* constructionPeriod 는 일방향이므로 write-back 대상에서 제외.
|
|
13
|
+
* kpiMetrics / kpiMetricValues 는 KPI 화면이 이미 로드한 것을 그대로 사용.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncKpiToProjectInfo(project: any, kpiMetrics: any[], kpiMetricValues: any[], projectCode?: string): Promise<void>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import moment from 'moment-timezone';
|
|
2
|
+
import { calcDateDiff } from './func';
|
|
3
|
+
import { getKpiMetrics, getKpiMetricValues, updateProjectCompleteStep1, updateBuildingComplexFields } from './complete-api';
|
|
4
|
+
/**
|
|
5
|
+
* 프로젝트 정보관리 ↔ KPI 정보입력 상호 동기.
|
|
6
|
+
*
|
|
7
|
+
* - 양방향(BIDIRECTIONAL_BC_KEYS): BuildingComplex 실컬럼을 가진 4필드는 어느 화면에서
|
|
8
|
+
* 저장하든 반대편도 갱신 (last-write-wins). 두 저장 위치(BuildingComplex 컬럼 /
|
|
9
|
+
* KpiMetricValue row)는 그대로 두고 값만 맞춘다.
|
|
10
|
+
* - 일방향(constructionPeriod): 공사일정(startDate/endDate)은 KPI "계획기간"에 해당하므로
|
|
11
|
+
* 프로젝트 → KPI 단방향으로만 반영한다. (KPI 에서 일정을 역수정하지 않음)
|
|
12
|
+
*
|
|
13
|
+
* metric ↔ project 필드 매칭은 metric.name(라벨) 기준 — KPI_METRIC_KEY_MAPPING 과 동일.
|
|
14
|
+
*/
|
|
15
|
+
/** metric 라벨 ↔ 프로젝트/BuildingComplex 필드 키. (pc-tab1-plan 의 매핑과 일치) */
|
|
16
|
+
const LABEL_BY_KEY = {
|
|
17
|
+
constructionPeriod: '공사기간',
|
|
18
|
+
workerCount: '총 투입인력(M/M)',
|
|
19
|
+
floorAreaRatio: '용적율',
|
|
20
|
+
designChangeCount: '총 설계 변경 건',
|
|
21
|
+
area: '연면적'
|
|
22
|
+
};
|
|
23
|
+
/** BuildingComplex 실컬럼 보유 → 프로젝트정보 ↔ KPI 양방향 동기 대상. */
|
|
24
|
+
export const BIDIRECTIONAL_BC_KEYS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount'];
|
|
25
|
+
/** BuildingComplexPatch 에서 Int 로 정의된 컬럼 — write 시 정수화 필요. */
|
|
26
|
+
const BC_INT_KEYS = new Set(['workerCount', 'designChangeCount']);
|
|
27
|
+
function metricByProjectKey(metrics, projectKey) {
|
|
28
|
+
const label = LABEL_BY_KEY[projectKey];
|
|
29
|
+
if (!label)
|
|
30
|
+
return undefined;
|
|
31
|
+
return metrics.find((m) => m.name === label);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* metric 의 기존 KpiMetricValue row 를 찾는다. (pc-tab1-plan 렌더/upsert 규칙과 동일)
|
|
35
|
+
* - MONTH : 전월(YYYY-MM) prefix row
|
|
36
|
+
* - 그 외 : (metricId, periodType) 단일 row
|
|
37
|
+
*/
|
|
38
|
+
function findExistingRow(values, metric) {
|
|
39
|
+
if (metric.periodType === 'MONTH') {
|
|
40
|
+
const lastMonthYm = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
|
|
41
|
+
return values.find((v) => v.metricId === metric.id && v.periodType === 'MONTH' && (v.valueDate || '').startsWith(lastMonthYm));
|
|
42
|
+
}
|
|
43
|
+
return values.find((v) => v.metricId === metric.id && v.periodType === metric.periodType);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* metric + value → KpiMetricValuePatch. 기존 row 가 있으면 그 id/valueDate 를 재사용해
|
|
47
|
+
* 서버의 (domain, metric, valueDate, org) upsert 가 동일 row 를 update 하도록 한다.
|
|
48
|
+
* org 는 projectId — 서버가 프로젝트 테넌트 도메인으로 스위치하는 데 필요.
|
|
49
|
+
*/
|
|
50
|
+
function buildPatch(metric, value, existing, projectId) {
|
|
51
|
+
var _a, _b, _c, _d;
|
|
52
|
+
const today = moment().tz('Asia/Seoul');
|
|
53
|
+
const valueDate = (_a = existing === null || existing === void 0 ? void 0 : existing.valueDate) !== null && _a !== void 0 ? _a : (metric.periodType === 'MONTH'
|
|
54
|
+
? today.clone().subtract(1, 'month').format('YYYY-MM-01')
|
|
55
|
+
: today.format('YYYY-MM-DD'));
|
|
56
|
+
return {
|
|
57
|
+
id: (_b = existing === null || existing === void 0 ? void 0 : existing.id) !== null && _b !== void 0 ? _b : crypto.randomUUID(),
|
|
58
|
+
metricId: metric.id,
|
|
59
|
+
value,
|
|
60
|
+
valueDate,
|
|
61
|
+
periodType: metric.periodType,
|
|
62
|
+
unit: (_d = (_c = existing === null || existing === void 0 ? void 0 : existing.unit) !== null && _c !== void 0 ? _c : metric.unit) !== null && _d !== void 0 ? _d : '',
|
|
63
|
+
org: projectId
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 프로젝트 정보관리 저장 → KPI 기준정보 반영.
|
|
68
|
+
* - 4개 양방향 필드: BuildingComplex 값 → KpiMetricValue
|
|
69
|
+
* - constructionPeriod: startDate/endDate → ceil(기간/30) 개월 (일방향)
|
|
70
|
+
* 대상 metric 이 kpi-admin 에서 비활성/삭제/개명됐으면 매칭 실패 → 조용히 skip.
|
|
71
|
+
*/
|
|
72
|
+
export async function syncProjectInfoToKpi(project, projectCode) {
|
|
73
|
+
if (!(project === null || project === void 0 ? void 0 : project.id))
|
|
74
|
+
return;
|
|
75
|
+
const [metrics, existing] = await Promise.all([
|
|
76
|
+
getKpiMetrics(projectCode),
|
|
77
|
+
getKpiMetricValues(project.id, projectCode)
|
|
78
|
+
]);
|
|
79
|
+
const bc = project.buildingComplex || {};
|
|
80
|
+
const patches = [];
|
|
81
|
+
for (const key of BIDIRECTIONAL_BC_KEYS) {
|
|
82
|
+
const raw = bc[key];
|
|
83
|
+
if (raw === null || raw === undefined || raw === '')
|
|
84
|
+
continue;
|
|
85
|
+
const metric = metricByProjectKey(metrics, key);
|
|
86
|
+
if (!metric)
|
|
87
|
+
continue;
|
|
88
|
+
patches.push(buildPatch(metric, Number(raw), findExistingRow(existing, metric), project.id));
|
|
89
|
+
}
|
|
90
|
+
// 공사기간 계획기간 — 프로젝트 일정 → KPI 단방향
|
|
91
|
+
if (project.startDate && project.endDate) {
|
|
92
|
+
const metric = metricByProjectKey(metrics, 'constructionPeriod');
|
|
93
|
+
if (metric) {
|
|
94
|
+
const months = Math.ceil(calcDateDiff(project.startDate, project.endDate) / 30);
|
|
95
|
+
patches.push(buildPatch(metric, months, findExistingRow(existing, metric), project.id));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (patches.length) {
|
|
99
|
+
await updateProjectCompleteStep1(patches, projectCode);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* KPI 정보입력 저장 → 프로젝트 정보(BuildingComplex) 반영. (양방향 4필드만)
|
|
104
|
+
* constructionPeriod 는 일방향이므로 write-back 대상에서 제외.
|
|
105
|
+
* kpiMetrics / kpiMetricValues 는 KPI 화면이 이미 로드한 것을 그대로 사용.
|
|
106
|
+
*/
|
|
107
|
+
export async function syncKpiToProjectInfo(project, kpiMetrics, kpiMetricValues, projectCode) {
|
|
108
|
+
var _a;
|
|
109
|
+
const bcId = (_a = project === null || project === void 0 ? void 0 : project.buildingComplex) === null || _a === void 0 ? void 0 : _a.id;
|
|
110
|
+
if (!bcId)
|
|
111
|
+
return;
|
|
112
|
+
const patch = {};
|
|
113
|
+
for (const key of BIDIRECTIONAL_BC_KEYS) {
|
|
114
|
+
const metric = metricByProjectKey(kpiMetrics, key);
|
|
115
|
+
if (!metric)
|
|
116
|
+
continue;
|
|
117
|
+
const row = findExistingRow(kpiMetricValues, metric);
|
|
118
|
+
if (!row || row.value === undefined || row.value === null)
|
|
119
|
+
continue;
|
|
120
|
+
const num = Number(row.value);
|
|
121
|
+
patch[key] = BC_INT_KEYS.has(key) ? Math.round(num) : num;
|
|
122
|
+
}
|
|
123
|
+
if (Object.keys(patch).length) {
|
|
124
|
+
await updateBuildingComplexFields(bcId, patch, projectCode);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=kpi-project-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kpi-project-sync.js","sourceRoot":"","sources":["../../client/shared/kpi-project-sync.ts"],"names":[],"mappings":"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","sourcesContent":["import moment from 'moment-timezone'\n\nimport { calcDateDiff } from './func'\nimport {\n getKpiMetrics,\n getKpiMetricValues,\n updateProjectCompleteStep1,\n updateBuildingComplexFields\n} from './complete-api'\n\n/**\n * 프로젝트 정보관리 ↔ KPI 정보입력 상호 동기.\n *\n * - 양방향(BIDIRECTIONAL_BC_KEYS): BuildingComplex 실컬럼을 가진 4필드는 어느 화면에서\n * 저장하든 반대편도 갱신 (last-write-wins). 두 저장 위치(BuildingComplex 컬럼 /\n * KpiMetricValue row)는 그대로 두고 값만 맞춘다.\n * - 일방향(constructionPeriod): 공사일정(startDate/endDate)은 KPI \"계획기간\"에 해당하므로\n * 프로젝트 → KPI 단방향으로만 반영한다. (KPI 에서 일정을 역수정하지 않음)\n *\n * metric ↔ project 필드 매칭은 metric.name(라벨) 기준 — KPI_METRIC_KEY_MAPPING 과 동일.\n */\n\n/** metric 라벨 ↔ 프로젝트/BuildingComplex 필드 키. (pc-tab1-plan 의 매핑과 일치) */\nconst LABEL_BY_KEY: Record<string, string> = {\n constructionPeriod: '공사기간',\n workerCount: '총 투입인력(M/M)',\n floorAreaRatio: '용적율',\n designChangeCount: '총 설계 변경 건',\n area: '연면적'\n}\n\n/** BuildingComplex 실컬럼 보유 → 프로젝트정보 ↔ KPI 양방향 동기 대상. */\nexport const BIDIRECTIONAL_BC_KEYS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount'] as const\n\n/** BuildingComplexPatch 에서 Int 로 정의된 컬럼 — write 시 정수화 필요. */\nconst BC_INT_KEYS = new Set(['workerCount', 'designChangeCount'])\n\nfunction metricByProjectKey(metrics: any[], projectKey: string): any | undefined {\n const label = LABEL_BY_KEY[projectKey]\n if (!label) return undefined\n return metrics.find((m: any) => m.name === label)\n}\n\n/**\n * metric 의 기존 KpiMetricValue row 를 찾는다. (pc-tab1-plan 렌더/upsert 규칙과 동일)\n * - MONTH : 전월(YYYY-MM) prefix row\n * - 그 외 : (metricId, periodType) 단일 row\n */\nfunction findExistingRow(values: any[], metric: any): any | undefined {\n if (metric.periodType === 'MONTH') {\n const lastMonthYm = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM')\n return values.find(\n (v: any) => v.metricId === metric.id && v.periodType === 'MONTH' && (v.valueDate || '').startsWith(lastMonthYm)\n )\n }\n return values.find((v: any) => v.metricId === metric.id && v.periodType === metric.periodType)\n}\n\n/**\n * metric + value → KpiMetricValuePatch. 기존 row 가 있으면 그 id/valueDate 를 재사용해\n * 서버의 (domain, metric, valueDate, org) upsert 가 동일 row 를 update 하도록 한다.\n * org 는 projectId — 서버가 프로젝트 테넌트 도메인으로 스위치하는 데 필요.\n */\nfunction buildPatch(metric: any, value: number, existing: any, projectId: string): any {\n const today = moment().tz('Asia/Seoul')\n const valueDate =\n existing?.valueDate ??\n (metric.periodType === 'MONTH'\n ? today.clone().subtract(1, 'month').format('YYYY-MM-01')\n : today.format('YYYY-MM-DD'))\n\n return {\n id: existing?.id ?? crypto.randomUUID(),\n metricId: metric.id,\n value,\n valueDate,\n periodType: metric.periodType,\n unit: existing?.unit ?? metric.unit ?? '',\n org: projectId\n }\n}\n\n/**\n * 프로젝트 정보관리 저장 → KPI 기준정보 반영.\n * - 4개 양방향 필드: BuildingComplex 값 → KpiMetricValue\n * - constructionPeriod: startDate/endDate → ceil(기간/30) 개월 (일방향)\n * 대상 metric 이 kpi-admin 에서 비활성/삭제/개명됐으면 매칭 실패 → 조용히 skip.\n */\nexport async function syncProjectInfoToKpi(project: any, projectCode?: string): Promise<void> {\n if (!project?.id) return\n\n const [metrics, existing] = await Promise.all([\n getKpiMetrics(projectCode),\n getKpiMetricValues(project.id, projectCode)\n ])\n\n const bc = project.buildingComplex || {}\n const patches: any[] = []\n\n for (const key of BIDIRECTIONAL_BC_KEYS) {\n const raw = bc[key]\n if (raw === null || raw === undefined || raw === '') continue\n const metric = metricByProjectKey(metrics, key)\n if (!metric) continue\n patches.push(buildPatch(metric, Number(raw), findExistingRow(existing, metric), project.id))\n }\n\n // 공사기간 계획기간 — 프로젝트 일정 → KPI 단방향\n if (project.startDate && project.endDate) {\n const metric = metricByProjectKey(metrics, 'constructionPeriod')\n if (metric) {\n const months = Math.ceil(calcDateDiff(project.startDate, project.endDate) / 30)\n patches.push(buildPatch(metric, months, findExistingRow(existing, metric), project.id))\n }\n }\n\n if (patches.length) {\n await updateProjectCompleteStep1(patches, projectCode)\n }\n}\n\n/**\n * KPI 정보입력 저장 → 프로젝트 정보(BuildingComplex) 반영. (양방향 4필드만)\n * constructionPeriod 는 일방향이므로 write-back 대상에서 제외.\n * kpiMetrics / kpiMetricValues 는 KPI 화면이 이미 로드한 것을 그대로 사용.\n */\nexport async function syncKpiToProjectInfo(\n project: any,\n kpiMetrics: any[],\n kpiMetricValues: any[],\n projectCode?: string\n): Promise<void> {\n const bcId = project?.buildingComplex?.id\n if (!bcId) return\n\n const patch: Record<string, number> = {}\n for (const key of BIDIRECTIONAL_BC_KEYS) {\n const metric = metricByProjectKey(kpiMetrics, key)\n if (!metric) continue\n const row = findExistingRow(kpiMetricValues, metric)\n if (!row || row.value === undefined || row.value === null) continue\n const num = Number(row.value)\n patch[key] = BC_INT_KEYS.has(key) ? Math.round(num) : num\n }\n\n if (Object.keys(patch).length) {\n await updateBuildingComplexFields(bcId, patch, projectCode)\n }\n}\n"]}
|