@dssp/dkpi 1.0.0-y.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/KPI-STATISTICS-SERVICE.md +233 -0
- package/_index.html +4 -9
- package/assets/helps/index.md +1 -1
- package/assets/images/project-image.png +0 -0
- package/assets/manifest.json +2 -2
- package/config/config.development.js +2 -1
- package/config/config.production.js +2 -1
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +15 -0
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
- package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
- package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
- package/dist-client/components/kpi-boxplot-chart.js +306 -0
- package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
- package/dist-client/components/kpi-lookup-chart.js +464 -0
- package/dist-client/components/kpi-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
- package/dist-client/components/kpi-mini-trend-chart.js +180 -0
- package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
- package/dist-client/components/kpi-radar-chart.d.ts +17 -0
- package/dist-client/components/kpi-radar-chart.js +259 -0
- package/dist-client/components/kpi-radar-chart.js.map +1 -0
- package/dist-client/components/kpi-single-boxplot-chart.d.ts +14 -2
- package/dist-client/components/kpi-single-boxplot-chart.js +237 -137
- package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
- package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
- package/dist-client/components/kpi-step-lookup-chart.js +181 -0
- package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-trend-chart.d.ts +25 -0
- package/dist-client/components/kpi-trend-chart.js +241 -0
- package/dist-client/components/kpi-trend-chart.js.map +1 -0
- package/dist-client/entries/auth/checkin.d.ts +38 -0
- package/dist-client/entries/auth/checkin.js +551 -0
- package/dist-client/entries/auth/checkin.js.map +1 -0
- package/dist-client/google-map/common-google-map.d.ts +35 -0
- package/dist-client/google-map/common-google-map.js +349 -0
- package/dist-client/google-map/common-google-map.js.map +1 -0
- package/dist-client/google-map/google-map-loader.d.ts +6 -0
- package/dist-client/google-map/google-map-loader.js +23 -0
- package/dist-client/google-map/google-map-loader.js.map +1 -0
- package/dist-client/pages/component/project-update-header.d.ts +1 -0
- package/dist-client/pages/component/project-update-header.js +127 -0
- package/dist-client/pages/component/project-update-header.js.map +1 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-list-page.d.ts +22 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js +57 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js.map +1 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
- package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.d.ts +20 -0
- package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js +445 -0
- package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js.map +1 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +18 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +259 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +22 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +346 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +26 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +433 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +78 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-alert-panel.d.ts +18 -0
- package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js +131 -0
- package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +63 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +1097 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.d.ts +12 -0
- package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js +82 -0
- package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-history-viewer.d.ts +11 -0
- package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js +65 -0
- package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-list-summary.d.ts +13 -0
- package/dist-client/pages/kpi-dashboard/kpi-list-summary.js +115 -0
- package/dist-client/pages/kpi-dashboard/kpi-list-summary.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-performance-summary.d.ts +15 -0
- package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js +147 -0
- package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-value-entry.d.ts +7 -0
- package/dist-client/pages/kpi-dashboard/kpi-value-entry.js +86 -0
- package/dist-client/pages/kpi-dashboard/kpi-value-entry.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +1 -13
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +20 -11
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +1 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -2
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +22 -33
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +56 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +609 -67
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +15 -3
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +180 -65
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +13 -4
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +213 -91
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
- package/dist-client/pages/sv-project-complete.d.ts +12 -3
- package/dist-client/pages/sv-project-complete.js +175 -29
- package/dist-client/pages/sv-project-complete.js.map +1 -1
- package/dist-client/pages/sv-project-completed-list.d.ts +3 -0
- package/dist-client/pages/sv-project-completed-list.js +81 -22
- package/dist-client/pages/sv-project-completed-list.js.map +1 -1
- package/dist-client/pages/sv-project-detail.d.ts +60 -0
- package/dist-client/pages/sv-project-detail.js +1092 -85
- package/dist-client/pages/sv-project-detail.js.map +1 -1
- package/dist-client/pages/sv-project-list.d.ts +20 -2
- package/dist-client/pages/sv-project-list.js +180 -30
- package/dist-client/pages/sv-project-list.js.map +1 -1
- package/dist-client/pages/sv-project-update.d.ts +91 -0
- package/dist-client/pages/sv-project-update.js +1150 -0
- package/dist-client/pages/sv-project-update.js.map +1 -0
- package/dist-client/pages/sv-user-management.d.ts +1 -0
- package/dist-client/pages/sv-user-management.js +5 -0
- package/dist-client/pages/sv-user-management.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +29 -2
- package/dist-client/route.js.map +1 -1
- package/dist-client/shared/complete-api.d.ts +45 -5
- package/dist-client/shared/complete-api.js +177 -28
- package/dist-client/shared/complete-api.js.map +1 -1
- package/dist-client/shared/func.d.ts +1 -1
- package/dist-client/shared/func.js.map +1 -1
- package/dist-client/shared/geo-group-mapping.d.ts +25 -0
- package/dist-client/shared/geo-group-mapping.js +189 -0
- package/dist-client/shared/geo-group-mapping.js.map +1 -0
- package/dist-client/shared/integration-fetch.d.ts +35 -0
- package/dist-client/shared/integration-fetch.js +53 -0
- package/dist-client/shared/integration-fetch.js.map +1 -0
- package/dist-client/shared/kpi-project-sync.d.ts +16 -0
- package/dist-client/shared/kpi-project-sync.js +129 -0
- package/dist-client/shared/kpi-project-sync.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/menu-tools.d.ts +11 -3
- package/dist-client/viewparts/menu-tools.js +126 -75
- package/dist-client/viewparts/menu-tools.js.map +1 -1
- package/dist-server/scripts/calculate-kpi-scores.js +65 -3
- package/dist-server/scripts/calculate-kpi-scores.js.map +1 -1
- package/dist-server/scripts/load-grade-data-migration.d.ts +4 -0
- package/dist-server/scripts/load-grade-data-migration.js +95 -10
- package/dist-server/scripts/load-grade-data-migration.js.map +1 -1
- package/dist-server/scripts/propagate-parent-kpi-values.d.ts +4 -0
- package/dist-server/scripts/propagate-parent-kpi-values.js +423 -77
- package/dist-server/scripts/propagate-parent-kpi-values.js.map +1 -1
- package/dist-server/scripts/recalculate-by-project-name.d.ts +2 -0
- package/dist-server/scripts/recalculate-by-project-name.js +72 -0
- package/dist-server/scripts/recalculate-by-project-name.js.map +1 -0
- package/dist-server/service/index.d.ts +1 -3
- package/dist-server/service/index.js +3 -4
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +192 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +993 -38
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +9 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +33 -8
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
- package/dist-server/service/kpi-stat/index.d.ts +4 -0
- package/dist-server/service/kpi-stat/index.js +8 -0
- package/dist-server/service/kpi-stat/index.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js +180 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +2 -1
- package/dist-server/service/kpi-value/kpi-value-query.js +49 -6
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/kpi-module-service-tests.md +1286 -0
- package/kpi-module-test-report.md +676 -0
- package/kpi-module-unit-test-detailed-report.md +925 -0
- package/kpi-module-unit-tests-detailed.md +1452 -0
- package/openapi/unstable.yaml +2 -2
- package/package.json +58 -54
- package/recalculate-batch.sh +64 -0
- package/recalculate-projects-range.sh +98 -0
- package/schema.graphql +1159 -55
- package/things-factory.config.js +6 -1
- package/translations/en.json +5 -1
- package/translations/ko.json +5 -1
- package/views/auth-page.html +4 -5
- package/views/public/home.html +5 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"complete-api.js","sourceRoot":"","sources":["../../client/shared/complete-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,SAAiB,EAAE,EAAE;IACpD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KA0BT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;KAC7B,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAA;AAC9B,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;IACtC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;KAWT;KACF,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAA;AACvC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EAAE,SAAiB,EAAE,EAAE;IAC5D,MAAM,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IAEnE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;KAYT;QACD,SAAS,EAAE;YACT,OAAO;SACR;KACF,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAA;AAC5C,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,EAAE,OAAY,EAAE,EAAE;IAC/D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;KAMZ;QACD,SAAS,EAAE;YACT,OAAO;SACR;KACF,CAAC,CAAA;IAEF,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,IAAI,EAAE;IACzC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;KAiBT;KACF,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAA;AACpC,CAAC,CAAA","sourcesContent":["import { gql } from '@apollo/client'\nimport { client } from '@operato/graphql'\n\nexport const getProject = async (projectId: string) => {\n const response = await client.query({\n query: gql`\n query Project($id: String!) {\n project(id: $id) {\n id\n name\n startDate\n endDate\n state\n\n completeReports {\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 })\n\n if (response.errors) {\n return {}\n }\n\n return response.data.project\n}\n\nexport const getKpiMetrics = async () => {\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 }\n }\n }\n `\n })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiMetrics.items\n}\n\nexport const getKpiMetricValues = async (projectId: 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 metricId\n }\n }\n }\n `,\n variables: {\n filters\n }\n })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiMetricValues.items\n}\n\nexport const updateProjectCompleteStep1 = async (patches: any) => {\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 })\n\n return response\n}\n\nexport const getKpiCategories = async () => {\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 })\n\n if (response.errors) {\n return []\n }\n\n return response.data.kpiCategories\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!], $projectId: String) {\n kpiMetricValues(filters: $filters, projectId: $projectId) {\n items {\n id\n value\n unit\n periodType\n valueDate\n metricId\n }\n }\n }\n `,\n variables: {\n filters,\n // projectId 전달 → 서버가 그 프로젝트 도메인으로 스코프 → cross-domain 중복 row 방지.\n projectId\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"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const calcDiff: (a: string, b: string) => number;
|
|
1
|
+
export declare const calcDiff: (a: string | number, b: string | number) => number;
|
|
2
2
|
export declare const calcDateDiff: (a: string, b: string) => number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"func.js","sourceRoot":"","sources":["../../client/shared/func.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAE3B,4BAA4B;AAC5B,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"func.js","sourceRoot":"","sources":["../../client/shared/func.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAE3B,4BAA4B;AAC5B,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAkB,EAAE,CAAkB,EAAU,EAAE;IACzE,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;IAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAA;IACjC,MAAM,IAAI,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC9B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IAC7B,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,+BAA+B;AAC/B,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;IAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,CAAA;IAC5C,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,CAAA;IAC9C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE;QAAE,OAAO,CAAC,CAAA;IAEtD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9C,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAE5B,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA","sourcesContent":["import moment from 'moment'\n\n// a와 b의 차이를 계산하여 값과 클래스를 반환\nexport const calcDiff = (a: string | number, b: string | number): number => {\n const origin = Number(a || '0')\n const complete = Number(b || '0')\n const diff = complete - origin\n if (!isFinite(diff)) return 0\n return diff\n}\n\n// a와 b의 날짜 차이를 계산하여 값과 클래스를 반환\nexport const calcDateDiff = (a: string, b: string): number => {\n const origin = moment(a, 'YYYY-MM-DD', true)\n const complete = moment(b, 'YYYY-MM-DD', true)\n if (!origin.isValid() || !complete.isValid()) return 0\n\n const diffDays = complete.diff(origin, 'days')\n if (diffDays === 0) return 0\n\n return diffDays\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geo_group 2자리 코드 ↔ 시도/시군구 매핑.
|
|
3
|
+
* convert-augmented-xlsx-to-csv.py 의 GEO_GROUPS dict 를 TypeScript 로 이식.
|
|
4
|
+
*
|
|
5
|
+
* 운영자가 프로젝트 주소를 입력하면 그 안에서 시도+시군구 표준명을 부분 매칭하여
|
|
6
|
+
* 2자리 코드를 추정. 자동 채움 후에도 운영자가 수동 정정 가능.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 우편번호 prefix (2자리) → geo_group 2자리 코드 best-guess 매핑.
|
|
10
|
+
* convert-augmented-xlsx-to-csv.py 의 GEO_GROUPS 세 번째 값 (postal_prefix) 기준,
|
|
11
|
+
* 1:N 케이스는 첫 매칭 사용. 정확한 시군구 분리에는 한국 행정안전부 5자리 우편번호
|
|
12
|
+
* 룩업 필요하지만 client embed 불가능 — best-guess 로 시도 단위 통계만 신뢰 가능.
|
|
13
|
+
*/
|
|
14
|
+
export declare const POSTAL_PREFIX_TO_GEO_GROUP: Record<string, string>;
|
|
15
|
+
export declare function inferGeoGroupFromPostalCode(postalCode: string | null | undefined): string | null;
|
|
16
|
+
/** "시도+시군구 표준명" → "2자리 geo_group 코드" */
|
|
17
|
+
export declare const ADDRESS_TO_GEO_GROUP: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* 주소 문자열에서 2자리 geo_group 코드 추정. 매칭 안 되면 null.
|
|
20
|
+
* 매칭 우선:
|
|
21
|
+
* 1) 정식 시도명 + 시군구명 (예: '서울특별시 종로구')
|
|
22
|
+
* 2) 약칭 시도 + 시군구 (예: '서울 종로구' → '서울특별시 종로구')
|
|
23
|
+
* 3) 시군구명 단독 매칭 (예: '종로구') — 유니크한 경우
|
|
24
|
+
*/
|
|
25
|
+
export declare function inferGeoGroupFromAddress(address: string | null | undefined): string | null;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geo_group 2자리 코드 ↔ 시도/시군구 매핑.
|
|
3
|
+
* convert-augmented-xlsx-to-csv.py 의 GEO_GROUPS dict 를 TypeScript 로 이식.
|
|
4
|
+
*
|
|
5
|
+
* 운영자가 프로젝트 주소를 입력하면 그 안에서 시도+시군구 표준명을 부분 매칭하여
|
|
6
|
+
* 2자리 코드를 추정. 자동 채움 후에도 운영자가 수동 정정 가능.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 우편번호 prefix (2자리) → geo_group 2자리 코드 best-guess 매핑.
|
|
10
|
+
* convert-augmented-xlsx-to-csv.py 의 GEO_GROUPS 세 번째 값 (postal_prefix) 기준,
|
|
11
|
+
* 1:N 케이스는 첫 매칭 사용. 정확한 시군구 분리에는 한국 행정안전부 5자리 우편번호
|
|
12
|
+
* 룩업 필요하지만 client embed 불가능 — best-guess 로 시도 단위 통계만 신뢰 가능.
|
|
13
|
+
*/
|
|
14
|
+
export const POSTAL_PREFIX_TO_GEO_GROUP = {
|
|
15
|
+
'03': '01', // 서울 종로구
|
|
16
|
+
'04': '02', // 서울 중구
|
|
17
|
+
'05': '05', // 서울 광진구
|
|
18
|
+
'06': '06', // 서울 강남구
|
|
19
|
+
'07': '08', // 서울 강서구
|
|
20
|
+
'10': '12', // 경기 고양시 / 파주시
|
|
21
|
+
'11': '11', // 경기 성남시
|
|
22
|
+
'12': '11', // 경기 성남시
|
|
23
|
+
'13': '11', // 경기 성남시
|
|
24
|
+
'14': '14', // 경기 부천시
|
|
25
|
+
'15': '15', // 경기 안산시
|
|
26
|
+
'16': '10', // 경기 수원시
|
|
27
|
+
'17': '13', // 경기 용인시 / 평택시
|
|
28
|
+
'18': '16', // 경기 화성시
|
|
29
|
+
'21': '21', // 인천 남동구 / 부평구
|
|
30
|
+
'22': '23', // 인천 서구
|
|
31
|
+
'24': '24', // 강원 춘천시
|
|
32
|
+
'25': '26', // 강원 강릉시
|
|
33
|
+
'26': '25', // 강원 원주시
|
|
34
|
+
'27': '28', // 충북 충주시 / 제천시
|
|
35
|
+
'28': '27', // 충북 청주시
|
|
36
|
+
'30': '30', // 세종
|
|
37
|
+
'31': '31', // 충남 천안시 / 아산시
|
|
38
|
+
'34': '35', // 대전 유성구
|
|
39
|
+
'35': '34', // 대전 서구
|
|
40
|
+
'36': '39', // 경북 안동시
|
|
41
|
+
'37': '36', // 경북 포항시
|
|
42
|
+
'38': '37', // 경북 경주시
|
|
43
|
+
'39': '38', // 경북 구미시 / 김천시
|
|
44
|
+
'41': '41', // 대구 중구
|
|
45
|
+
'42': '42', // 대구 수성구 / 달서구
|
|
46
|
+
'44': '44', // 울산 남구 / 중구
|
|
47
|
+
'46': '49', // 부산 금정구
|
|
48
|
+
'48': '46', // 부산 중구 / 해운대구
|
|
49
|
+
'49': '48', // 부산 사하구
|
|
50
|
+
'50': '51', // 경남 김해시 / 양산시
|
|
51
|
+
'51': '50', // 경남 창원시
|
|
52
|
+
'52': '52', // 경남 진주시
|
|
53
|
+
'54': '54', // 전북 전주시 / 익산시 / 군산시
|
|
54
|
+
'57': '59', // 전남 순천시
|
|
55
|
+
'58': '57', // 전남 목포시
|
|
56
|
+
'59': '58', // 전남 여수시
|
|
57
|
+
'61': '61', // 광주 서구 / 북구
|
|
58
|
+
'63': '63' // 제주
|
|
59
|
+
};
|
|
60
|
+
export function inferGeoGroupFromPostalCode(postalCode) {
|
|
61
|
+
if (!postalCode)
|
|
62
|
+
return null;
|
|
63
|
+
const prefix = postalCode.trim().slice(0, 2);
|
|
64
|
+
return POSTAL_PREFIX_TO_GEO_GROUP[prefix] || null;
|
|
65
|
+
}
|
|
66
|
+
/** "시도+시군구 표준명" → "2자리 geo_group 코드" */
|
|
67
|
+
export const ADDRESS_TO_GEO_GROUP = {
|
|
68
|
+
'서울특별시 종로구': '01',
|
|
69
|
+
'서울특별시 중구': '02',
|
|
70
|
+
'서울특별시 용산구': '03',
|
|
71
|
+
'서울특별시 성동구': '04',
|
|
72
|
+
'서울특별시 광진구': '05',
|
|
73
|
+
'서울특별시 강남구': '06',
|
|
74
|
+
'서울특별시 송파구': '07',
|
|
75
|
+
'서울특별시 강서구': '08',
|
|
76
|
+
'경기도 수원시': '10',
|
|
77
|
+
'경기도 성남시': '11',
|
|
78
|
+
'경기도 고양시': '12',
|
|
79
|
+
'경기도 용인시': '13',
|
|
80
|
+
'경기도 부천시': '14',
|
|
81
|
+
'경기도 안산시': '15',
|
|
82
|
+
'경기도 화성시': '16',
|
|
83
|
+
'경기도 평택시': '17',
|
|
84
|
+
'경기도 파주시': '18',
|
|
85
|
+
'인천광역시 남동구': '21',
|
|
86
|
+
'인천광역시 부평구': '22',
|
|
87
|
+
'인천광역시 서구': '23',
|
|
88
|
+
'강원도 춘천시': '24',
|
|
89
|
+
'강원도 원주시': '25',
|
|
90
|
+
'강원도 강릉시': '26',
|
|
91
|
+
'충청북도 청주시': '27',
|
|
92
|
+
'충청북도 충주시': '28',
|
|
93
|
+
'충청북도 제천시': '29',
|
|
94
|
+
'세종특별자치시': '30',
|
|
95
|
+
'충청남도 천안시': '31',
|
|
96
|
+
'충청남도 아산시': '32',
|
|
97
|
+
'강원도 속초시': '33',
|
|
98
|
+
'대전광역시 서구': '34',
|
|
99
|
+
'대전광역시 유성구': '35',
|
|
100
|
+
'경상북도 포항시': '36',
|
|
101
|
+
'경상북도 경주시': '37',
|
|
102
|
+
'경상북도 구미시': '38',
|
|
103
|
+
'경상북도 안동시': '39',
|
|
104
|
+
'경상북도 김천시': '40',
|
|
105
|
+
'대구광역시 중구': '41',
|
|
106
|
+
'대구광역시 수성구': '42',
|
|
107
|
+
'대구광역시 달서구': '43',
|
|
108
|
+
'울산광역시 남구': '44',
|
|
109
|
+
'울산광역시 중구': '45',
|
|
110
|
+
'부산광역시 중구': '46',
|
|
111
|
+
'부산광역시 해운대구': '47',
|
|
112
|
+
'부산광역시 사하구': '48',
|
|
113
|
+
'부산광역시 금정구': '49',
|
|
114
|
+
'경상남도 창원시': '50',
|
|
115
|
+
'경상남도 김해시': '51',
|
|
116
|
+
'경상남도 진주시': '52',
|
|
117
|
+
'경상남도 양산시': '53',
|
|
118
|
+
'전라북도 전주시': '54',
|
|
119
|
+
'전라북도 익산시': '55',
|
|
120
|
+
'전라북도 군산시': '56',
|
|
121
|
+
'전라남도 목포시': '57',
|
|
122
|
+
'전라남도 여수시': '58',
|
|
123
|
+
'전라남도 순천시': '59',
|
|
124
|
+
'광주광역시 서구': '61',
|
|
125
|
+
'광주광역시 북구': '62',
|
|
126
|
+
'제주특별자치도 제주시': '63'
|
|
127
|
+
};
|
|
128
|
+
/** 시도 약칭 → 정식 명 (부분 매칭용). */
|
|
129
|
+
const PROVINCE_ALIASES = {
|
|
130
|
+
서울: '서울특별시',
|
|
131
|
+
경기: '경기도',
|
|
132
|
+
인천: '인천광역시',
|
|
133
|
+
강원: '강원도',
|
|
134
|
+
충북: '충청북도',
|
|
135
|
+
충남: '충청남도',
|
|
136
|
+
세종: '세종특별자치시',
|
|
137
|
+
대전: '대전광역시',
|
|
138
|
+
경북: '경상북도',
|
|
139
|
+
경남: '경상남도',
|
|
140
|
+
대구: '대구광역시',
|
|
141
|
+
울산: '울산광역시',
|
|
142
|
+
부산: '부산광역시',
|
|
143
|
+
전북: '전라북도',
|
|
144
|
+
전남: '전라남도',
|
|
145
|
+
광주: '광주광역시',
|
|
146
|
+
제주: '제주특별자치도'
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* 주소 문자열에서 2자리 geo_group 코드 추정. 매칭 안 되면 null.
|
|
150
|
+
* 매칭 우선:
|
|
151
|
+
* 1) 정식 시도명 + 시군구명 (예: '서울특별시 종로구')
|
|
152
|
+
* 2) 약칭 시도 + 시군구 (예: '서울 종로구' → '서울특별시 종로구')
|
|
153
|
+
* 3) 시군구명 단독 매칭 (예: '종로구') — 유니크한 경우
|
|
154
|
+
*/
|
|
155
|
+
export function inferGeoGroupFromAddress(address) {
|
|
156
|
+
if (!address)
|
|
157
|
+
return null;
|
|
158
|
+
const trimmed = address.trim();
|
|
159
|
+
if (!trimmed)
|
|
160
|
+
return null;
|
|
161
|
+
// 1) 정식 매칭
|
|
162
|
+
for (const key of Object.keys(ADDRESS_TO_GEO_GROUP)) {
|
|
163
|
+
if (trimmed.includes(key))
|
|
164
|
+
return ADDRESS_TO_GEO_GROUP[key];
|
|
165
|
+
}
|
|
166
|
+
// 2) 약칭 시도 → 정식 시도로 expand 후 다시 매칭
|
|
167
|
+
for (const [short, full] of Object.entries(PROVINCE_ALIASES)) {
|
|
168
|
+
if (trimmed.startsWith(short) && !trimmed.startsWith(full)) {
|
|
169
|
+
const expanded = trimmed.replace(short, full);
|
|
170
|
+
for (const key of Object.keys(ADDRESS_TO_GEO_GROUP)) {
|
|
171
|
+
if (expanded.includes(key))
|
|
172
|
+
return ADDRESS_TO_GEO_GROUP[key];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 3) 시군구명 단독 매칭 (시도 prefix 없는 경우). 유니크한 시군구만 통과.
|
|
177
|
+
const districtMatches = Object.entries(ADDRESS_TO_GEO_GROUP)
|
|
178
|
+
.filter(([key]) => {
|
|
179
|
+
const parts = key.split(' ');
|
|
180
|
+
const district = parts[parts.length - 1];
|
|
181
|
+
return trimmed.includes(district);
|
|
182
|
+
})
|
|
183
|
+
.map(([, code]) => code);
|
|
184
|
+
const uniqueDistrictCodes = Array.from(new Set(districtMatches));
|
|
185
|
+
if (uniqueDistrictCodes.length === 1)
|
|
186
|
+
return uniqueDistrictCodes[0];
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=geo-group-mapping.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo-group-mapping.js","sourceRoot":"","sources":["../../client/shared/geo-group-mapping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * geo_group 2자리 코드 ↔ 시도/시군구 매핑.\n * convert-augmented-xlsx-to-csv.py 의 GEO_GROUPS dict 를 TypeScript 로 이식.\n *\n * 운영자가 프로젝트 주소를 입력하면 그 안에서 시도+시군구 표준명을 부분 매칭하여\n * 2자리 코드를 추정. 자동 채움 후에도 운영자가 수동 정정 가능.\n */\n\n/**\n * 우편번호 prefix (2자리) → geo_group 2자리 코드 best-guess 매핑.\n * convert-augmented-xlsx-to-csv.py 의 GEO_GROUPS 세 번째 값 (postal_prefix) 기준,\n * 1:N 케이스는 첫 매칭 사용. 정확한 시군구 분리에는 한국 행정안전부 5자리 우편번호\n * 룩업 필요하지만 client embed 불가능 — best-guess 로 시도 단위 통계만 신뢰 가능.\n */\nexport const POSTAL_PREFIX_TO_GEO_GROUP: Record<string, string> = {\n '03': '01', // 서울 종로구\n '04': '02', // 서울 중구\n '05': '05', // 서울 광진구\n '06': '06', // 서울 강남구\n '07': '08', // 서울 강서구\n '10': '12', // 경기 고양시 / 파주시\n '11': '11', // 경기 성남시\n '12': '11', // 경기 성남시\n '13': '11', // 경기 성남시\n '14': '14', // 경기 부천시\n '15': '15', // 경기 안산시\n '16': '10', // 경기 수원시\n '17': '13', // 경기 용인시 / 평택시\n '18': '16', // 경기 화성시\n '21': '21', // 인천 남동구 / 부평구\n '22': '23', // 인천 서구\n '24': '24', // 강원 춘천시\n '25': '26', // 강원 강릉시\n '26': '25', // 강원 원주시\n '27': '28', // 충북 충주시 / 제천시\n '28': '27', // 충북 청주시\n '30': '30', // 세종\n '31': '31', // 충남 천안시 / 아산시\n '34': '35', // 대전 유성구\n '35': '34', // 대전 서구\n '36': '39', // 경북 안동시\n '37': '36', // 경북 포항시\n '38': '37', // 경북 경주시\n '39': '38', // 경북 구미시 / 김천시\n '41': '41', // 대구 중구\n '42': '42', // 대구 수성구 / 달서구\n '44': '44', // 울산 남구 / 중구\n '46': '49', // 부산 금정구\n '48': '46', // 부산 중구 / 해운대구\n '49': '48', // 부산 사하구\n '50': '51', // 경남 김해시 / 양산시\n '51': '50', // 경남 창원시\n '52': '52', // 경남 진주시\n '54': '54', // 전북 전주시 / 익산시 / 군산시\n '57': '59', // 전남 순천시\n '58': '57', // 전남 목포시\n '59': '58', // 전남 여수시\n '61': '61', // 광주 서구 / 북구\n '63': '63' // 제주\n}\n\nexport function inferGeoGroupFromPostalCode(postalCode: string | null | undefined): string | null {\n if (!postalCode) return null\n const prefix = postalCode.trim().slice(0, 2)\n return POSTAL_PREFIX_TO_GEO_GROUP[prefix] || null\n}\n\n/** \"시도+시군구 표준명\" → \"2자리 geo_group 코드\" */\nexport const ADDRESS_TO_GEO_GROUP: Record<string, string> = {\n '서울특별시 종로구': '01',\n '서울특별시 중구': '02',\n '서울특별시 용산구': '03',\n '서울특별시 성동구': '04',\n '서울특별시 광진구': '05',\n '서울특별시 강남구': '06',\n '서울특별시 송파구': '07',\n '서울특별시 강서구': '08',\n '경기도 수원시': '10',\n '경기도 성남시': '11',\n '경기도 고양시': '12',\n '경기도 용인시': '13',\n '경기도 부천시': '14',\n '경기도 안산시': '15',\n '경기도 화성시': '16',\n '경기도 평택시': '17',\n '경기도 파주시': '18',\n '인천광역시 남동구': '21',\n '인천광역시 부평구': '22',\n '인천광역시 서구': '23',\n '강원도 춘천시': '24',\n '강원도 원주시': '25',\n '강원도 강릉시': '26',\n '충청북도 청주시': '27',\n '충청북도 충주시': '28',\n '충청북도 제천시': '29',\n '세종특별자치시': '30',\n '충청남도 천안시': '31',\n '충청남도 아산시': '32',\n '강원도 속초시': '33',\n '대전광역시 서구': '34',\n '대전광역시 유성구': '35',\n '경상북도 포항시': '36',\n '경상북도 경주시': '37',\n '경상북도 구미시': '38',\n '경상북도 안동시': '39',\n '경상북도 김천시': '40',\n '대구광역시 중구': '41',\n '대구광역시 수성구': '42',\n '대구광역시 달서구': '43',\n '울산광역시 남구': '44',\n '울산광역시 중구': '45',\n '부산광역시 중구': '46',\n '부산광역시 해운대구': '47',\n '부산광역시 사하구': '48',\n '부산광역시 금정구': '49',\n '경상남도 창원시': '50',\n '경상남도 김해시': '51',\n '경상남도 진주시': '52',\n '경상남도 양산시': '53',\n '전라북도 전주시': '54',\n '전라북도 익산시': '55',\n '전라북도 군산시': '56',\n '전라남도 목포시': '57',\n '전라남도 여수시': '58',\n '전라남도 순천시': '59',\n '광주광역시 서구': '61',\n '광주광역시 북구': '62',\n '제주특별자치도 제주시': '63'\n}\n\n/** 시도 약칭 → 정식 명 (부분 매칭용). */\nconst PROVINCE_ALIASES: Record<string, string> = {\n 서울: '서울특별시',\n 경기: '경기도',\n 인천: '인천광역시',\n 강원: '강원도',\n 충북: '충청북도',\n 충남: '충청남도',\n 세종: '세종특별자치시',\n 대전: '대전광역시',\n 경북: '경상북도',\n 경남: '경상남도',\n 대구: '대구광역시',\n 울산: '울산광역시',\n 부산: '부산광역시',\n 전북: '전라북도',\n 전남: '전라남도',\n 광주: '광주광역시',\n 제주: '제주특별자치도'\n}\n\n/**\n * 주소 문자열에서 2자리 geo_group 코드 추정. 매칭 안 되면 null.\n * 매칭 우선:\n * 1) 정식 시도명 + 시군구명 (예: '서울특별시 종로구')\n * 2) 약칭 시도 + 시군구 (예: '서울 종로구' → '서울특별시 종로구')\n * 3) 시군구명 단독 매칭 (예: '종로구') — 유니크한 경우\n */\nexport function inferGeoGroupFromAddress(address: string | null | undefined): string | null {\n if (!address) return null\n const trimmed = address.trim()\n if (!trimmed) return null\n\n // 1) 정식 매칭\n for (const key of Object.keys(ADDRESS_TO_GEO_GROUP)) {\n if (trimmed.includes(key)) return ADDRESS_TO_GEO_GROUP[key]\n }\n\n // 2) 약칭 시도 → 정식 시도로 expand 후 다시 매칭\n for (const [short, full] of Object.entries(PROVINCE_ALIASES)) {\n if (trimmed.startsWith(short) && !trimmed.startsWith(full)) {\n const expanded = trimmed.replace(short, full)\n for (const key of Object.keys(ADDRESS_TO_GEO_GROUP)) {\n if (expanded.includes(key)) return ADDRESS_TO_GEO_GROUP[key]\n }\n }\n }\n\n // 3) 시군구명 단독 매칭 (시도 prefix 없는 경우). 유니크한 시군구만 통과.\n const districtMatches = Object.entries(ADDRESS_TO_GEO_GROUP)\n .filter(([key]) => {\n const parts = key.split(' ')\n const district = parts[parts.length - 1]\n return trimmed.includes(district)\n })\n .map(([, code]) => code)\n const uniqueDistrictCodes = Array.from(new Set(districtMatches))\n if (uniqueDistrictCodes.length === 1) return uniqueDistrictCodes[0]\n\n return null\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 완공 입력 화면의 "자동 수집" 워크플로용 외부 시스템 연동 수집 함수.
|
|
3
|
+
*
|
|
4
|
+
* 데모 목적: 키스콘·세움터·올바로에서 "연동을 통해 값을 가져온다"는 워크플로를
|
|
5
|
+
* 보여주기 위한 것. 실제 스크래핑/API 연동의 깊이는 이 함수 내부에 캡슐화되어
|
|
6
|
+
* 있으므로, 추후 실연동(integration-kiscon / data-go-kr / integration-allbaro)
|
|
7
|
+
* 으로 교체할 때 이 본문만 바꾸면 화면/저장 흐름은 그대로 유지된다.
|
|
8
|
+
*/
|
|
9
|
+
export type IntegrationSource = 'kiscon' | 'saeumteo' | 'allbaro';
|
|
10
|
+
export interface IntegrationSourceMeta {
|
|
11
|
+
source: IntegrationSource;
|
|
12
|
+
label: string;
|
|
13
|
+
icon: string;
|
|
14
|
+
desc: string;
|
|
15
|
+
}
|
|
16
|
+
/** 화면에 표시할 연동 소스 목록 (수집 순서대로) */
|
|
17
|
+
export declare const INTEGRATION_SOURCES: IntegrationSourceMeta[];
|
|
18
|
+
export interface CollectedField {
|
|
19
|
+
/** 폼의 projectKey (KPI_METRIC_KEY_MAPPING 의 projectKey 와 일치) */
|
|
20
|
+
projectKey: string;
|
|
21
|
+
label: string;
|
|
22
|
+
value: number | string;
|
|
23
|
+
unit?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CollectionResult {
|
|
26
|
+
source: IntegrationSource;
|
|
27
|
+
label: string;
|
|
28
|
+
fields: CollectedField[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 한 외부 시스템에서 연동으로 값을 수집한다.
|
|
32
|
+
* 데모에서는 연동 수집을 흉내내기 위해 짧은 지연 후 대표값을 반환한다
|
|
33
|
+
* (프로젝트에 이미 있는 값이 있으면 우선 사용).
|
|
34
|
+
*/
|
|
35
|
+
export declare function collectFromSource(source: IntegrationSource, project: any): Promise<CollectionResult>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 완공 입력 화면의 "자동 수집" 워크플로용 외부 시스템 연동 수집 함수.
|
|
3
|
+
*
|
|
4
|
+
* 데모 목적: 키스콘·세움터·올바로에서 "연동을 통해 값을 가져온다"는 워크플로를
|
|
5
|
+
* 보여주기 위한 것. 실제 스크래핑/API 연동의 깊이는 이 함수 내부에 캡슐화되어
|
|
6
|
+
* 있으므로, 추후 실연동(integration-kiscon / data-go-kr / integration-allbaro)
|
|
7
|
+
* 으로 교체할 때 이 본문만 바꾸면 화면/저장 흐름은 그대로 유지된다.
|
|
8
|
+
*/
|
|
9
|
+
/** 화면에 표시할 연동 소스 목록 (수집 순서대로) */
|
|
10
|
+
export const INTEGRATION_SOURCES = [
|
|
11
|
+
{ source: 'kiscon', label: '키스콘', icon: '🏗', desc: '건설공사대장 통보' },
|
|
12
|
+
{ source: 'saeumteo', label: '세움터', icon: '🏛', desc: '건축물대장 / 건축HUB' },
|
|
13
|
+
{ source: 'allbaro', label: '올바로', icon: '♻', desc: '건설폐기물 처리실적' }
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* 한 외부 시스템에서 연동으로 값을 수집한다.
|
|
17
|
+
* 데모에서는 연동 수집을 흉내내기 위해 짧은 지연 후 대표값을 반환한다
|
|
18
|
+
* (프로젝트에 이미 있는 값이 있으면 우선 사용).
|
|
19
|
+
*/
|
|
20
|
+
export async function collectFromSource(source, project) {
|
|
21
|
+
// 연동 수집이 "진행되는" 느낌을 주기 위한 짧은 지연 (데모용)
|
|
22
|
+
await new Promise(r => setTimeout(r, 700));
|
|
23
|
+
const bc = (project === null || project === void 0 ? void 0 : project.buildingComplex) || {};
|
|
24
|
+
switch (source) {
|
|
25
|
+
case 'kiscon':
|
|
26
|
+
return {
|
|
27
|
+
source,
|
|
28
|
+
label: '키스콘',
|
|
29
|
+
fields: [
|
|
30
|
+
{ projectKey: 'constructionPeriod', label: '계획공사기간', value: 30, unit: '개월' },
|
|
31
|
+
{ projectKey: 'constructionCost', label: '계획공사비', value: bc.constructionCost || 2500, unit: '억원' }
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
case 'saeumteo':
|
|
35
|
+
return {
|
|
36
|
+
source,
|
|
37
|
+
label: '세움터',
|
|
38
|
+
fields: [
|
|
39
|
+
{ projectKey: 'area', label: '연면적', value: bc.area || 114000, unit: '㎡' },
|
|
40
|
+
{ projectKey: 'floorAreaRatio', label: '용적률', value: bc.floorAreaRatio || 250, unit: '%' },
|
|
41
|
+
{ projectKey: 'upperFloorCount', label: '지상층수', value: 25, unit: '층' },
|
|
42
|
+
{ projectKey: 'lowerFloorCount', label: '지하층수', value: 3, unit: '층' }
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
case 'allbaro':
|
|
46
|
+
return {
|
|
47
|
+
source,
|
|
48
|
+
label: '올바로',
|
|
49
|
+
fields: [{ projectKey: 'totalConstructionWasteAmount', label: '총 건설폐기물', value: 15, unit: 'ton' }]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=integration-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration-fetch.js","sourceRoot":"","sources":["../../client/shared/integration-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,iCAAiC;AACjC,MAAM,CAAC,MAAM,mBAAmB,GAA4B;IAC1D,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE;IACjE,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE;IACvE,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE;CACnE,CAAA;AAgBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAyB,EAAE,OAAY;IAC7E,sCAAsC;IACtC,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IAE1C,MAAM,EAAE,GAAG,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,eAAe,KAAI,EAAE,CAAA;IAEzC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,QAAQ;YACX,OAAO;gBACL,MAAM;gBACN,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE;oBACN,EAAE,UAAU,EAAE,oBAAoB,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;oBAC5E,EAAE,UAAU,EAAE,kBAAkB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,gBAAgB,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;iBACnG;aACF,CAAA;QAEH,KAAK,UAAU;YACb,OAAO;gBACL,MAAM;gBACN,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE;oBACN,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,IAAI,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE;oBACzE,EAAE,UAAU,EAAE,gBAAgB,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,cAAc,IAAI,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE;oBAC1F,EAAE,UAAU,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;oBACtE,EAAE,UAAU,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE;iBACtE;aACF,CAAA;QAEH,KAAK,SAAS;YACZ,OAAO;gBACL,MAAM;gBACN,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,8BAA8B,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;aACnG,CAAA;IACL,CAAC;AACH,CAAC","sourcesContent":["/**\n * 완공 입력 화면의 \"자동 수집\" 워크플로용 외부 시스템 연동 수집 함수.\n *\n * 데모 목적: 키스콘·세움터·올바로에서 \"연동을 통해 값을 가져온다\"는 워크플로를\n * 보여주기 위한 것. 실제 스크래핑/API 연동의 깊이는 이 함수 내부에 캡슐화되어\n * 있으므로, 추후 실연동(integration-kiscon / data-go-kr / integration-allbaro)\n * 으로 교체할 때 이 본문만 바꾸면 화면/저장 흐름은 그대로 유지된다.\n */\n\nexport type IntegrationSource = 'kiscon' | 'saeumteo' | 'allbaro'\n\nexport interface IntegrationSourceMeta {\n source: IntegrationSource\n label: string\n icon: string\n desc: string\n}\n\n/** 화면에 표시할 연동 소스 목록 (수집 순서대로) */\nexport const INTEGRATION_SOURCES: IntegrationSourceMeta[] = [\n { source: 'kiscon', label: '키스콘', icon: '🏗', desc: '건설공사대장 통보' },\n { source: 'saeumteo', label: '세움터', icon: '🏛', desc: '건축물대장 / 건축HUB' },\n { source: 'allbaro', label: '올바로', icon: '♻', desc: '건설폐기물 처리실적' }\n]\n\nexport interface CollectedField {\n /** 폼의 projectKey (KPI_METRIC_KEY_MAPPING 의 projectKey 와 일치) */\n projectKey: string\n label: string\n value: number | string\n unit?: string\n}\n\nexport interface CollectionResult {\n source: IntegrationSource\n label: string\n fields: CollectedField[]\n}\n\n/**\n * 한 외부 시스템에서 연동으로 값을 수집한다.\n * 데모에서는 연동 수집을 흉내내기 위해 짧은 지연 후 대표값을 반환한다\n * (프로젝트에 이미 있는 값이 있으면 우선 사용).\n */\nexport async function collectFromSource(source: IntegrationSource, project: any): Promise<CollectionResult> {\n // 연동 수집이 \"진행되는\" 느낌을 주기 위한 짧은 지연 (데모용)\n await new Promise(r => setTimeout(r, 700))\n\n const bc = project?.buildingComplex || {}\n\n switch (source) {\n case 'kiscon':\n return {\n source,\n label: '키스콘',\n fields: [\n { projectKey: 'constructionPeriod', label: '계획공사기간', value: 30, unit: '개월' },\n { projectKey: 'constructionCost', label: '계획공사비', value: bc.constructionCost || 2500, unit: '억원' }\n ]\n }\n\n case 'saeumteo':\n return {\n source,\n label: '세움터',\n fields: [\n { projectKey: 'area', label: '연면적', value: bc.area || 114000, unit: '㎡' },\n { projectKey: 'floorAreaRatio', label: '용적률', value: bc.floorAreaRatio || 250, unit: '%' },\n { projectKey: 'upperFloorCount', label: '지상층수', value: 25, unit: '층' },\n { projectKey: 'lowerFloorCount', label: '지하층수', value: 3, unit: '층' }\n ]\n }\n\n case 'allbaro':\n return {\n source,\n label: '올바로',\n fields: [{ projectKey: 'totalConstructionWasteAmount', label: '총 건설폐기물', value: 15, unit: 'ton' }]\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** BuildingComplex 실컬럼 보유 → 프로젝트정보 ↔ KPI 양방향 동기 대상.
|
|
2
|
+
* (constructionCost 는 억원 소수 가능 → 정수화 대상 아님) */
|
|
3
|
+
export declare const BIDIRECTIONAL_BC_KEYS: readonly ["workerCount", "area", "floorAreaRatio", "designChangeCount", "constructionCost"];
|
|
4
|
+
/**
|
|
5
|
+
* 프로젝트 정보관리 저장 → KPI 기준정보 반영.
|
|
6
|
+
* - 4개 양방향 필드: BuildingComplex 값 → KpiMetricValue
|
|
7
|
+
* - constructionPeriod: startDate/endDate → ceil(기간/30) 개월 (일방향)
|
|
8
|
+
* 대상 metric 이 kpi-admin 에서 비활성/삭제/개명됐으면 매칭 실패 → 조용히 skip.
|
|
9
|
+
*/
|
|
10
|
+
export declare function syncProjectInfoToKpi(project: any, projectCode?: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* KPI 정보입력 저장 → 프로젝트 정보(BuildingComplex) 반영. (양방향 4필드만)
|
|
13
|
+
* constructionPeriod 는 일방향이므로 write-back 대상에서 제외.
|
|
14
|
+
* kpiMetrics / kpiMetricValues 는 KPI 화면이 이미 로드한 것을 그대로 사용.
|
|
15
|
+
*/
|
|
16
|
+
export declare function syncKpiToProjectInfo(project: any, kpiMetrics: any[], kpiMetricValues: any[], projectCode?: string): Promise<void>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import moment from 'moment-timezone';
|
|
2
|
+
import { getKpiMetrics, getKpiMetricValues, updateProjectCompleteStep1, updateBuildingComplexFields } from './complete-api';
|
|
3
|
+
/**
|
|
4
|
+
* 프로젝트 정보관리 ↔ KPI 정보입력 상호 동기.
|
|
5
|
+
*
|
|
6
|
+
* - 양방향(BIDIRECTIONAL_BC_KEYS): BuildingComplex 실컬럼을 가진 5필드(투입인력/연면적/
|
|
7
|
+
* 용적률/설계변경건/계획공사비)는 어느 화면에서 저장하든 반대편도 갱신 (last-write-wins).
|
|
8
|
+
* 두 저장 위치(BuildingComplex 컬럼 / KpiMetricValue row)는 그대로 두고 값만 맞춘다.
|
|
9
|
+
* - 일방향(constructionPeriod): 공사일정(startDate/endDate)은 KPI "계획기간"에 해당하므로
|
|
10
|
+
* 프로젝트 → KPI 단방향으로만 반영한다. (KPI 에서 일정을 역수정하지 않음)
|
|
11
|
+
*
|
|
12
|
+
* metric ↔ project 필드 매칭은 metric.name(라벨) 기준 — KPI_METRIC_KEY_MAPPING 과 동일.
|
|
13
|
+
*/
|
|
14
|
+
/** metric 라벨 ↔ 프로젝트/BuildingComplex 필드 키. 라벨은 실제 DB KpiMetric.name 과 일치해야 함.
|
|
15
|
+
* (DB 확인: 계획공사비/계획공사기간/총 투입인력/용적률/총 설계 변경 건/연면적 존재) */
|
|
16
|
+
const LABEL_BY_KEY = {
|
|
17
|
+
constructionPeriod: '계획공사기간',
|
|
18
|
+
constructionCost: '계획공사비',
|
|
19
|
+
workerCount: '총 투입인력',
|
|
20
|
+
floorAreaRatio: '용적률',
|
|
21
|
+
designChangeCount: '총 설계 변경 건',
|
|
22
|
+
area: '연면적'
|
|
23
|
+
};
|
|
24
|
+
/** BuildingComplex 실컬럼 보유 → 프로젝트정보 ↔ KPI 양방향 동기 대상.
|
|
25
|
+
* (constructionCost 는 억원 소수 가능 → 정수화 대상 아님) */
|
|
26
|
+
export const BIDIRECTIONAL_BC_KEYS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount', 'constructionCost'];
|
|
27
|
+
/** BuildingComplexPatch 에서 Int 로 정의된 컬럼 — write 시 정수화 필요. */
|
|
28
|
+
const BC_INT_KEYS = new Set(['workerCount', 'designChangeCount']);
|
|
29
|
+
function metricByProjectKey(metrics, projectKey) {
|
|
30
|
+
const label = LABEL_BY_KEY[projectKey];
|
|
31
|
+
if (!label)
|
|
32
|
+
return undefined;
|
|
33
|
+
return metrics.find((m) => m.name === label);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* metric 의 기존 KpiMetricValue row 를 찾는다. (pc-tab1-plan 렌더/upsert 규칙과 동일)
|
|
37
|
+
* - MONTH : 전월(YYYY-MM) prefix row
|
|
38
|
+
* - 그 외 : (metricId, periodType) 단일 row
|
|
39
|
+
*/
|
|
40
|
+
function findExistingRow(values, metric) {
|
|
41
|
+
if (metric.periodType === 'MONTH') {
|
|
42
|
+
const lastMonthYm = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
|
|
43
|
+
return values.find((v) => v.metricId === metric.id && v.periodType === 'MONTH' && (v.valueDate || '').startsWith(lastMonthYm));
|
|
44
|
+
}
|
|
45
|
+
return values.find((v) => v.metricId === metric.id && v.periodType === metric.periodType);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* metric + value → KpiMetricValuePatch. 기존 row 가 있으면 그 id/valueDate 를 재사용해
|
|
49
|
+
* 서버의 (domain, metric, valueDate, org) upsert 가 동일 row 를 update 하도록 한다.
|
|
50
|
+
* org 는 projectId — 서버가 프로젝트 테넌트 도메인으로 스위치하는 데 필요.
|
|
51
|
+
*/
|
|
52
|
+
function buildPatch(metric, value, existing, projectId) {
|
|
53
|
+
var _a, _b, _c, _d;
|
|
54
|
+
const today = moment().tz('Asia/Seoul');
|
|
55
|
+
const valueDate = (_a = existing === null || existing === void 0 ? void 0 : existing.valueDate) !== null && _a !== void 0 ? _a : (metric.periodType === 'MONTH' ? today.clone().subtract(1, 'month').format('YYYY-MM-01') : 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([getKpiMetrics(projectCode), getKpiMetricValues(project.id, projectCode)]);
|
|
76
|
+
const bc = project.buildingComplex || {};
|
|
77
|
+
const patches = [];
|
|
78
|
+
for (const key of BIDIRECTIONAL_BC_KEYS) {
|
|
79
|
+
const raw = bc[key];
|
|
80
|
+
if (raw === null || raw === undefined || raw === '')
|
|
81
|
+
continue;
|
|
82
|
+
const metric = metricByProjectKey(metrics, key);
|
|
83
|
+
if (!metric)
|
|
84
|
+
continue;
|
|
85
|
+
patches.push(buildPatch(metric, Number(raw), findExistingRow(existing, metric), project.id));
|
|
86
|
+
}
|
|
87
|
+
// 계획공사기간 — 프로젝트 착공~준공 일정 → KPI 단방향.
|
|
88
|
+
// calcDateDiff 는 strict 'YYYY-MM-DD' 파싱이라 DB 가 ISO datetime 을 주면 0 이 되므로
|
|
89
|
+
// 여기서는 lenient moment 파싱으로 일수를 구해 개월(올림)로 환산한다.
|
|
90
|
+
if (project.startDate && project.endDate) {
|
|
91
|
+
const start = moment(project.startDate);
|
|
92
|
+
const end = moment(project.endDate);
|
|
93
|
+
const metric = metricByProjectKey(metrics, 'constructionPeriod');
|
|
94
|
+
if (metric && start.isValid() && end.isValid()) {
|
|
95
|
+
// 개월 = 일수/30, 소수점 한자리 반올림 (예: 373일 → 12.4개월)
|
|
96
|
+
const months = Math.max(0, Math.round((end.diff(start, 'days') / 30) * 10) / 10);
|
|
97
|
+
patches.push(buildPatch(metric, months, findExistingRow(existing, metric), project.id));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (patches.length) {
|
|
101
|
+
await updateProjectCompleteStep1(patches, projectCode);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* KPI 정보입력 저장 → 프로젝트 정보(BuildingComplex) 반영. (양방향 4필드만)
|
|
106
|
+
* constructionPeriod 는 일방향이므로 write-back 대상에서 제외.
|
|
107
|
+
* kpiMetrics / kpiMetricValues 는 KPI 화면이 이미 로드한 것을 그대로 사용.
|
|
108
|
+
*/
|
|
109
|
+
export async function syncKpiToProjectInfo(project, kpiMetrics, kpiMetricValues, projectCode) {
|
|
110
|
+
var _a;
|
|
111
|
+
const bcId = (_a = project === null || project === void 0 ? void 0 : project.buildingComplex) === null || _a === void 0 ? void 0 : _a.id;
|
|
112
|
+
if (!bcId)
|
|
113
|
+
return;
|
|
114
|
+
const patch = {};
|
|
115
|
+
for (const key of BIDIRECTIONAL_BC_KEYS) {
|
|
116
|
+
const metric = metricByProjectKey(kpiMetrics, key);
|
|
117
|
+
if (!metric)
|
|
118
|
+
continue;
|
|
119
|
+
const row = findExistingRow(kpiMetricValues, metric);
|
|
120
|
+
if (!row || row.value === undefined || row.value === null)
|
|
121
|
+
continue;
|
|
122
|
+
const num = Number(row.value);
|
|
123
|
+
patch[key] = BC_INT_KEYS.has(key) ? Math.round(num) : num;
|
|
124
|
+
}
|
|
125
|
+
if (Object.keys(patch).length) {
|
|
126
|
+
await updateBuildingComplexFields(bcId, patch, projectCode);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# 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 { getKpiMetrics, getKpiMetricValues, updateProjectCompleteStep1, updateBuildingComplexFields } from './complete-api'\n\n/**\n * 프로젝트 정보관리 ↔ KPI 정보입력 상호 동기.\n *\n * - 양방향(BIDIRECTIONAL_BC_KEYS): BuildingComplex 실컬럼을 가진 5필드(투입인력/연면적/\n * 용적률/설계변경건/계획공사비)는 어느 화면에서 저장하든 반대편도 갱신 (last-write-wins).\n * 두 저장 위치(BuildingComplex 컬럼 / 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 필드 키. 라벨은 실제 DB KpiMetric.name 과 일치해야 함.\n * (DB 확인: 계획공사비/계획공사기간/총 투입인력/용적률/총 설계 변경 건/연면적 존재) */\nconst LABEL_BY_KEY: Record<string, string> = {\n constructionPeriod: '계획공사기간',\n constructionCost: '계획공사비',\n workerCount: '총 투입인력',\n floorAreaRatio: '용적률',\n designChangeCount: '총 설계 변경 건',\n area: '연면적'\n}\n\n/** BuildingComplex 실컬럼 보유 → 프로젝트정보 ↔ KPI 양방향 동기 대상.\n * (constructionCost 는 억원 소수 가능 → 정수화 대상 아님) */\nexport const BIDIRECTIONAL_BC_KEYS = ['workerCount', 'area', 'floorAreaRatio', 'designChangeCount', 'constructionCost'] 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' ? today.clone().subtract(1, 'month').format('YYYY-MM-01') : 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([getKpiMetrics(projectCode), getKpiMetricValues(project.id, projectCode)])\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 // calcDateDiff 는 strict 'YYYY-MM-DD' 파싱이라 DB 가 ISO datetime 을 주면 0 이 되므로\n // 여기서는 lenient moment 파싱으로 일수를 구해 개월(올림)로 환산한다.\n if (project.startDate && project.endDate) {\n const start = moment(project.startDate)\n const end = moment(project.endDate)\n const metric = metricByProjectKey(metrics, 'constructionPeriod')\n if (metric && start.isValid() && end.isValid()) {\n // 개월 = 일수/30, 소수점 한자리 반올림 (예: 373일 → 12.4개월)\n const months = Math.max(0, Math.round((end.diff(start, 'days') / 30) * 10) / 10)\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"]}
|