@things-factory/ops-master 10.0.18 → 10.0.19
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/master-axes.js +20 -7
- package/dist-client/pages/master-axes.js.map +1 -1
- package/dist-client/pages/master-list.d.ts +3 -3
- package/dist-client/pages/master-list.js +10 -38
- package/dist-client/pages/master-list.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +9 -2
- package/dist-server/service/index.js +11 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/master-axis.d.ts +1 -1
- package/dist-server/service/master-axis.js +16 -1
- package/dist-server/service/master-axis.js.map +1 -1
- package/dist-server/service/operation/index.d.ts +2 -1
- package/dist-server/service/operation/index.js +2 -1
- package/dist-server/service/operation/index.js.map +1 -1
- package/dist-server/service/operation/operation-readiness-resolver.d.ts +39 -0
- package/dist-server/service/operation/operation-readiness-resolver.js +111 -0
- package/dist-server/service/operation/operation-readiness-resolver.js.map +1 -0
- package/dist-server/service/operation/operation-readiness.d.ts +34 -0
- package/dist-server/service/operation/operation-readiness.js +133 -0
- package/dist-server/service/operation/operation-readiness.js.map +1 -0
- package/dist-server/service/personnel/personnel-class-readiness.d.ts +22 -0
- package/dist-server/service/personnel/personnel-class-readiness.js +98 -0
- package/dist-server/service/personnel/personnel-class-readiness.js.map +1 -0
- package/dist-server/service/personnel/qualification.d.ts +7 -0
- package/dist-server/service/personnel/qualification.js +25 -0
- package/dist-server/service/personnel/qualification.js.map +1 -1
- package/dist-server/service/recipe/index.d.ts +9 -0
- package/dist-server/service/recipe/index.js +14 -0
- package/dist-server/service/recipe/index.js.map +1 -0
- package/dist-server/service/recipe/recipe-definition.d.ts +71 -0
- package/dist-server/service/recipe/recipe-definition.js +130 -0
- package/dist-server/service/recipe/recipe-definition.js.map +1 -0
- package/dist-server/service/recipe/recipe-master-resolver.d.ts +28 -0
- package/dist-server/service/recipe/recipe-master-resolver.js +124 -0
- package/dist-server/service/recipe/recipe-master-resolver.js.map +1 -0
- package/dist-server/service/recipe/recipe-master.d.ts +37 -0
- package/dist-server/service/recipe/recipe-master.js +47 -0
- package/dist-server/service/recipe/recipe-master.js.map +1 -0
- package/dist-server/service/recipe/recipe-query.d.ts +13 -0
- package/dist-server/service/recipe/recipe-query.js +54 -0
- package/dist-server/service/recipe/recipe-query.js.map +1 -0
- package/dist-server/service/recipe/route-definition.d.ts +51 -0
- package/dist-server/service/recipe/route-definition.js +113 -0
- package/dist-server/service/recipe/route-definition.js.map +1 -0
- package/dist-server/service/status/master-status.d.ts +0 -7
- package/dist-server/service/status/master-status.js +5 -6
- package/dist-server/service/status/master-status.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/translations/en.json +3 -3
- package/translations/ja.json +5 -1
- package/translations/ko.json +3 -3
- package/translations/ms.json +5 -1
- package/translations/zh.json +5 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.personnelClassReadiness = personnelClassReadiness;
|
|
4
|
+
const ops_contract_1 = require("@operato/ops-contract");
|
|
5
|
+
const operation_definition_1 = require("../operation/operation-definition");
|
|
6
|
+
const resource_class_1 = require("../resource/resource-class");
|
|
7
|
+
const person_1 = require("./person");
|
|
8
|
+
const personnel_class_1 = require("./personnel-class");
|
|
9
|
+
const qualification_1 = require("./qualification");
|
|
10
|
+
/**
|
|
11
|
+
* 인원 등급 하나의 **선언과 현실**.
|
|
12
|
+
*
|
|
13
|
+
* ── 이 화면에서 내리는 판단 ────────────────────────────────────────────────
|
|
14
|
+
* 「이 자격은 무엇을 요구하나」와 **「이 자격이 어디에 쓰이나」**다. 두 번째가 표로는 나오지 않는다 —
|
|
15
|
+
* 공정이 등급을 가리키므로 정방향은 가지만, 등급에서 공정으로 되돌아오는 길이 없다.
|
|
16
|
+
*
|
|
17
|
+
* ── 왜 「누가 갖고 있나」를 함께 내나 ──────────────────────────────────────
|
|
18
|
+
* 자격을 등록하는 이유는 그것으로 사람을 고르기 위해서다. 등급만 보이고 보유자를 안 보이면 「이 자격을
|
|
19
|
+
* 만들어 둔 값이 있나」를 알 수 없다.
|
|
20
|
+
*/
|
|
21
|
+
/** 한 번에 자격을 확인하는 인원 수의 상한. */
|
|
22
|
+
const MAX_PEOPLE_SCANNED = 2000;
|
|
23
|
+
/** 이 기간 안에 끊기는 자격을 「곧 만료」로 센다. */
|
|
24
|
+
const EXPIRING_SOON_DAYS = 30;
|
|
25
|
+
async function personnelClassReadiness(manager, domain, name, now = new Date()) {
|
|
26
|
+
const personnelClass = await manager.findOne(personnel_class_1.PersonnelClass, { where: { domain: { id: domain.id }, name } });
|
|
27
|
+
if (!personnelClass) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const at = now.toISOString();
|
|
31
|
+
const soonMs = now.getTime() + EXPIRING_SOON_DAYS * 24 * 3_600_000;
|
|
32
|
+
const [classes, people, peopleTotal, operations] = await Promise.all([
|
|
33
|
+
manager.find(personnel_class_1.PersonnelClass, { where: { domain: { id: domain.id } } }),
|
|
34
|
+
manager.find(person_1.Person, { where: { domain: { id: domain.id } }, take: MAX_PEOPLE_SCANNED }),
|
|
35
|
+
manager.count(person_1.Person, { where: { domain: { id: domain.id } } }),
|
|
36
|
+
manager.find(operation_definition_1.OperationDefinition, { where: { domain: { id: domain.id } } })
|
|
37
|
+
]);
|
|
38
|
+
const classDefs = classes.map(resource_class_1.toResourceClassDef);
|
|
39
|
+
const holders = [];
|
|
40
|
+
let expiringSoon = 0;
|
|
41
|
+
for (const person of people) {
|
|
42
|
+
const judgeable = {
|
|
43
|
+
name: person.name,
|
|
44
|
+
personnelClassIds: person.personnelClassIds,
|
|
45
|
+
testResults: person.testResults,
|
|
46
|
+
effectiveStart: person.effectiveStart,
|
|
47
|
+
effectiveEnd: person.effectiveEnd
|
|
48
|
+
};
|
|
49
|
+
if ((0, qualification_1.qualificationGapsFor)(judgeable, name, classDefs, at).length) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
holders.push(person.name);
|
|
53
|
+
const expiry = (0, qualification_1.nearestQualificationExpiry)(judgeable, name, classDefs, at);
|
|
54
|
+
if (expiry && Date.parse(expiry) <= soonMs) {
|
|
55
|
+
expiringSoon += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
personnelClass,
|
|
60
|
+
descendants: descendantsOf(name, classes, classDefs, at),
|
|
61
|
+
holders,
|
|
62
|
+
expiringSoon,
|
|
63
|
+
requiredBy: requiringOperations(name, operations),
|
|
64
|
+
peopleTotal,
|
|
65
|
+
peopleScanned: people.length
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 이 등급을 상속하는 등급들.
|
|
70
|
+
*
|
|
71
|
+
* 「6축 용접 자격」이 「용접 자격」을 상속하면, 용접을 요구하는 공정에 6축 용접 자격자도 선다. 그것이
|
|
72
|
+
* 이 화면에 보여야 「이 등급의 자격자가 왜 이렇게 많은가」에 답할 수 있다.
|
|
73
|
+
*
|
|
74
|
+
* 상속은 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.
|
|
75
|
+
*/
|
|
76
|
+
function descendantsOf(name, classes, classDefs, at) {
|
|
77
|
+
return classes
|
|
78
|
+
.filter(candidate => candidate.name !== name && (0, ops_contract_1.classClosure)([candidate.name], classDefs, at).has(name))
|
|
79
|
+
.map(candidate => candidate.name);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 이 등급을 요구하는 공정들.
|
|
83
|
+
*
|
|
84
|
+
* 상속을 보지 않는다 — 공정이 「용접」을 요구했으면 그것이 선언이고, 「6축 용접」 화면에서 「용접을
|
|
85
|
+
* 요구하는 공정」을 내면 선언하지 않은 것을 선언한 것처럼 보이게 된다.
|
|
86
|
+
*/
|
|
87
|
+
function requiringOperations(name, operations) {
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const operation of operations) {
|
|
90
|
+
for (const entry of (0, qualification_1.personnelSpecificationOf)(operation.personnelSpecification)) {
|
|
91
|
+
if (entry.personnelClassId === name) {
|
|
92
|
+
found.push({ operationName: operation.name, required: entry.quantity ?? 1 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=personnel-class-readiness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personnel-class-readiness.js","sourceRoot":"","sources":["../../../server/service/personnel/personnel-class-readiness.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\nimport { classClosure } from '@operato/ops-contract'\nimport type { TestResult } from '@operato/ops-contract'\n\nimport { OperationDefinition } from '../operation/operation-definition'\nimport { toResourceClassDef } from '../resource/resource-class'\nimport { Person } from './person'\nimport { PersonnelClass } from './personnel-class'\nimport { nearestQualificationExpiry, personnelSpecificationOf, qualificationGapsFor } from './qualification'\n\n/**\n * 인원 등급 하나의 **선언과 현실**.\n *\n * ── 이 화면에서 내리는 판단 ────────────────────────────────────────────────\n * 「이 자격은 무엇을 요구하나」와 **「이 자격이 어디에 쓰이나」**다. 두 번째가 표로는 나오지 않는다 —\n * 공정이 등급을 가리키므로 정방향은 가지만, 등급에서 공정으로 되돌아오는 길이 없다.\n *\n * ── 왜 「누가 갖고 있나」를 함께 내나 ──────────────────────────────────────\n * 자격을 등록하는 이유는 그것으로 사람을 고르기 위해서다. 등급만 보이고 보유자를 안 보이면 「이 자격을\n * 만들어 둔 값이 있나」를 알 수 없다.\n */\n\n/** 한 번에 자격을 확인하는 인원 수의 상한. */\nconst MAX_PEOPLE_SCANNED = 2000\n\n/** 이 기간 안에 끊기는 자격을 「곧 만료」로 센다. */\nconst EXPIRING_SOON_DAYS = 30\n\nexport interface RequiringOperation {\n operationName: string\n /** 그 공정이 이 등급으로 몇 명을 요구하나. */\n required: number\n}\n\nexport interface PersonnelClassReadiness {\n personnelClass: PersonnelClass\n /** 이 등급을 상속하는 하위 등급들 — 이들의 자격자도 이 등급을 만족시킨다. */\n descendants: string[]\n /** 지금 이 자격을 갖춘 인원. */\n holders: string[]\n /** 그중 곧 끊기는 인원 수. */\n expiringSoon: number\n /** 이 등급을 요구하는 공정들 — 역방향이다. */\n requiredBy: RequiringOperation[]\n peopleTotal: number\n peopleScanned: number\n}\n\nexport async function personnelClassReadiness(\n manager: EntityManager,\n domain: Domain,\n name: string,\n now = new Date()\n): Promise<PersonnelClassReadiness | undefined> {\n const personnelClass = await manager.findOne(PersonnelClass, { where: { domain: { id: domain.id }, name } })\n\n if (!personnelClass) {\n return undefined\n }\n\n const at = now.toISOString()\n const soonMs = now.getTime() + EXPIRING_SOON_DAYS * 24 * 3_600_000\n\n const [classes, people, peopleTotal, operations] = await Promise.all([\n manager.find(PersonnelClass, { where: { domain: { id: domain.id } } }),\n manager.find(Person, { where: { domain: { id: domain.id } }, take: MAX_PEOPLE_SCANNED }),\n manager.count(Person, { where: { domain: { id: domain.id } } }),\n manager.find(OperationDefinition, { where: { domain: { id: domain.id } } })\n ])\n\n const classDefs = classes.map(toResourceClassDef)\n\n const holders: string[] = []\n let expiringSoon = 0\n\n for (const person of people) {\n const judgeable = {\n name: person.name,\n personnelClassIds: person.personnelClassIds,\n testResults: person.testResults as unknown as TestResult[] | undefined,\n effectiveStart: person.effectiveStart,\n effectiveEnd: person.effectiveEnd\n }\n\n if (qualificationGapsFor(judgeable, name, classDefs, at).length) {\n continue\n }\n\n holders.push(person.name)\n\n const expiry = nearestQualificationExpiry(judgeable, name, classDefs, at)\n if (expiry && Date.parse(expiry) <= soonMs) {\n expiringSoon += 1\n }\n }\n\n return {\n personnelClass,\n descendants: descendantsOf(name, classes, classDefs, at),\n holders,\n expiringSoon,\n requiredBy: requiringOperations(name, operations),\n peopleTotal,\n peopleScanned: people.length\n }\n}\n\n/**\n * 이 등급을 상속하는 등급들.\n *\n * 「6축 용접 자격」이 「용접 자격」을 상속하면, 용접을 요구하는 공정에 6축 용접 자격자도 선다. 그것이\n * 이 화면에 보여야 「이 등급의 자격자가 왜 이렇게 많은가」에 답할 수 있다.\n *\n * 상속은 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.\n */\nfunction descendantsOf(\n name: string,\n classes: readonly PersonnelClass[],\n classDefs: readonly ReturnType<typeof toResourceClassDef>[],\n at: string\n): string[] {\n return classes\n .filter(candidate => candidate.name !== name && classClosure([candidate.name], classDefs, at).has(name))\n .map(candidate => candidate.name)\n}\n\n/**\n * 이 등급을 요구하는 공정들.\n *\n * 상속을 보지 않는다 — 공정이 「용접」을 요구했으면 그것이 선언이고, 「6축 용접」 화면에서 「용접을\n * 요구하는 공정」을 내면 선언하지 않은 것을 선언한 것처럼 보이게 된다.\n */\nfunction requiringOperations(name: string, operations: readonly OperationDefinition[]): RequiringOperation[] {\n const found: RequiringOperation[] = []\n\n for (const operation of operations) {\n for (const entry of personnelSpecificationOf(operation.personnelSpecification)) {\n if (entry.personnelClassId === name) {\n found.push({ operationName: operation.name, required: entry.quantity ?? 1 })\n }\n }\n }\n\n return found\n}\n"]}
|
|
@@ -95,3 +95,10 @@ export declare function qualificationGapsFor(person: QualifiablePerson, classId:
|
|
|
95
95
|
* 일이 아니다. 여기서는 각 요구가 채워졌는지만 본다.
|
|
96
96
|
*/
|
|
97
97
|
export declare function judgePersonnel(spec: readonly PersonnelSpecificationEntry[], persons: readonly QualifiablePerson[], classDefs: readonly ResourceClassDef[], at: string): QualificationVerdict;
|
|
98
|
+
/**
|
|
99
|
+
* 이 사람의 이 등급 자격이 **언제 가장 먼저 끊기나.**
|
|
100
|
+
*
|
|
101
|
+
* 요구되는 검사가 여럿이면 가장 이른 만료가 그 등급의 만료다 — 하나라도 끊기면 자격이 성립하지 않는다.
|
|
102
|
+
* 만료가 선언되지 않은 검사는 무기한이므로 세지 않는다(모르는 것과 지난 것은 다르다).
|
|
103
|
+
*/
|
|
104
|
+
export declare function nearestQualificationExpiry(person: QualifiablePerson, classId: string, classDefs: readonly ResourceClassDef[], at: string): string | undefined;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.personnelSpecificationOf = personnelSpecificationOf;
|
|
4
4
|
exports.qualificationGapsFor = qualificationGapsFor;
|
|
5
5
|
exports.judgePersonnel = judgePersonnel;
|
|
6
|
+
exports.nearestQualificationExpiry = nearestQualificationExpiry;
|
|
6
7
|
const ops_contract_1 = require("@operato/ops-contract");
|
|
7
8
|
/**
|
|
8
9
|
* 선언된 필요 인원을 읽는다.
|
|
@@ -119,4 +120,28 @@ function isoOf(at) {
|
|
|
119
120
|
}
|
|
120
121
|
return typeof at === 'string' ? at : at.toISOString();
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* 이 사람의 이 등급 자격이 **언제 가장 먼저 끊기나.**
|
|
125
|
+
*
|
|
126
|
+
* 요구되는 검사가 여럿이면 가장 이른 만료가 그 등급의 만료다 — 하나라도 끊기면 자격이 성립하지 않는다.
|
|
127
|
+
* 만료가 선언되지 않은 검사는 무기한이므로 세지 않는다(모르는 것과 지난 것은 다르다).
|
|
128
|
+
*/
|
|
129
|
+
function nearestQualificationExpiry(person, classId, classDefs, at) {
|
|
130
|
+
const required = new Set((0, ops_contract_1.requiredTestsFor)([classId], classDefs, at));
|
|
131
|
+
if (!required.size) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const bySpec = new Map((person.testResults ?? []).map(result => [result.specId, result]));
|
|
135
|
+
let nearest;
|
|
136
|
+
for (const specId of required) {
|
|
137
|
+
const result = bySpec.get(specId);
|
|
138
|
+
if (!result?.expiresAt || result.result !== 'pass') {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!nearest || Date.parse(result.expiresAt) < Date.parse(nearest)) {
|
|
142
|
+
nearest = result.expiresAt;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return nearest;
|
|
146
|
+
}
|
|
122
147
|
//# sourceMappingURL=qualification.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qualification.js","sourceRoot":"","sources":["../../../server/service/personnel/qualification.ts"],"names":[],"mappings":"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","sourcesContent":["import { classClosure, effectivityAt, requiredTestsFor, testPassedAt } from '@operato/ops-contract'\nimport type { ResourceClassDef, TestResult } from '@operato/ops-contract'\n\n/**\n * 이 사람이 이 공정을 할 자격이 있나 — **ISA-95 `PersonnelSpecification` 을 실제로 거는 자리.**\n *\n * 공정은 등급으로 요구하고(`OperationDefinition.personnelSpecification`), 등급은 검사를 요구하고\n * (`PersonnelClass.testSpecificationIds`), 사람은 결과를 든다(`Person.testResults`). 세 자리가 다\n * 있었는데 그것을 잇는 판단이 없어서, 마스터에 적힌 자격이 아무 일도 하지 않고 있었다.\n *\n * ── 기록이 없을 때의 판단이 커널과 반대다 ──────────────────────────────────\n * 계약의 `meetsTests` 는 결과가 선언되지 않은 검사를 **제약으로 보지 않는다**(`contract.ts` §meetsTests).\n * 트윈에서는 그것이 맞다 — 모델 데이터가 성긴데 기록이 없다고 다 막으면 라인이 굶는다.\n *\n * 실행 시스템에서는 반대다. 「이 사람의 용접 자격 기록이 없다」는 통과 사유가 아니라 막을 사유다.\n * 그래서 등급 상속(`classClosure`) · 필수 검사 수집(`requiredTestsFor`) · 만료 판정(`testPassedAt`) 은\n * 계약 것을 그대로 쓰고, **없을 때의 판단만 여기서 뒤집는다.** 커널 함수는 고치지 않는다.\n *\n * ── 왜 이유를 종류별로 돌려주나 ────────────────────────────────────────────\n * 막기만 하고 무엇이 모자란지 말하지 않는 화면은 현장에서 우회 대상이 된다. 자격이 없는 것과 만료된\n * 것은 할 일이 다르다 — 앞은 사람을 바꾸고, 뒤는 갱신하면 된다.\n */\n\n/** 무엇이 모자란가. 종류마다 현장이 할 일이 다르다. */\nexport type QualificationLack =\n /** 그 등급에 속하지 않는다 — 다른 사람을 부르는 수밖에 없다. */\n | 'not-in-class'\n /** 등급이 요구하는 검사의 기록이 없다 — 받았는지 확인하고 기록한다. */\n | 'test-missing'\n /** 불합격 기록이다. */\n | 'test-failed'\n /** 합격했으나 유효기간이 지났다 — 갱신하면 된다. */\n | 'test-expired'\n /** 아직 현장에 없거나(입사 전) 이미 없다(퇴사). */\n | 'not-effective'\n /**\n * 그 이름이 인원 마스터에 없다 — 자격을 따질 수가 없다.\n *\n * 「자격 없음」으로 내면 안 된다. 자격이 없는 것과 그 사람이 누구인지 모르는 것은 할 일이 다르다\n * (앞은 사람을 바꾸고, 뒤는 등록하거나 이름을 고친다).\n */\n | 'not-registered'\n\nexport interface QualificationGap {\n /** 누가 — `Person.name`. */\n personName: string\n /** 어느 등급을 요구했나 — `PersonnelClass.name`. */\n classId: string\n lack: QualificationLack\n /** 검사 때문이면 어느 검사인가 — `TestSpecification.id`. */\n specId?: string\n /** 만료된 것이면 언제 만료됐나(ISO). */\n expiredAt?: string\n}\n\n/** 등급별로 몇 명이 필요한데 몇 명이 자격을 갖췄나. */\nexport interface QualificationShortfall {\n classId: string\n required: number\n qualified: number\n}\n\nexport interface QualificationVerdict {\n qualified: boolean\n gaps: QualificationGap[]\n shortfalls: QualificationShortfall[]\n}\n\n/** 판단에 필요한 사람의 사실만. 엔티티를 통째로 받지 않는다 — 그러면 테스트가 DB 를 끌고 온다. */\nexport interface QualifiablePerson {\n name: string\n personnelClassIds?: string[]\n testResults?: TestResult[]\n effectiveStart?: Date | string\n effectiveEnd?: Date | string\n}\n\n/** ISA-95 `PersonnelSpecification` — 등급 하나와 그 등급으로 몇 명이 필요한가. */\nexport interface PersonnelSpecificationEntry {\n personnelClassId: string\n quantity?: number\n}\n\n/**\n * 선언된 필요 인원을 읽는다.\n *\n * `OperationDefinition.personnelSpecification` 은 JSON 칸이라 모양이 보장되지 않는다. 등급 이름이 없는\n * 줄은 **버리지 않고 걸러 낸다** — 그런 줄로 요구를 만들면 아무도 만족시킬 수 없는 공정이 된다.\n * 수량이 없으면 1명으로 읽는다(ISA-95 `Quantity` 는 선택 항목이고, 요구했다는 것 자체가 최소 한 명이다).\n */\nexport function personnelSpecificationOf(raw: readonly Record<string, unknown>[] | undefined): PersonnelSpecificationEntry[] {\n if (!raw?.length) {\n return []\n }\n\n const entries: PersonnelSpecificationEntry[] = []\n\n for (const row of raw) {\n const classId = row?.personnelClassId ?? row?.personnelClassID ?? row?.classId\n if (typeof classId !== 'string' || !classId) {\n continue\n }\n\n const quantity = Number(row?.quantity)\n entries.push({ personnelClassId: classId, quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1 })\n }\n\n return entries\n}\n\n/**\n * 이 사람이 이 등급의 자격을 갖췄나. 모자라면 **무엇이** 모자란지 돌려준다.\n *\n * 상속을 탄다 — 「6축 용접 자격」을 가진 사람은 「용접 자격」을 요구하는 공정에 선다. 그 상속은\n * 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.\n */\nexport function qualificationGapsFor(\n person: QualifiablePerson,\n classId: string,\n classDefs: readonly ResourceClassDef[],\n at: string\n): QualificationGap[] {\n const period = {\n effectiveStart: isoOf(person.effectiveStart),\n effectiveEnd: isoOf(person.effectiveEnd)\n }\n\n /* 유효기간 밖이면 자격을 볼 것도 없다 — 퇴사자의 용접 자격은 유효해도 배정 대상이 아니다. */\n if (effectivityAt(period, at) !== undefined) {\n return [{ personName: person.name, classId, lack: 'not-effective' }]\n }\n\n const closure = classClosure(person.personnelClassIds, classDefs, at)\n if (!closure.has(classId)) {\n return [{ personName: person.name, classId, lack: 'not-in-class' }]\n }\n\n const required = requiredTestsFor([classId], classDefs, at)\n if (!required.length) {\n return []\n }\n\n const bySpec = new Map((person.testResults ?? []).map(result => [result.specId, result]))\n const gaps: QualificationGap[] = []\n\n for (const specId of required) {\n const result = bySpec.get(specId)\n\n /*\n * 여기가 커널과 갈리는 한 줄이다. 계약은 없는 결과를 제약으로 보지 않고, 실행 시스템은 막는다.\n * 기록이 없다는 것은 「자격이 있다」가 아니라 「모른다」이고, 모르는 채로 작업을 보내지 않는다.\n */\n if (!result) {\n gaps.push({ personName: person.name, classId, lack: 'test-missing', specId })\n continue\n }\n\n if (result.result === 'fail') {\n gaps.push({ personName: person.name, classId, lack: 'test-failed', specId })\n continue\n }\n\n /* 판정이 비어 있는 것도 통과가 아니다 — 「재기만 하고 판정하지 않았다」는 합격이 아니다. */\n if (result.result !== 'pass') {\n gaps.push({ personName: person.name, classId, lack: 'test-missing', specId })\n continue\n }\n\n if (!testPassedAt(result, at)) {\n gaps.push({ personName: person.name, classId, lack: 'test-expired', specId, expiredAt: result.expiresAt })\n }\n }\n\n return gaps\n}\n\n/**\n * 이 사람들로 이 공정을 돌릴 수 있나.\n *\n * 등급마다 수량을 센다. 한 사람이 여러 등급을 만족시킬 수 있으므로 **등급별로 따로 센다** — 「용접\n * 1명 · 지게차 1명」을 두 자격을 가진 한 사람으로 채울 수 있는지는 현장이 정할 일이지 여기서 정할\n * 일이 아니다. 여기서는 각 요구가 채워졌는지만 본다.\n */\nexport function judgePersonnel(\n spec: readonly PersonnelSpecificationEntry[],\n persons: readonly QualifiablePerson[],\n classDefs: readonly ResourceClassDef[],\n at: string\n): QualificationVerdict {\n if (!spec.length) {\n return { qualified: true, gaps: [], shortfalls: [] }\n }\n\n const gaps: QualificationGap[] = []\n const shortfalls: QualificationShortfall[] = []\n\n for (const { personnelClassId, quantity } of spec) {\n const required = quantity ?? 1\n let count = 0\n\n for (const person of persons) {\n const found = qualificationGapsFor(person, personnelClassId, classDefs, at)\n\n if (!found.length) {\n count += 1\n continue\n }\n\n /*\n * 그 등급에 아예 속하지 않는 사람은 이유로 내지 않는다. 실적에 이름이 다섯이면 그 중 넷은\n * 다른 일을 하는 사람이고, 그들까지 「용접 자격 없음」으로 늘어놓으면 진짜 이유가 묻힌다.\n */\n if (found.some(gap => gap.lack === 'not-in-class')) {\n continue\n }\n\n gaps.push(...found)\n }\n\n if (count < required) {\n shortfalls.push({ classId: personnelClassId, required, qualified: count })\n }\n }\n\n return { qualified: shortfalls.length === 0, gaps, shortfalls }\n}\n\nfunction isoOf(at: Date | string | undefined): string | undefined {\n if (!at) {\n return undefined\n }\n\n return typeof at === 'string' ? at : at.toISOString()\n}\n"]}
|
|
1
|
+
{"version":3,"file":"qualification.js","sourceRoot":"","sources":["../../../server/service/personnel/qualification.ts"],"names":[],"mappings":"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","sourcesContent":["import { classClosure, effectivityAt, requiredTestsFor, testPassedAt } from '@operato/ops-contract'\nimport type { ResourceClassDef, TestResult } from '@operato/ops-contract'\n\n/**\n * 이 사람이 이 공정을 할 자격이 있나 — **ISA-95 `PersonnelSpecification` 을 실제로 거는 자리.**\n *\n * 공정은 등급으로 요구하고(`OperationDefinition.personnelSpecification`), 등급은 검사를 요구하고\n * (`PersonnelClass.testSpecificationIds`), 사람은 결과를 든다(`Person.testResults`). 세 자리가 다\n * 있었는데 그것을 잇는 판단이 없어서, 마스터에 적힌 자격이 아무 일도 하지 않고 있었다.\n *\n * ── 기록이 없을 때의 판단이 커널과 반대다 ──────────────────────────────────\n * 계약의 `meetsTests` 는 결과가 선언되지 않은 검사를 **제약으로 보지 않는다**(`contract.ts` §meetsTests).\n * 트윈에서는 그것이 맞다 — 모델 데이터가 성긴데 기록이 없다고 다 막으면 라인이 굶는다.\n *\n * 실행 시스템에서는 반대다. 「이 사람의 용접 자격 기록이 없다」는 통과 사유가 아니라 막을 사유다.\n * 그래서 등급 상속(`classClosure`) · 필수 검사 수집(`requiredTestsFor`) · 만료 판정(`testPassedAt`) 은\n * 계약 것을 그대로 쓰고, **없을 때의 판단만 여기서 뒤집는다.** 커널 함수는 고치지 않는다.\n *\n * ── 왜 이유를 종류별로 돌려주나 ────────────────────────────────────────────\n * 막기만 하고 무엇이 모자란지 말하지 않는 화면은 현장에서 우회 대상이 된다. 자격이 없는 것과 만료된\n * 것은 할 일이 다르다 — 앞은 사람을 바꾸고, 뒤는 갱신하면 된다.\n */\n\n/** 무엇이 모자란가. 종류마다 현장이 할 일이 다르다. */\nexport type QualificationLack =\n /** 그 등급에 속하지 않는다 — 다른 사람을 부르는 수밖에 없다. */\n | 'not-in-class'\n /** 등급이 요구하는 검사의 기록이 없다 — 받았는지 확인하고 기록한다. */\n | 'test-missing'\n /** 불합격 기록이다. */\n | 'test-failed'\n /** 합격했으나 유효기간이 지났다 — 갱신하면 된다. */\n | 'test-expired'\n /** 아직 현장에 없거나(입사 전) 이미 없다(퇴사). */\n | 'not-effective'\n /**\n * 그 이름이 인원 마스터에 없다 — 자격을 따질 수가 없다.\n *\n * 「자격 없음」으로 내면 안 된다. 자격이 없는 것과 그 사람이 누구인지 모르는 것은 할 일이 다르다\n * (앞은 사람을 바꾸고, 뒤는 등록하거나 이름을 고친다).\n */\n | 'not-registered'\n\nexport interface QualificationGap {\n /** 누가 — `Person.name`. */\n personName: string\n /** 어느 등급을 요구했나 — `PersonnelClass.name`. */\n classId: string\n lack: QualificationLack\n /** 검사 때문이면 어느 검사인가 — `TestSpecification.id`. */\n specId?: string\n /** 만료된 것이면 언제 만료됐나(ISO). */\n expiredAt?: string\n}\n\n/** 등급별로 몇 명이 필요한데 몇 명이 자격을 갖췄나. */\nexport interface QualificationShortfall {\n classId: string\n required: number\n qualified: number\n}\n\nexport interface QualificationVerdict {\n qualified: boolean\n gaps: QualificationGap[]\n shortfalls: QualificationShortfall[]\n}\n\n/** 판단에 필요한 사람의 사실만. 엔티티를 통째로 받지 않는다 — 그러면 테스트가 DB 를 끌고 온다. */\nexport interface QualifiablePerson {\n name: string\n personnelClassIds?: string[]\n testResults?: TestResult[]\n effectiveStart?: Date | string\n effectiveEnd?: Date | string\n}\n\n/** ISA-95 `PersonnelSpecification` — 등급 하나와 그 등급으로 몇 명이 필요한가. */\nexport interface PersonnelSpecificationEntry {\n personnelClassId: string\n quantity?: number\n}\n\n/**\n * 선언된 필요 인원을 읽는다.\n *\n * `OperationDefinition.personnelSpecification` 은 JSON 칸이라 모양이 보장되지 않는다. 등급 이름이 없는\n * 줄은 **버리지 않고 걸러 낸다** — 그런 줄로 요구를 만들면 아무도 만족시킬 수 없는 공정이 된다.\n * 수량이 없으면 1명으로 읽는다(ISA-95 `Quantity` 는 선택 항목이고, 요구했다는 것 자체가 최소 한 명이다).\n */\nexport function personnelSpecificationOf(raw: readonly Record<string, unknown>[] | undefined): PersonnelSpecificationEntry[] {\n if (!raw?.length) {\n return []\n }\n\n const entries: PersonnelSpecificationEntry[] = []\n\n for (const row of raw) {\n const classId = row?.personnelClassId ?? row?.personnelClassID ?? row?.classId\n if (typeof classId !== 'string' || !classId) {\n continue\n }\n\n const quantity = Number(row?.quantity)\n entries.push({ personnelClassId: classId, quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1 })\n }\n\n return entries\n}\n\n/**\n * 이 사람이 이 등급의 자격을 갖췄나. 모자라면 **무엇이** 모자란지 돌려준다.\n *\n * 상속을 탄다 — 「6축 용접 자격」을 가진 사람은 「용접 자격」을 요구하는 공정에 선다. 그 상속은\n * 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.\n */\nexport function qualificationGapsFor(\n person: QualifiablePerson,\n classId: string,\n classDefs: readonly ResourceClassDef[],\n at: string\n): QualificationGap[] {\n const period = {\n effectiveStart: isoOf(person.effectiveStart),\n effectiveEnd: isoOf(person.effectiveEnd)\n }\n\n /* 유효기간 밖이면 자격을 볼 것도 없다 — 퇴사자의 용접 자격은 유효해도 배정 대상이 아니다. */\n if (effectivityAt(period, at) !== undefined) {\n return [{ personName: person.name, classId, lack: 'not-effective' }]\n }\n\n const closure = classClosure(person.personnelClassIds, classDefs, at)\n if (!closure.has(classId)) {\n return [{ personName: person.name, classId, lack: 'not-in-class' }]\n }\n\n const required = requiredTestsFor([classId], classDefs, at)\n if (!required.length) {\n return []\n }\n\n const bySpec = new Map((person.testResults ?? []).map(result => [result.specId, result]))\n const gaps: QualificationGap[] = []\n\n for (const specId of required) {\n const result = bySpec.get(specId)\n\n /*\n * 여기가 커널과 갈리는 한 줄이다. 계약은 없는 결과를 제약으로 보지 않고, 실행 시스템은 막는다.\n * 기록이 없다는 것은 「자격이 있다」가 아니라 「모른다」이고, 모르는 채로 작업을 보내지 않는다.\n */\n if (!result) {\n gaps.push({ personName: person.name, classId, lack: 'test-missing', specId })\n continue\n }\n\n if (result.result === 'fail') {\n gaps.push({ personName: person.name, classId, lack: 'test-failed', specId })\n continue\n }\n\n /* 판정이 비어 있는 것도 통과가 아니다 — 「재기만 하고 판정하지 않았다」는 합격이 아니다. */\n if (result.result !== 'pass') {\n gaps.push({ personName: person.name, classId, lack: 'test-missing', specId })\n continue\n }\n\n if (!testPassedAt(result, at)) {\n gaps.push({ personName: person.name, classId, lack: 'test-expired', specId, expiredAt: result.expiresAt })\n }\n }\n\n return gaps\n}\n\n/**\n * 이 사람들로 이 공정을 돌릴 수 있나.\n *\n * 등급마다 수량을 센다. 한 사람이 여러 등급을 만족시킬 수 있으므로 **등급별로 따로 센다** — 「용접\n * 1명 · 지게차 1명」을 두 자격을 가진 한 사람으로 채울 수 있는지는 현장이 정할 일이지 여기서 정할\n * 일이 아니다. 여기서는 각 요구가 채워졌는지만 본다.\n */\nexport function judgePersonnel(\n spec: readonly PersonnelSpecificationEntry[],\n persons: readonly QualifiablePerson[],\n classDefs: readonly ResourceClassDef[],\n at: string\n): QualificationVerdict {\n if (!spec.length) {\n return { qualified: true, gaps: [], shortfalls: [] }\n }\n\n const gaps: QualificationGap[] = []\n const shortfalls: QualificationShortfall[] = []\n\n for (const { personnelClassId, quantity } of spec) {\n const required = quantity ?? 1\n let count = 0\n\n for (const person of persons) {\n const found = qualificationGapsFor(person, personnelClassId, classDefs, at)\n\n if (!found.length) {\n count += 1\n continue\n }\n\n /*\n * 그 등급에 아예 속하지 않는 사람은 이유로 내지 않는다. 실적에 이름이 다섯이면 그 중 넷은\n * 다른 일을 하는 사람이고, 그들까지 「용접 자격 없음」으로 늘어놓으면 진짜 이유가 묻힌다.\n */\n if (found.some(gap => gap.lack === 'not-in-class')) {\n continue\n }\n\n gaps.push(...found)\n }\n\n if (count < required) {\n shortfalls.push({ classId: personnelClassId, required, qualified: count })\n }\n }\n\n return { qualified: shortfalls.length === 0, gaps, shortfalls }\n}\n\nfunction isoOf(at: Date | string | undefined): string | undefined {\n if (!at) {\n return undefined\n }\n\n return typeof at === 'string' ? at : at.toISOString()\n}\n\n/**\n * 이 사람의 이 등급 자격이 **언제 가장 먼저 끊기나.**\n *\n * 요구되는 검사가 여럿이면 가장 이른 만료가 그 등급의 만료다 — 하나라도 끊기면 자격이 성립하지 않는다.\n * 만료가 선언되지 않은 검사는 무기한이므로 세지 않는다(모르는 것과 지난 것은 다르다).\n */\nexport function nearestQualificationExpiry(\n person: QualifiablePerson,\n classId: string,\n classDefs: readonly ResourceClassDef[],\n at: string\n): string | undefined {\n const required = new Set(requiredTestsFor([classId], classDefs, at))\n if (!required.size) {\n return undefined\n }\n\n const bySpec = new Map((person.testResults ?? []).map(result => [result.specId, result]))\n let nearest: string | undefined\n\n for (const specId of required) {\n const result = bySpec.get(specId)\n if (!result?.expiresAt || result.result !== 'pass') {\n continue\n }\n if (!nearest || Date.parse(result.expiresAt) < Date.parse(nearest)) {\n nearest = result.expiresAt\n }\n }\n\n return nearest\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RecipeDefinition } from './recipe-definition';
|
|
2
|
+
import { RouteDefinition } from './route-definition';
|
|
3
|
+
import { RecipeDefinitionResolver, RouteDefinitionResolver } from './recipe-master-resolver';
|
|
4
|
+
import { RecipeQuery } from './recipe-query';
|
|
5
|
+
export declare const entities: (typeof RecipeDefinition | typeof RouteDefinition)[];
|
|
6
|
+
export declare const resolvers: (typeof RecipeDefinitionResolver | typeof RouteDefinitionResolver | typeof RecipeQuery)[];
|
|
7
|
+
export * from './recipe-definition';
|
|
8
|
+
export * from './route-definition';
|
|
9
|
+
export * from './recipe-master';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvers = exports.entities = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const recipe_definition_1 = require("./recipe-definition");
|
|
6
|
+
const route_definition_1 = require("./route-definition");
|
|
7
|
+
const recipe_master_resolver_1 = require("./recipe-master-resolver");
|
|
8
|
+
const recipe_query_1 = require("./recipe-query");
|
|
9
|
+
exports.entities = [recipe_definition_1.RecipeDefinition, route_definition_1.RouteDefinition];
|
|
10
|
+
exports.resolvers = [recipe_master_resolver_1.RecipeDefinitionResolver, recipe_master_resolver_1.RouteDefinitionResolver, recipe_query_1.RecipeQuery];
|
|
11
|
+
tslib_1.__exportStar(require("./recipe-definition"), exports);
|
|
12
|
+
tslib_1.__exportStar(require("./route-definition"), exports);
|
|
13
|
+
tslib_1.__exportStar(require("./recipe-master"), exports);
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/recipe/index.ts"],"names":[],"mappings":";;;;AAAA,2DAAsD;AACtD,yDAAoD;AACpD,qEAA4F;AAC5F,iDAA4C;AAE/B,QAAA,QAAQ,GAAG,CAAC,oCAAgB,EAAE,kCAAe,CAAC,CAAA;AAC9C,QAAA,SAAS,GAAG,CAAC,iDAAwB,EAAE,gDAAuB,EAAE,0BAAW,CAAC,CAAA;AAEzF,8DAAmC;AACnC,6DAAkC;AAClC,0DAA+B","sourcesContent":["import { RecipeDefinition } from './recipe-definition'\nimport { RouteDefinition } from './route-definition'\nimport { RecipeDefinitionResolver, RouteDefinitionResolver } from './recipe-master-resolver'\nimport { RecipeQuery } from './recipe-query'\n\nexport const entities = [RecipeDefinition, RouteDefinition]\nexport const resolvers = [RecipeDefinitionResolver, RouteDefinitionResolver, RecipeQuery]\n\nexport * from './recipe-definition'\nexport * from './route-definition'\nexport * from './recipe-master'\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '@things-factory/auth-base';
|
|
3
|
+
/**
|
|
4
|
+
* 레시피 — **무엇으로 무엇을 만드나**. 커널 `RecipeDef` 와 같은 모양이다.
|
|
5
|
+
*
|
|
6
|
+
* ── 이 자리가 답하는 것 ──────────────────────────────────────────────────
|
|
7
|
+
* 「이 품목은 무엇으로 만드나」와 「이 품목을 만드는 것은 어느 레시피인가」다. 둘째가 MES 가 물어야 했던
|
|
8
|
+
* 것인데 물을 곳이 없었다.
|
|
9
|
+
*
|
|
10
|
+
* ── 품목에 지시서를 달지 않는 이유 ───────────────────────────────────────
|
|
11
|
+
* ISA-95 에는 자재 명세가 붙는 자리가 둘이고 뜻이 다르다(§`RecipePart.operation`).
|
|
12
|
+
*
|
|
13
|
+
* · `ProcessSegment` — 그 자리가 **할 수 있는 일**. 품목에 매이지 않는다 → `OperationDefinition`
|
|
14
|
+
* · `OperationsSegment` — 그 **품목을 만드는 절차**의 한 단계. 품목에 매인다 → **레시피가 그 범위다**
|
|
15
|
+
*
|
|
16
|
+
* 그래서 품목→지시서를 이으려면 그 사이에 레시피가 있어야 한다. 공정에 품목을 붙이면 범주 오류이고,
|
|
17
|
+
* 실 데이터가 그 크기를 보였다 — 「배합」에 품목별 BOM 을 붙이면 배합을 지나는 **모든** 로트가 자재
|
|
18
|
+
* 631종 36톤을 먹는다.
|
|
19
|
+
*
|
|
20
|
+
* ── 같은 품목에 레시피가 둘일 수 있다 ────────────────────────────────────
|
|
21
|
+
* 대체 레시피가 정상이다(첫 실 연동에서 경로가 둘 이상인 레시피가 306/471). 그래서 **품목이 레시피를
|
|
22
|
+
* 가리키지 않고 레시피가 품목을 가리킨다** — 품목은 하나를 고를 수 없다.
|
|
23
|
+
*/
|
|
24
|
+
export declare class RecipeDefinition {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
domain?: Domain;
|
|
27
|
+
domainId?: string;
|
|
28
|
+
/** 레시피 키 — 오더의 `recipeKey` 가 이 값을 가리킨다(커널 `RecipeDef.key`). */
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
/**
|
|
32
|
+
* **무엇을 만드나** — `MaterialDefinition.name`.
|
|
33
|
+
*
|
|
34
|
+
* `outputs` 안에도 있지만 이 칸을 따로 둔다. 「이 품목을 만드는 레시피」를 JSON 안을 뒤져 찾으면
|
|
35
|
+
* 다섯 드라이버에서 색인이 서지 않고, 그 질의는 MES 가 지시를 받을 때마다 부른다.
|
|
36
|
+
*
|
|
37
|
+
* 산출이 여럿인 레시피(부산물)에서는 **주 산출**을 적는다. 나머지는 `outputs` 가 든다.
|
|
38
|
+
*/
|
|
39
|
+
materialDefinitionName: string;
|
|
40
|
+
/**
|
|
41
|
+
* 투입 — 커널 `RecipePart[]`(`material` · `qty` · `operation?`).
|
|
42
|
+
*
|
|
43
|
+
* `operation` 은 **이 투입이 들어가는 공정**이다. 없으면 오더 착수에 확보한다. 그 태그가 여기 있는
|
|
44
|
+
* 이유는 품목별 소요가 사는 자리가 레시피이기 때문이다.
|
|
45
|
+
*/
|
|
46
|
+
inputs: Record<string, unknown>[];
|
|
47
|
+
/** 산출 — 커널 `RecipePart[]`. 부산물이 있으면 여기 함께 든다. */
|
|
48
|
+
outputs: Record<string, unknown>[];
|
|
49
|
+
/**
|
|
50
|
+
* 이 산출물을 만드는 공정 경로 — `RouteDefinition.name`(커널 `RecipeDef.route`).
|
|
51
|
+
*
|
|
52
|
+
* 순서는 라우트가 소유하고 레시피는 그것을 가리킨다. 레시피에 단계 목록을 또 두면 두 목록이 어긋날 수
|
|
53
|
+
* 있고, 어긋났을 때 어느 쪽이 맞는지 말할 근거가 없다.
|
|
54
|
+
*/
|
|
55
|
+
route?: string;
|
|
56
|
+
/** 언제부터 유효한가 — 표준 `EffectiveStartDate`. */
|
|
57
|
+
effectiveStart?: Date;
|
|
58
|
+
/**
|
|
59
|
+
* 언제까지 유효한가 — 표준 `EffectiveEndDate`.
|
|
60
|
+
*
|
|
61
|
+
* **개정할 때 행을 고치지 않고 이 칸을 채우고 새 행을 세운다.** 고치면 그 레시피로 만든 지난 실적이
|
|
62
|
+
* 어느 배합으로 만든 것인지 잃는다.
|
|
63
|
+
*/
|
|
64
|
+
effectiveEnd?: Date;
|
|
65
|
+
createdAt?: Date;
|
|
66
|
+
updatedAt?: Date;
|
|
67
|
+
creator?: User;
|
|
68
|
+
creatorId?: string;
|
|
69
|
+
updater?: User;
|
|
70
|
+
updaterId?: string;
|
|
71
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RecipeDefinition = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const type_graphql_1 = require("type-graphql");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
9
|
+
/**
|
|
10
|
+
* 레시피 — **무엇으로 무엇을 만드나**. 커널 `RecipeDef` 와 같은 모양이다.
|
|
11
|
+
*
|
|
12
|
+
* ── 이 자리가 답하는 것 ──────────────────────────────────────────────────
|
|
13
|
+
* 「이 품목은 무엇으로 만드나」와 「이 품목을 만드는 것은 어느 레시피인가」다. 둘째가 MES 가 물어야 했던
|
|
14
|
+
* 것인데 물을 곳이 없었다.
|
|
15
|
+
*
|
|
16
|
+
* ── 품목에 지시서를 달지 않는 이유 ───────────────────────────────────────
|
|
17
|
+
* ISA-95 에는 자재 명세가 붙는 자리가 둘이고 뜻이 다르다(§`RecipePart.operation`).
|
|
18
|
+
*
|
|
19
|
+
* · `ProcessSegment` — 그 자리가 **할 수 있는 일**. 품목에 매이지 않는다 → `OperationDefinition`
|
|
20
|
+
* · `OperationsSegment` — 그 **품목을 만드는 절차**의 한 단계. 품목에 매인다 → **레시피가 그 범위다**
|
|
21
|
+
*
|
|
22
|
+
* 그래서 품목→지시서를 이으려면 그 사이에 레시피가 있어야 한다. 공정에 품목을 붙이면 범주 오류이고,
|
|
23
|
+
* 실 데이터가 그 크기를 보였다 — 「배합」에 품목별 BOM 을 붙이면 배합을 지나는 **모든** 로트가 자재
|
|
24
|
+
* 631종 36톤을 먹는다.
|
|
25
|
+
*
|
|
26
|
+
* ── 같은 품목에 레시피가 둘일 수 있다 ────────────────────────────────────
|
|
27
|
+
* 대체 레시피가 정상이다(첫 실 연동에서 경로가 둘 이상인 레시피가 306/471). 그래서 **품목이 레시피를
|
|
28
|
+
* 가리키지 않고 레시피가 품목을 가리킨다** — 품목은 하나를 고를 수 없다.
|
|
29
|
+
*/
|
|
30
|
+
let RecipeDefinition = class RecipeDefinition {
|
|
31
|
+
};
|
|
32
|
+
exports.RecipeDefinition = RecipeDefinition;
|
|
33
|
+
tslib_1.__decorate([
|
|
34
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
35
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID),
|
|
36
|
+
tslib_1.__metadata("design:type", String)
|
|
37
|
+
], RecipeDefinition.prototype, "id", void 0);
|
|
38
|
+
tslib_1.__decorate([
|
|
39
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
40
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain),
|
|
41
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
42
|
+
], RecipeDefinition.prototype, "domain", void 0);
|
|
43
|
+
tslib_1.__decorate([
|
|
44
|
+
(0, typeorm_1.RelationId)((recipe) => recipe.domain),
|
|
45
|
+
tslib_1.__metadata("design:type", String)
|
|
46
|
+
], RecipeDefinition.prototype, "domainId", void 0);
|
|
47
|
+
tslib_1.__decorate([
|
|
48
|
+
(0, typeorm_1.Column)(),
|
|
49
|
+
(0, type_graphql_1.Field)(),
|
|
50
|
+
tslib_1.__metadata("design:type", String)
|
|
51
|
+
], RecipeDefinition.prototype, "name", void 0);
|
|
52
|
+
tslib_1.__decorate([
|
|
53
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
54
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
55
|
+
tslib_1.__metadata("design:type", String)
|
|
56
|
+
], RecipeDefinition.prototype, "description", void 0);
|
|
57
|
+
tslib_1.__decorate([
|
|
58
|
+
(0, typeorm_1.Column)(),
|
|
59
|
+
(0, type_graphql_1.Field)(),
|
|
60
|
+
tslib_1.__metadata("design:type", String)
|
|
61
|
+
], RecipeDefinition.prototype, "materialDefinitionName", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, typeorm_1.Column)({ type: 'simple-json' }),
|
|
64
|
+
(0, type_graphql_1.Field)(type => String, { description: 'RecipePart[] as JSON text — material · qty · operation?' }),
|
|
65
|
+
tslib_1.__metadata("design:type", Array)
|
|
66
|
+
], RecipeDefinition.prototype, "inputs", void 0);
|
|
67
|
+
tslib_1.__decorate([
|
|
68
|
+
(0, typeorm_1.Column)({ type: 'simple-json' }),
|
|
69
|
+
(0, type_graphql_1.Field)(type => String, { description: 'RecipePart[] as JSON text' }),
|
|
70
|
+
tslib_1.__metadata("design:type", Array)
|
|
71
|
+
], RecipeDefinition.prototype, "outputs", void 0);
|
|
72
|
+
tslib_1.__decorate([
|
|
73
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
74
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
75
|
+
tslib_1.__metadata("design:type", String)
|
|
76
|
+
], RecipeDefinition.prototype, "route", void 0);
|
|
77
|
+
tslib_1.__decorate([
|
|
78
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
79
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
80
|
+
tslib_1.__metadata("design:type", Date
|
|
81
|
+
/**
|
|
82
|
+
* 언제까지 유효한가 — 표준 `EffectiveEndDate`.
|
|
83
|
+
*
|
|
84
|
+
* **개정할 때 행을 고치지 않고 이 칸을 채우고 새 행을 세운다.** 고치면 그 레시피로 만든 지난 실적이
|
|
85
|
+
* 어느 배합으로 만든 것인지 잃는다.
|
|
86
|
+
*/
|
|
87
|
+
)
|
|
88
|
+
], RecipeDefinition.prototype, "effectiveStart", void 0);
|
|
89
|
+
tslib_1.__decorate([
|
|
90
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
91
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
92
|
+
tslib_1.__metadata("design:type", Date)
|
|
93
|
+
], RecipeDefinition.prototype, "effectiveEnd", void 0);
|
|
94
|
+
tslib_1.__decorate([
|
|
95
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
96
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
97
|
+
tslib_1.__metadata("design:type", Date)
|
|
98
|
+
], RecipeDefinition.prototype, "createdAt", void 0);
|
|
99
|
+
tslib_1.__decorate([
|
|
100
|
+
(0, typeorm_1.UpdateDateColumn)(),
|
|
101
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
102
|
+
tslib_1.__metadata("design:type", Date)
|
|
103
|
+
], RecipeDefinition.prototype, "updatedAt", void 0);
|
|
104
|
+
tslib_1.__decorate([
|
|
105
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
106
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
107
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
108
|
+
], RecipeDefinition.prototype, "creator", void 0);
|
|
109
|
+
tslib_1.__decorate([
|
|
110
|
+
(0, typeorm_1.RelationId)((recipe) => recipe.creator),
|
|
111
|
+
tslib_1.__metadata("design:type", String)
|
|
112
|
+
], RecipeDefinition.prototype, "creatorId", void 0);
|
|
113
|
+
tslib_1.__decorate([
|
|
114
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
115
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
116
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
117
|
+
], RecipeDefinition.prototype, "updater", void 0);
|
|
118
|
+
tslib_1.__decorate([
|
|
119
|
+
(0, typeorm_1.RelationId)((recipe) => recipe.updater),
|
|
120
|
+
tslib_1.__metadata("design:type", String)
|
|
121
|
+
], RecipeDefinition.prototype, "updaterId", void 0);
|
|
122
|
+
exports.RecipeDefinition = RecipeDefinition = tslib_1.__decorate([
|
|
123
|
+
(0, typeorm_1.Entity)({ name: 'ops_recipe_definition' }),
|
|
124
|
+
(0, typeorm_1.Index)('ix_ops_recipe_definition_0', (recipe) => [recipe.domain, recipe.name], { unique: true })
|
|
125
|
+
/* 「이 품목을 만드는 레시피」로 묻는 질의가 이 축으로 온다 — MES 가 지시를 받을 때마다 부른다. */
|
|
126
|
+
,
|
|
127
|
+
(0, typeorm_1.Index)('ix_ops_recipe_definition_1', (recipe) => [recipe.domain, recipe.materialDefinitionName]),
|
|
128
|
+
(0, type_graphql_1.ObjectType)({ description: 'Recipe — what a material is made of and through which route (kernel RecipeDef).' })
|
|
129
|
+
], RecipeDefinition);
|
|
130
|
+
//# sourceMappingURL=recipe-definition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe-definition.js","sourceRoot":"","sources":["../../../server/service/recipe/recipe-definition.ts"],"names":[],"mappings":"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","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 레시피 — **무엇으로 무엇을 만드나**. 커널 `RecipeDef` 와 같은 모양이다.\n *\n * ── 이 자리가 답하는 것 ──────────────────────────────────────────────────\n * 「이 품목은 무엇으로 만드나」와 「이 품목을 만드는 것은 어느 레시피인가」다. 둘째가 MES 가 물어야 했던\n * 것인데 물을 곳이 없었다.\n *\n * ── 품목에 지시서를 달지 않는 이유 ───────────────────────────────────────\n * ISA-95 에는 자재 명세가 붙는 자리가 둘이고 뜻이 다르다(§`RecipePart.operation`).\n *\n * · `ProcessSegment` — 그 자리가 **할 수 있는 일**. 품목에 매이지 않는다 → `OperationDefinition`\n * · `OperationsSegment` — 그 **품목을 만드는 절차**의 한 단계. 품목에 매인다 → **레시피가 그 범위다**\n *\n * 그래서 품목→지시서를 이으려면 그 사이에 레시피가 있어야 한다. 공정에 품목을 붙이면 범주 오류이고,\n * 실 데이터가 그 크기를 보였다 — 「배합」에 품목별 BOM 을 붙이면 배합을 지나는 **모든** 로트가 자재\n * 631종 36톤을 먹는다.\n *\n * ── 같은 품목에 레시피가 둘일 수 있다 ────────────────────────────────────\n * 대체 레시피가 정상이다(첫 실 연동에서 경로가 둘 이상인 레시피가 306/471). 그래서 **품목이 레시피를\n * 가리키지 않고 레시피가 품목을 가리킨다** — 품목은 하나를 고를 수 없다.\n */\n@Entity({ name: 'ops_recipe_definition' })\n@Index('ix_ops_recipe_definition_0', (recipe: RecipeDefinition) => [recipe.domain, recipe.name], { unique: true })\n/* 「이 품목을 만드는 레시피」로 묻는 질의가 이 축으로 온다 — MES 가 지시를 받을 때마다 부른다. */\n@Index('ix_ops_recipe_definition_1', (recipe: RecipeDefinition) => [recipe.domain, recipe.materialDefinitionName])\n@ObjectType({ description: 'Recipe — what a material is made of and through which route (kernel RecipeDef).' })\nexport class RecipeDefinition {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((recipe: RecipeDefinition) => recipe.domain)\n domainId?: string\n\n /** 레시피 키 — 오더의 `recipeKey` 가 이 값을 가리킨다(커널 `RecipeDef.key`). */\n @Column()\n @Field()\n name: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n /**\n * **무엇을 만드나** — `MaterialDefinition.name`.\n *\n * `outputs` 안에도 있지만 이 칸을 따로 둔다. 「이 품목을 만드는 레시피」를 JSON 안을 뒤져 찾으면\n * 다섯 드라이버에서 색인이 서지 않고, 그 질의는 MES 가 지시를 받을 때마다 부른다.\n *\n * 산출이 여럿인 레시피(부산물)에서는 **주 산출**을 적는다. 나머지는 `outputs` 가 든다.\n */\n @Column()\n @Field()\n materialDefinitionName: string\n\n /**\n * 투입 — 커널 `RecipePart[]`(`material` · `qty` · `operation?`).\n *\n * `operation` 은 **이 투입이 들어가는 공정**이다. 없으면 오더 착수에 확보한다. 그 태그가 여기 있는\n * 이유는 품목별 소요가 사는 자리가 레시피이기 때문이다.\n */\n @Column({ type: 'simple-json' })\n @Field(type => String, { description: 'RecipePart[] as JSON text — material · qty · operation?' })\n inputs: Record<string, unknown>[]\n\n /** 산출 — 커널 `RecipePart[]`. 부산물이 있으면 여기 함께 든다. */\n @Column({ type: 'simple-json' })\n @Field(type => String, { description: 'RecipePart[] as JSON text' })\n outputs: Record<string, unknown>[]\n\n /**\n * 이 산출물을 만드는 공정 경로 — `RouteDefinition.name`(커널 `RecipeDef.route`).\n *\n * 순서는 라우트가 소유하고 레시피는 그것을 가리킨다. 레시피에 단계 목록을 또 두면 두 목록이 어긋날 수\n * 있고, 어긋났을 때 어느 쪽이 맞는지 말할 근거가 없다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n route?: string\n\n /** 언제부터 유효한가 — 표준 `EffectiveStartDate`. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveStart?: Date\n\n /**\n * 언제까지 유효한가 — 표준 `EffectiveEndDate`.\n *\n * **개정할 때 행을 고치지 않고 이 칸을 채우고 새 행을 세운다.** 고치면 그 레시피로 만든 지난 실적이\n * 어느 배합으로 만든 것인지 잃는다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveEnd?: Date\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((recipe: RecipeDefinition) => recipe.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((recipe: RecipeDefinition) => recipe.updater)\n updaterId?: string\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ListParam } from '@things-factory/shell';
|
|
2
|
+
import { MasterPatch } from '../master-crud';
|
|
3
|
+
import { RecipeDefinition } from './recipe-definition';
|
|
4
|
+
import { RouteDefinition } from './route-definition';
|
|
5
|
+
/**
|
|
6
|
+
* 레시피·라우트 축을 채우고 고친다.
|
|
7
|
+
*
|
|
8
|
+
* 규칙은 `master-crud.ts` 한 곳에 있다. 여기는 축의 이름만 붙인다 — 다른 열셋과 같은 모양이다.
|
|
9
|
+
*
|
|
10
|
+
* **개정할 때 행을 고치지 않고 유효 기간의 끝을 채운다.** 고치면 그 레시피로 만든 지난 실적이 어느
|
|
11
|
+
* 배합으로 만든 것인지 잃는다.
|
|
12
|
+
*/
|
|
13
|
+
export declare class RecipeDefinitionList {
|
|
14
|
+
items: RecipeDefinition[];
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class RouteDefinitionList {
|
|
18
|
+
items: RouteDefinition[];
|
|
19
|
+
total: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class RecipeDefinitionResolver {
|
|
22
|
+
recipeDefinitions(params: ListParam, context: ResolverContext): Promise<RecipeDefinitionList>;
|
|
23
|
+
updateMultipleRecipeDefinition(patches: MasterPatch[], context: ResolverContext): Promise<RecipeDefinition[]>;
|
|
24
|
+
}
|
|
25
|
+
export declare class RouteDefinitionResolver {
|
|
26
|
+
routeDefinitions(params: ListParam, context: ResolverContext): Promise<RouteDefinitionList>;
|
|
27
|
+
updateMultipleRouteDefinition(patches: MasterPatch[], context: ResolverContext): Promise<RouteDefinition[]>;
|
|
28
|
+
}
|