@things-factory/ops-master 10.1.28 → 10.1.31
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/master-options.d.ts +7 -0
- package/dist-client/pages/master/master-options.js +8 -1
- package/dist-client/pages/master/master-options.js.map +1 -1
- package/dist-client/pages/master/operational-location.js +6 -1
- package/dist-client/pages/master/operational-location.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/location/operational-location.d.ts +27 -0
- package/dist-server/service/location/operational-location.js +5 -0
- package/dist-server/service/location/operational-location.js.map +1 -1
- package/dist-server/service/personnel/personnel-class.d.ts +1 -1
- package/dist-server/service/personnel/personnel-class.js +1 -1
- package/dist-server/service/personnel/personnel-class.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/translations/en.json +3 -1
- package/translations/ja.json +6 -4
- package/translations/ko.json +3 -1
- package/translations/ms.json +6 -4
- package/translations/zh.json +6 -4
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
/** 자리의 계층 — ISA-95 장비 계층. 계약이 실행시 상수로 준다. */
|
|
20
20
|
export declare const LEVEL_OPTIONS: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* 자리의 **주인 영역** — 창고 · 야드 · 공장 · 에너지(ADR-0081). 계약이 실행시 상수로 준다.
|
|
23
|
+
*
|
|
24
|
+
* 비워 둘 수 있다. 비면 「모름」이고, 어느 제품의 재고에도 들지 않는다 — 타입을 보고 채우는 대체값을
|
|
25
|
+
* 두지 않는다(그 추론이 틀리는 날 남의 자리에 선 물건이 우리 재고가 된다).
|
|
26
|
+
*/
|
|
27
|
+
export declare const SYSTEM_OPTIONS: readonly string[];
|
|
21
28
|
/**
|
|
22
29
|
* 공정의 의도 — 운반 · 가공 · 체류.
|
|
23
30
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EQUIPMENT_LEVEL } from '@operato/ops-contract';
|
|
1
|
+
import { DOMAIN_SYSTEMS, EQUIPMENT_LEVEL } from '@operato/ops-contract';
|
|
2
2
|
/**
|
|
3
3
|
* 닫힌 값 집합 — **고르는 칸의 값은 커널에서 온다.**
|
|
4
4
|
*
|
|
@@ -19,6 +19,13 @@ import { EQUIPMENT_LEVEL } from '@operato/ops-contract';
|
|
|
19
19
|
*/
|
|
20
20
|
/** 자리의 계층 — ISA-95 장비 계층. 계약이 실행시 상수로 준다. */
|
|
21
21
|
export const LEVEL_OPTIONS = EQUIPMENT_LEVEL;
|
|
22
|
+
/**
|
|
23
|
+
* 자리의 **주인 영역** — 창고 · 야드 · 공장 · 에너지(ADR-0081). 계약이 실행시 상수로 준다.
|
|
24
|
+
*
|
|
25
|
+
* 비워 둘 수 있다. 비면 「모름」이고, 어느 제품의 재고에도 들지 않는다 — 타입을 보고 채우는 대체값을
|
|
26
|
+
* 두지 않는다(그 추론이 틀리는 날 남의 자리에 선 물건이 우리 재고가 된다).
|
|
27
|
+
*/
|
|
28
|
+
export const SYSTEM_OPTIONS = DOMAIN_SYSTEMS;
|
|
22
29
|
/**
|
|
23
30
|
* 공정의 의도 — 운반 · 가공 · 체류.
|
|
24
31
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"master-options.js","sourceRoot":"","sources":["../../../client/pages/master/master-options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"master-options.js","sourceRoot":"","sources":["../../../client/pages/master/master-options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAGvE;;;;;;;;;;;;;;;;;GAiBG;AAEH,6CAA6C;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAsB,eAAe,CAAA;AAE/D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB,cAAc,CAAA;AAE/D;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,SAAS,EAAE,OAAO,CAAU,CAAA;AAExE;;;;;GAKG;AACH,MAAM,mBAAmB,GAAsB,CAAC,GAAG,cAAc,CAAC,CAAA;AAClE,MAAM,mBAAmB,GAAsC,CAAC,WAAW,EAAE,SAAS,EAAE,OAAO,CAAsB,CAAA;AAErH,KAAK,mBAAmB,CAAA;AACxB,KAAK,mBAAmB,CAAA;AAExB;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,OAA0B;IAClD,OAAO,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,CAAA;AACzB,CAAC","sourcesContent":["import { DOMAIN_SYSTEMS, EQUIPMENT_LEVEL } from '@operato/ops-contract'\nimport type { OperationIntent } from '@operato/ops-contract'\n\n/**\n * 닫힌 값 집합 — **고르는 칸의 값은 커널에서 온다.**\n *\n * ── 왜 이 파일이 있나 ──────────────────────────────────────────────────────\n * 마스터 화면의 칸이 거의 다 자유 텍스트였다. `level` 에 `WorkCel` 이라고 쳐도 아무 일도 안 나고,\n * 저장되고, 그 뒤로 그 자리는 어떤 계산에도 안 잡힌다. 값이 정해져 있는 칸은 고르게 해야 한다.\n *\n * ── 목록을 여기에 손으로 적지 않는다 ────────────────────────────────────────\n * 화면에 값을 베껴 적으면 커널이 값을 하나 늘렸을 때 화면만 옛 목록을 보인다. 그래서 **계약이\n * 실행시 상수로 내주는 것은 그대로 가져다 쓴다.**\n *\n * 계약이 **타입으로만** 주는 것이 있다. 타입은 실행시에 사라지므로 목록을 만들 수 없다. 그때만\n * 거울을 두고, **컴파일러가 그 거울을 계약에 묶는다** — 계약 쪽이 값을 더하거나 빼면 이 파일이\n * 컴파일되지 않는다. 베껴 적은 것과 다른 점이 그것이다.\n *\n * 계약이 상수를 내주면 거울을 지운다. 아키텍트에게 올려 두었다(`EQUIPMENT_LEVEL` ·\n * `DISPOSITION_DECISION` 처럼 `OPERATION_INTENT` 도 상수로).\n */\n\n/** 자리의 계층 — ISA-95 장비 계층. 계약이 실행시 상수로 준다. */\nexport const LEVEL_OPTIONS: readonly string[] = EQUIPMENT_LEVEL\n\n/**\n * 자리의 **주인 영역** — 창고 · 야드 · 공장 · 에너지(ADR-0081). 계약이 실행시 상수로 준다.\n *\n * 비워 둘 수 있다. 비면 「모름」이고, 어느 제품의 재고에도 들지 않는다 — 타입을 보고 채우는 대체값을\n * 두지 않는다(그 추론이 틀리는 날 남의 자리에 선 물건이 우리 재고가 된다).\n */\nexport const SYSTEM_OPTIONS: readonly string[] = DOMAIN_SYSTEMS\n\n/**\n * 공정의 의도 — 운반 · 가공 · 체류.\n *\n * 계약은 `OperationIntent` 를 **타입**으로만 낸다. 아래 두 줄이 이 배열을 그 타입에 묶는다.\n */\nexport const INTENT_OPTIONS = ['transport', 'process', 'dwell'] as const\n\n/*\n * **양쪽으로 묶는다.**\n *\n * 위 줄만 있으면 계약이 값을 하나 빼도 안 깨진다(우리 것이 더 넓어질 뿐이다). 아래 줄만 있으면\n * 계약이 값을 하나 더해도 안 깨진다. 둘 다 있어야 어느 쪽이 움직여도 컴파일이 멈춘다.\n */\nconst _intentIsInContract: OperationIntent[] = [...INTENT_OPTIONS]\nconst _contractIsInIntent: (typeof INTENT_OPTIONS)[number][] = ['transport', 'process', 'dwell'] as OperationIntent[]\n\nvoid _intentIsInContract\nvoid _contractIsInIntent\n\n/**\n * 고르는 칸의 첫 줄은 **빈 값**이다.\n *\n * 안 넣으면 「아직 안 정했다」를 고를 수 없어서, 칸을 한 번 건드리면 되돌릴 수 없다. 비워 둘 수\n * 있는 칸에만 붙인다.\n */\nexport function withBlank(options: readonly string[]): string[] {\n return ['', ...options]\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import { customElement, state } from 'lit/decorators.js';
|
|
|
4
4
|
import { i18next } from '@operato/i18n';
|
|
5
5
|
import { navigate } from '@operato/shell';
|
|
6
6
|
import { EFFECTIVE, IDENTITY, MasterPageBase } from './master-page-base.js';
|
|
7
|
-
import { LEVEL_OPTIONS, withBlank } from './master-options.js';
|
|
7
|
+
import { LEVEL_OPTIONS, SYSTEM_OPTIONS, withBlank } from './master-options.js';
|
|
8
8
|
/**
|
|
9
9
|
* 로케이션 — 공장 · 라인 · 설비 앞 · 창고 칸.
|
|
10
10
|
*
|
|
@@ -117,6 +117,11 @@ let OpsMasterOperationalLocation = class OpsMasterOperationalLocation extends Ma
|
|
|
117
117
|
...IDENTITY,
|
|
118
118
|
{ name: 'level', label: 'equipment-level', type: 'select', options: withBlank(LEVEL_OPTIONS), width: 140 },
|
|
119
119
|
{ name: 'type', label: 'location-type', type: 'string', width: 140 },
|
|
120
|
+
/*
|
|
121
|
+
* 누구의 자리인가 — 한 현장에 제품이 둘 서면 재고가 이 값으로 갈린다(ADR-0081). 타입 옆에 둔다:
|
|
122
|
+
* 둘 다 「이 자리가 무엇인가」이고, 타입을 적는 사람이 주인도 안다.
|
|
123
|
+
*/
|
|
124
|
+
{ name: 'system', label: 'owning-system', type: 'select', options: withBlank(SYSTEM_OPTIONS), width: 110 },
|
|
120
125
|
{ name: 'capacity', label: 'capacity', type: 'number', width: 90 },
|
|
121
126
|
/*
|
|
122
127
|
* ── 나를 누가 쓰나 ────────────────────────────────────────────────
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operational-location.js","sourceRoot":"","sources":["../../../client/pages/master/operational-location.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { FetchOption } from '@operato/data-grist'\nimport { i18next } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\n\nimport { EFFECTIVE, IDENTITY, MasterPageBase, MasterSpec } from './master-page-base.js'\nimport { INTENT_OPTIONS, LEVEL_OPTIONS, withBlank } from './master-options.js'\n\n/** 서버가 주는 로케이션 한 줄 — 상위를 `parentId` 로 가리킨다. */\ntype LocationRecord = Record<string, any> & { id: string; name: string; parentId?: string | null }\n\n/**\n * 로케이션 — 공장 · 라인 · 설비 앞 · 창고 칸.\n *\n * ── 계층을 한 단씩 걷는다 ──────────────────────────────────────────────────\n * 로케이션은 계층이다. 공장 안에 라인이 있고 라인 안에 설비 자리가 있다. 평면 표로 늘어놓으면 그 구조가\n * 사라진다 — 이름 순으로 섞여서 공장이 다섯째 줄에, 그 아래 라인이 넷째 줄에 온다.\n *\n * **그리고 이것이 틀리면 그 위의 집계가 전부 틀린다.** 실적이 「어디서」를 이 값으로 가리키므로, 라인\n * 단위 생산량과 공장 단위 가동률이 다 이 구조 위에서 계산된다.\n *\n * 그래서 이 화면은 지금 자리의 **자식만** 보인다. 어디에 있는지는 빵가루가 말하고, 형제가 많으면\n * 페이지네이션이 나눈다.\n *\n * ── 왜 트리 표가 아닌가 ────────────────────────────────────────────────────\n * `ox-grist` 에 트리 표가 있고 처음에 그것으로 만들었다. 두 가지가 걸렸다.\n *\n * **하나. 트리는 서브트리 전부를 메모리로 요구한다.** 지연 로딩이 없다 — 펼침은 플래그만 바꾼다.\n *\n * data-manipulator.ts:231 expandNode(record) { record.__expanded__ = true; this.refresh() }\n *\n * 페이지네이션으로 막을 수도 없다. 그 페이지네이션은 **뿌리를 센다**(`data-grist.ts:1095` 의 `idx` 는\n * 받은 배열 = 뿌리의 첨자). 로케이션의 뿌리는 사이트라서 공장 열 곳을 운영해도 열이고, 그 밑에 이십만이\n * 달려 있어도 페이지 수는 1이다. 창고는 랙 칸마다 한 줄이라 수만이 된다.\n *\n * 지연 로딩을 붙이는 길은 있다 — `expand-node` 가 `composed: true` 로 밖까지 나온다. 그러나 그것도\n * **깊이만** 막는다. 통로 하나에 랙 칸이 이천이면 그 자리를 여는 순간 이천이 온다. 너비까지 막으려면\n * 가지마다 「더 보기」 줄이 필요하고, 그 줄은 데이터 줄처럼 생겨서 `dirtyRecords` · `exportRecords` ·\n * `checkDirties` · 순번 · 저장마다 예외가 된다. 한 곳을 빠뜨리면 「1,900개 더」가 로케이션으로 저장된다.\n *\n * **둘. 개수에 따라 모습을 바꾸면 그 화면은 배울 수 없다.** 한동안 「적으면 트리, 많으면 드릴다운」으로\n * 두었다. 원칙처럼 들렸지만 실제로는 제품이 둘이 되는 것이었다 — 고객마다 다른 화면을 쓰고, 공장이\n * 커지면 쓰던 사람 밑에서 모습이 바뀐다. 링크도 트리에서는 뜻이 없고, 빵가루 · 순번 · 페이지네이션이\n * 있다 없다 한다. 그래서 하나로 정했다.\n *\n * ── 자리의 주인은 URL 이다 ─────────────────────────────────────────────────\n * 지금 어디를 보고 있나는 주소가 갖는다(`pageUpdated`). 그래야 자리를 붙여 보낼 수 있고, 뒤로 가기가\n * 한 단 올라가고, 새로 고쳐도 그 자리에 남는다.\n */\n@customElement('ops-master-operational-location')\nexport class OpsMasterOperationalLocation extends MasterPageBase {\n static styles = [\n ...MasterPageBase.styles,\n css`\n .scope {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 26px;\n }\n\n .scope button {\n display: flex;\n align-items: center;\n gap: 2px;\n border: none;\n background: none;\n padding: 2px 6px;\n border-radius: 4px;\n font: inherit;\n font-size: 12.5px;\n color: var(--ui-muted, #57676f);\n cursor: pointer;\n }\n\n .scope button:hover {\n background-color: var(--ui-hover, rgba(0, 0, 0, 0.05));\n color: var(--ui-ink, #17272f);\n }\n\n /* 지금 있는 자리. 선택할 곳이 아니므로 버튼으로 그리지 않는다. */\n .scope .here {\n padding: 2px 6px;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--ui-ink, #17272f);\n }\n\n .scope md-icon {\n --md-icon-size: 16px;\n font-size: 16px;\n width: 16px;\n height: 16px;\n }\n\n .scope .sep {\n color: var(--ui-line, #c9d4d9);\n font-size: 12px;\n }\n `\n ]\n\n protected get spec(): MasterSpec {\n return {\n key: 'operationalLocation',\n title: 'location',\n help: 'ops-master/operational-location',\n query: 'operationalLocations',\n mutation: 'updateMultipleOperationalLocation',\n columns: [\n ...IDENTITY,\n { name: 'level', label: 'equipment-level', type: 'select', options: withBlank(LEVEL_OPTIONS), width: 140 },\n { name: 'type', label: 'location-type', type: 'string', width: 140 },\n { name: 'capacity', label: 'capacity', type: 'number', width: 90 },\n /*\n * ── 나를 누가 쓰나 ────────────────────────────────────────────────\n *\n * 값의 주인이 이 표가 아니라 **저쪽 표**다. 그래서 여기서 못 고친다 — 바꾸려면 그쪽에서\n * 이 이름을 가리키게 하거나 떼면 된다.\n *\n * 넘치면 **몇이 안 보이는지 함께 낸다.** 오류보고없이 자르면 「셋이 있다」와 「서른이 있는데\n * 셋만 보인다」가 같아 보이고, 그 칸을 보고 사람이 판단을 내린다.\n */\n /*\n * ── 여기서 도는 근무조 ────────────────────────────────────────────\n *\n * **비어 있는 것이 정상이다.** 채우는 것은 위와 다를 때뿐이고, 아래 단은 물려받는다. 공장에\n * 한 줄 적으면 그 아래 라인·셀·설비가 전부 그것으로 돈다.\n *\n * 전에는 이 사실이 설비마다 사본으로 있었다 — 한 공장 22대가 같은 값을 22벌 들었다. 3교대를\n * 2교대로 바꾸려면 22줄을 고쳐야 했고, 하나를 빠뜨리면 그 설비만 옛 근무로 계산됐다.\n */\n {\n name: 'workShiftNames',\n label: 'work-shifts-here',\n type: 'reference-list',\n reference: { queryName: 'workShifts' },\n width: 200\n },\n { name: 'staff', label: 'staff-here', type: 'list', width: 180, readonly: true },\n { name: 'equipmentHere', label: 'equipment-here', type: 'list', width: 200, readonly: true },\n { name: 'assetsHere', label: 'assets-here', type: 'list', width: 200, readonly: true },\n { name: 'occupantOverflow', label: 'not-shown', type: 'number', width: 80, readonly: true },\n /*\n * 상위 로케이션은 컬럼으로 두지 않는다 — 빵가루가 그것을 말한다. 같은 사실을 두 군데 보이면 둘이\n * 어긋날 때 어느 쪽이 맞는지 알 수 없다.\n *\n * 다만 검색에는 남긴다(hidden). 「LINE-1 아래 있는 것」을 찾는 일이 있다.\n */\n {\n /* 이름이 `parentId` 인데 담기는 것은 상위 자리의 **이름**이다(`GUNSAN`). 커널이 이름으로 주소를 잡는다. */\n name: 'parentId',\n label: 'parent-location',\n type: 'reference',\n reference: { queryName: 'operationalLocations' },\n width: 150,\n hidden: true\n },\n /*\n * 하위 개수 — 표에는 「하위」 칸이 따로 그리므로 여기서는 감춘다. 이 줄이 있어야 질의가 그 값을\n * 가져온다(`master-page-base` 의 fetchHandler 가 spec 의 컬럼 이름으로 필드를 짓는다).\n */\n { name: 'childCount', label: 'sub-locations', type: 'number', width: 0, readonly: true, hidden: true },\n ...EFFECTIVE\n ]\n }\n }\n\n /**\n * 빵가루 — 최상위부터 지금 자리까지. 빈 배열이면 최상위를 보고 있다.\n *\n * **이 값의 주인은 URL 이다.** 여기 직접 넣지 않는다 — `enter` 는 주소만 바꾸고, 바뀐 주소를\n * `pageUpdated` 가 받아 이 값을 다시 세운다. 상태를 여기 두고 주소를 따로 갱신하면 둘이 어긋나는 날이\n * 온다.\n */\n @state() private path: LocationRecord[] = []\n\n /* 이 화면은 주소 조각을 **파고들기**에 쓴다 — 목록 좁히기로 겹쳐 쓰지 않는다. */\n protected get pathSegmentNarrowsList(): boolean {\n return false\n }\n\n async pageInitialized() {\n /*\n * 주소에 자리가 담겨 있으면 여기서도 세운다.\n *\n * `pageUpdated` 만 믿으면 **주소로 바로 들어온 경우가 빠진다** — 그 호출이 이 함수보다 먼저\n * 지나간다. 실제로 `/LINE-1` 로 들어갔을 때 최상위가 나왔다. [확인함] 2026-09-01\n */\n await this.syncPath(this.lifecycle?.resourceId)\n\n await super.pageInitialized()\n }\n\n /**\n * 주소가 바뀌면 빵가루를 다시 세운다.\n *\n * 주소에는 **지금 자리 하나만** 담는다(`.../ops-master-operational-location/AISLE-01`). 조상은\n * `parentId` 를 타고 올라가 알아낸다 — 경로 전체를 주소에 늘어놓으면 이름을 고칠 때마다 주소가 깨지고,\n * 사람이 읽을 수 없게 길어진다.\n */\n async pageUpdated(changes: any, after: any, before: any) {\n if (after?.resourceId !== before?.resourceId) {\n await this.syncPath(after?.resourceId)\n }\n\n await super.pageUpdated()\n }\n\n /** 주소가 가리키는 자리로 빵가루를 맞춘다. 자리가 없으면 최상위다. */\n private async syncPath(key?: string) {\n this.path = key ? await this.pathTo(key) : []\n }\n\n /** 이 자리부터 최상위까지 거슬러 올라가며 빵가루를 만든다. */\n private async pathTo(key: string): Promise<LocationRecord[]> {\n const chain: LocationRecord[] = []\n /* 자기 자신이나 서로를 상위로 참조하는 데이터가 있으면 여기서 무한히 반복된다. 밟은 자리를 기억해 끊는다. */\n const walked = new Set<string>()\n\n let cursor: string | null | undefined = key\n\n while (cursor && !walked.has(cursor)) {\n walked.add(cursor)\n\n const node = await this.lookup(cursor)\n if (!node) break\n\n chain.unshift(node)\n cursor = node.parentId\n }\n\n return chain\n }\n\n /** 한 자리를 찾는다. 아이디로 찾고, 없으면 이름으로 찾는다 — 주소에는 사람이 읽는 이름이 온다. */\n private async lookup(key: string): Promise<LocationRecord | undefined> {\n for (const field of ['id', 'name']) {\n const { records } = await super.fetchHandler({\n page: 1,\n limit: 1,\n filters: [{ name: field, operator: 'eq', value: key }]\n } as FetchOption)\n\n if (records.length) return records[0] as LocationRecord\n }\n\n return undefined\n }\n\n protected renderScope() {\n return html`\n <div class=\"scope\">\n <button @click=${() => this.enter(null)}>\n <md-icon>home</md-icon>\n ${i18next.t('label.all-locations')}\n </button>\n ${this.path.map((node, index) =>\n index === this.path.length - 1\n ? html`<span class=\"sep\">/</span><span class=\"here\">${node.name}</span>`\n : html`<span class=\"sep\">/</span><button @click=${() => this.enter(node)}>${node.name}</button>`\n )}\n </div>\n `\n }\n\n /**\n * 그 자리로 들어간다. `null` 이면 최상위로 돌아간다.\n *\n * **주소만 바꾼다.** 빵가루도 목록도 여기서 손대지 않는다 — 셸이 주소를 물고 `pageUpdated` 를 부르고,\n * 거기서 하나로 다시 만들어진다. 여기서 함께 고치면 뒤로 가기로 들어왔을 때와 다른 길이 생긴다.\n */\n private enter(node: LocationRecord | null) {\n /*\n * 아직 저장하지 않은 줄로는 들어갈 수 없다. 표 끝의 빈 줄에도 이 버추가 붙기 때문이다.\n *\n * [확인함] 이 가드가 없을 때 화면을 새로 열면 주소가 `.../operational-location/undefined` 가 됐다.\n * 무엇이 그 클릭을 냈는지는 **아직 못 찾았다** — 화면을 새로 열 때만 나고, 앱 안에서 옮겨 갈 때는\n * 나지 않는다. 원인을 모른 채 「고쳤다」고 하지 않는다. 다만 저장 안 된 줄로 들어가지 못하는 것은\n * 원인과 무관하게 맞는 규칙이다.\n */\n if (node && !node.id) {\n return\n }\n\n navigate(node ? `${this.lifecycle?.page}/${node.name || node.id}` : `${this.lifecycle?.page}`)\n }\n\n protected configOf() {\n const config = super.configOf()\n\n const enterColumn =\n /*\n * 「하위」 칸 — 개수와 들어가기를 함께 담는다.\n *\n * ── 개수가 뜻을 나른다 ─────────────────────────────────────────────\n * 처음에는 아이콘만 두었다. 그러니 그 버튼이 아무것도 알려 주지 않았다. 선택해 보기 전에는 안에\n * 무엇이 있는지, 있기는 한지 알 수 없다. 그리고 **왼쪽 끝의 화살은 트리의 펼침 버튼으로\n * 읽힌다.** 실제로는 다른 자리로 옮겨 가는데.\n *\n * 아이콘을 갈아 봤지만(굽은 화살은 표 안에서 지저분했다) 아이콘이 풀 문제가 아니었다. 칸에\n * 머리를 달고 개수를 넣으면 그림 없이도 읽힌다. 펼침 버튼에는 머리가 없다.\n *\n * 하위\n * 4 › 안에 넷이 있다. 누르면 그리로 간다\n * ― 안이 비었다. 누를 일이 없다\n *\n * 개수는 서버가 낸다(`childCount` 필드 리졸버).\n *\n * ── 왜 거터의 기본 버튼을 쓰지 않나 ─────────────────────────────────\n * 그 거터는 아이콘만 그리고, 누름은 표의 셀 클릭 경로를 탄다. 그 경로에는 이동이 견딜 수 없는\n * 관문이 둘 있다. [확인함] 2026-09-01\n *\n * data-grid-body-click-handler.ts:18 if (this.editTarget) return\n * data-grid-body-click-handler.ts:23 (e.target).closest('ox-grid-field')\n *\n * 앞은 **다른 칸이 편집 중이면 첫 누름을 가져간다.** 값을 고치다가 옮겨 가려면 두 번 선택해야\n * 한다. 뒤는 셀 경계를 스치면 칸을 못 찾는다(아이콘 한가운데를 선택해도 안 먹혔다).\n *\n * 그래서 자기 버튼을 그린다. 진짜 `button` 이라 키보드로도 닿고 초점 표시가 붙는다. 모양은\n * `data-grid-field` 의 `button.icon` 이 갖는다 — 렌더러의 결과는 셀의 그림자 안에 그려지므로\n * 이 화면의 스타일이 닿지 않는다.\n *\n * `gutterName` 은 남긴다 — `type: 'gutter'` 는 등록된 이름이 있어야 컴파일된다. 없이 두면\n * 컬럼 컴파일이 터져 **표에 컬럼이 하나도 안 남는다**.\n *\n * main.js Cannot destructure property 'name' of 'compiled' as it is undefined\n */\n /*\n * ── 거터가 아니라 보통 칸이다 ─────────────────────────────────────────\n * 처음에 `type: 'gutter'` 로 두었다. 렌더러를 덮기 쉬워서였는데, 거터는 여백 규칙이 달라\n * (`--grid-gutter-padding` 2px 대 보통 칸 8px 10px) 경계선도 없다. 그래서 왼쪽 셋이 한 덩어리로\n * 붙어 보였다. [확인함] 2026-09-02\n *\n * # w=20 padding 2px 0 구분선 없음\n * 선택 w=30 padding 2px 0 구분선 없음\n * 하위 w=62 padding 2px 0 구분선 없음 ← 여기\n * 이름 w=160 padding 8px 0 8px 10px 구분선 1px\n *\n * 이 칸은 **개수를 담는 데이터 칸**이다. 거터는 표의 장치(순번 · 선택 · 더러움 표식)를 위한\n * 자리다. 보통 칸으로 두면 여백과 경계선을 다른 칸과 같이 받는다.\n */\n {\n type: 'string',\n name: 'childCount',\n width: 62,\n /* 폭을 끌어 바꿀 것이 없고, 개수로 정렬하려면 서버가 그 축을 내야 한다. */\n resizable: false,\n sortable: false,\n header: i18next.t('field.sub-locations'),\n record: {\n align: 'center',\n renderer: (_value: any, _column: any, record: LocationRecord) => {\n /* 표 끝의 빈 줄에는 그리지 않는다 — 갈 곳이 없다. */\n if (!record?.id) return html``\n\n const count = record.childCount ?? 0\n\n /*\n * 비었으면 **아무것도 그리지 않는다.**\n *\n * 선택해도 빈 화면이 나오는 버튼은 없는 것이 낫다. 줄표를 둘까 했지만 그것은 「아직 안 세어\n * 봤다」와 구별하려는 표시인데, 개수는 줄과 함께 오므로 그런 상태가 없다. 그리고 계층의\n * 아래쪽은 대부분 잎이라 줄표가 화면을 덮는다.\n *\n * 비우면 **들어갈 것이 있는 줄만** 눈에 띈다 — 그게 이 칸의 목적이다.\n */\n if (!count) return html``\n\n return html`<button\n class=\"icon wide\"\n title=${i18next.t('button.enter-location')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.enter(record)\n }}\n >\n <span>${count}</span>\n <md-icon>chevron_right</md-icon>\n </button>`\n }\n },\n fixed: true\n }\n\n /*\n * 표준 거터 뒤, 이름 앞에 끼운다.\n *\n * `#`(순번)과 체크박스는 **모든 마스터 화면에 같은 자리로 있는** 거터다. 그 왼쪽에 이 화면만의 칸을\n * 두었더니 체크박스가 이 화면에서만 오른쪽으로 밀렸다 — 열두 화면을 오가며 고르는 작업에서 손이\n * 매번 다른 자리를 찾는다.\n *\n * 이름 바로 앞인 이유: 「하위」는 그 줄이 무엇인지(이름)에 딸린 사실이라 이름 옆에 있어야 읽힌다.\n */\n const columns = [...config.columns]\n const firstNonGutter = columns.findIndex((column: any) => column?.type !== 'gutter')\n\n columns.splice(firstNonGutter < 0 ? columns.length : firstNonGutter, 0, enterColumn)\n\n return {\n ...config,\n columns,\n emptyNote: {\n icon: 'segment',\n title: i18next.t('text.no-location-here'),\n description: i18next.t('text.no-location-here-description')\n }\n }\n }\n\n /**\n * 지금 자리의 자식만 받는다. 페이지네이션이 그대로 동작한다 — 세는 것이 형제이므로.\n */\n async fetchHandler(options: FetchOption) {\n const filters = options.filters || []\n\n /* 검색이나 필터가 걸려 있으면 범위를 푼다 — 이름을 아는 사람은 계층을 걷지 않는다. */\n if (filters.length > 0 || this.grist?.searchText) {\n return super.fetchHandler(options)\n }\n\n const here = this.path[this.path.length - 1]\n\n return super.fetchHandler({\n ...options,\n filters: [\n ...filters,\n here\n ? /*\n * ── `parentId` 는 아이디를 담기도 하고 이름을 담기도 한다 ──────────\n * 이 칸은 외래 키가 아니라 문자열이라 쓰는 쪽이 정하고, 지금 데이터는 **이름**을 담고 있다.\n * [확인함] 2026-09-01\n *\n * sqlite3 db.sqlite \"select name, parent_id from ops_operational_location\"\n * LINE-1|PLANT-1 ← 아이디가 아니라 이름\n * CUT-01|LINE-1\n *\n * 그래서 아이디만으로 걸면 **아무것도 나오지 않는다**(실제로 그렇게 나왔다). 둘을 다 받는다 —\n * 한쪽만 받으면 규약이 바뀌는 날 화면이 오류보고없이 빈다.\n *\n * 이 두 규약이 섞여 있는 것 자체는 이 화면이 풀 문제가 아니다. 커널과 초기 데이터 쪽 결정이 필요하다.\n */\n { name: 'parentId', operator: 'in', value: [here.id, here.name].filter(Boolean) }\n : /* 최상위 — 상위가 없는 줄들. */ { name: 'parentId', operator: 'is_null', value: '' }\n ]\n } as FetchOption)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"operational-location.js","sourceRoot":"","sources":["../../../client/pages/master/operational-location.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAGxD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAc,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAkB,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAK9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEI,IAAM,4BAA4B,GAAlC,MAAM,4BAA6B,SAAQ,cAAc;IAAzD;;QA4HL;;;;;;WAMG;QACc,SAAI,GAAqB,EAAE,CAAA;IAgR9C,CAAC;aAlZQ,WAAM,GAAG;QACd,GAAG,cAAc,CAAC,MAAM;QACxB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA+CF;KACF,AAlDY,CAkDZ;IAED,IAAc,IAAI;QAChB,OAAO;YACL,GAAG,EAAE,qBAAqB;YAC1B,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,iCAAiC;YACvC,KAAK,EAAE,sBAAsB;YAC7B,QAAQ,EAAE,mCAAmC;YAC7C,OAAO,EAAE;gBACP,GAAG,QAAQ;gBACX,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE;gBAC1G,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE;gBACpE;;;mBAGG;gBACH,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE;gBAC1G,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE;gBAClE;;;;;;;;mBAQG;gBACH;;;;;;;;mBAQG;gBACH;oBACE,IAAI,EAAE,gBAAgB;oBACtB,KAAK,EAAE,kBAAkB;oBACzB,IAAI,EAAE,gBAAgB;oBACtB,SAAS,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;oBACtC,KAAK,EAAE,GAAG;iBACX;gBACD,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAChF,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC5F,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACtF,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC3F;;;;;mBAKG;gBACH;oBACE,2EAA2E;oBAC3E,IAAI,EAAE,UAAU;oBAChB,KAAK,EAAE,iBAAiB;oBACxB,IAAI,EAAE,WAAW;oBACjB,SAAS,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;oBAChD,KAAK,EAAE,GAAG;oBACV,MAAM,EAAE,IAAI;iBACb;gBACD;;;mBAGG;gBACH,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;gBACtG,GAAG,SAAS;aACb;SACF,CAAA;IACH,CAAC;IAWD,oDAAoD;IACpD,IAAc,sBAAsB;QAClC,OAAO,KAAK,CAAA;IACd,CAAC;IAED,KAAK,CAAC,eAAe;QACnB;;;;;WAKG;QACH,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAE/C,MAAM,KAAK,CAAC,eAAe,EAAE,CAAA;IAC/B,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,KAAU,EAAE,MAAW;QACrD,IAAI,KAAK,EAAE,UAAU,KAAK,MAAM,EAAE,UAAU,EAAE,CAAC;YAC7C,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;QACxC,CAAC;QAED,MAAM,KAAK,CAAC,WAAW,EAAE,CAAA;IAC3B,CAAC;IAED,2CAA2C;IACnC,KAAK,CAAC,QAAQ,CAAC,GAAY;QACjC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/C,CAAC;IAED,sCAAsC;IAC9B,KAAK,CAAC,MAAM,CAAC,GAAW;QAC9B,MAAM,KAAK,GAAqB,EAAE,CAAA;QAClC,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAA;QAEhC,IAAI,MAAM,GAA8B,GAAG,CAAA;QAE3C,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAElB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACtC,IAAI,CAAC,IAAI;gBAAE,MAAK;YAEhB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YACnB,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAA;QACxB,CAAC;QAED,OAAO,KAAK,CAAA;IACd,CAAC;IAED,6DAA6D;IACrD,KAAK,CAAC,MAAM,CAAC,GAAW;QAC9B,KAAK,MAAM,KAAK,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;YACnC,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC;gBAC3C,IAAI,EAAE,CAAC;gBACP,KAAK,EAAE,CAAC;gBACR,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;aACxC,CAAC,CAAA;YAEjB,IAAI,OAAO,CAAC,MAAM;gBAAE,OAAO,OAAO,CAAC,CAAC,CAAmB,CAAA;QACzD,CAAC;QAED,OAAO,SAAS,CAAA;IAClB,CAAC;IAES,WAAW;QACnB,OAAO,IAAI,CAAA;;yBAEU,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;;YAEnC,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC;;UAElC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC9B,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAC5B,CAAC,CAAC,IAAI,CAAA,gDAAgD,IAAI,CAAC,IAAI,SAAS;YACxE,CAAC,CAAC,IAAI,CAAA,4CAA4C,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,WAAW,CACnG;;KAEJ,CAAA;IACH,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,IAA2B;QACvC;;;;;;;WAOG;QACH,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACrB,OAAM;QACR,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAChG,CAAC;IAES,QAAQ;QAChB,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAA;QAE/B,MAAM,WAAW;QACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAmCG;QACH;;;;;;;;;;;;;WAaG;QACH;YACE,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE,EAAE;YACT,+CAA+C;YAC/C,SAAS,EAAE,KAAK;YAChB,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC;YACxC,MAAM,EAAE;gBACN,KAAK,EAAE,QAAQ;gBACf,QAAQ,EAAE,CAAC,MAAW,EAAE,OAAY,EAAE,MAAsB,EAAE,EAAE;oBAC9D,mCAAmC;oBACnC,IAAI,CAAC,MAAM,EAAE,EAAE;wBAAE,OAAO,IAAI,CAAA,EAAE,CAAA;oBAE9B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,IAAI,CAAC,CAAA;oBAEpC;;;;;;;;uBAQG;oBACH,IAAI,CAAC,KAAK;wBAAE,OAAO,IAAI,CAAA,EAAE,CAAA;oBAEzB,OAAO,IAAI,CAAA;;sBAED,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;uBACjC,CAAC,CAAQ,EAAE,EAAE;wBAClB,CAAC,CAAC,eAAe,EAAE,CAAA;wBACnB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;oBACpB,CAAC;;sBAEK,KAAK;;sBAEL,CAAA;gBACZ,CAAC;aACF;YACD,KAAK,EAAE,IAAI;SACZ,CAAA;QAEH;;;;;;;;WAQG;QACH,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;QACnC,MAAM,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,MAAW,EAAE,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAA;QAEpF,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,EAAE,WAAW,CAAC,CAAA;QAEpF,OAAO;YACL,GAAG,MAAM;YACT,OAAO;YACP,SAAS,EAAE;gBACT,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;gBACzC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC;aAC5D;SACF,CAAA;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAAC,OAAoB;QACrC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAA;QAErC,qDAAqD;QACrD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC;YACjD,OAAO,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QACpC,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAE5C,OAAO,KAAK,CAAC,YAAY,CAAC;YACxB,GAAG,OAAO;YACV,OAAO,EAAE;gBACP,GAAG,OAAO;gBACV,IAAI;oBACF,CAAC,CAAC;;;;;;;;;;;;;yBAaG;wBACH,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;oBACnF,CAAC,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE;aAChF;SACa,CAAC,CAAA;IACnB,CAAC;;AA/QgB;IAAhB,KAAK,EAAE;;0DAAoC;AAnIjC,4BAA4B;IADxC,aAAa,CAAC,iCAAiC,CAAC;GACpC,4BAA4B,CAmZxC","sourcesContent":["import { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { FetchOption } from '@operato/data-grist'\nimport { i18next } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\n\nimport { EFFECTIVE, IDENTITY, MasterPageBase, MasterSpec } from './master-page-base.js'\nimport { INTENT_OPTIONS, LEVEL_OPTIONS, SYSTEM_OPTIONS, withBlank } from './master-options.js'\n\n/** 서버가 주는 로케이션 한 줄 — 상위를 `parentId` 로 가리킨다. */\ntype LocationRecord = Record<string, any> & { id: string; name: string; parentId?: string | null }\n\n/**\n * 로케이션 — 공장 · 라인 · 설비 앞 · 창고 칸.\n *\n * ── 계층을 한 단씩 걷는다 ──────────────────────────────────────────────────\n * 로케이션은 계층이다. 공장 안에 라인이 있고 라인 안에 설비 자리가 있다. 평면 표로 늘어놓으면 그 구조가\n * 사라진다 — 이름 순으로 섞여서 공장이 다섯째 줄에, 그 아래 라인이 넷째 줄에 온다.\n *\n * **그리고 이것이 틀리면 그 위의 집계가 전부 틀린다.** 실적이 「어디서」를 이 값으로 가리키므로, 라인\n * 단위 생산량과 공장 단위 가동률이 다 이 구조 위에서 계산된다.\n *\n * 그래서 이 화면은 지금 자리의 **자식만** 보인다. 어디에 있는지는 빵가루가 말하고, 형제가 많으면\n * 페이지네이션이 나눈다.\n *\n * ── 왜 트리 표가 아닌가 ────────────────────────────────────────────────────\n * `ox-grist` 에 트리 표가 있고 처음에 그것으로 만들었다. 두 가지가 걸렸다.\n *\n * **하나. 트리는 서브트리 전부를 메모리로 요구한다.** 지연 로딩이 없다 — 펼침은 플래그만 바꾼다.\n *\n * data-manipulator.ts:231 expandNode(record) { record.__expanded__ = true; this.refresh() }\n *\n * 페이지네이션으로 막을 수도 없다. 그 페이지네이션은 **뿌리를 센다**(`data-grist.ts:1095` 의 `idx` 는\n * 받은 배열 = 뿌리의 첨자). 로케이션의 뿌리는 사이트라서 공장 열 곳을 운영해도 열이고, 그 밑에 이십만이\n * 달려 있어도 페이지 수는 1이다. 창고는 랙 칸마다 한 줄이라 수만이 된다.\n *\n * 지연 로딩을 붙이는 길은 있다 — `expand-node` 가 `composed: true` 로 밖까지 나온다. 그러나 그것도\n * **깊이만** 막는다. 통로 하나에 랙 칸이 이천이면 그 자리를 여는 순간 이천이 온다. 너비까지 막으려면\n * 가지마다 「더 보기」 줄이 필요하고, 그 줄은 데이터 줄처럼 생겨서 `dirtyRecords` · `exportRecords` ·\n * `checkDirties` · 순번 · 저장마다 예외가 된다. 한 곳을 빠뜨리면 「1,900개 더」가 로케이션으로 저장된다.\n *\n * **둘. 개수에 따라 모습을 바꾸면 그 화면은 배울 수 없다.** 한동안 「적으면 트리, 많으면 드릴다운」으로\n * 두었다. 원칙처럼 들렸지만 실제로는 제품이 둘이 되는 것이었다 — 고객마다 다른 화면을 쓰고, 공장이\n * 커지면 쓰던 사람 밑에서 모습이 바뀐다. 링크도 트리에서는 뜻이 없고, 빵가루 · 순번 · 페이지네이션이\n * 있다 없다 한다. 그래서 하나로 정했다.\n *\n * ── 자리의 주인은 URL 이다 ─────────────────────────────────────────────────\n * 지금 어디를 보고 있나는 주소가 갖는다(`pageUpdated`). 그래야 자리를 붙여 보낼 수 있고, 뒤로 가기가\n * 한 단 올라가고, 새로 고쳐도 그 자리에 남는다.\n */\n@customElement('ops-master-operational-location')\nexport class OpsMasterOperationalLocation extends MasterPageBase {\n static styles = [\n ...MasterPageBase.styles,\n css`\n .scope {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 26px;\n }\n\n .scope button {\n display: flex;\n align-items: center;\n gap: 2px;\n border: none;\n background: none;\n padding: 2px 6px;\n border-radius: 4px;\n font: inherit;\n font-size: 12.5px;\n color: var(--ui-muted, #57676f);\n cursor: pointer;\n }\n\n .scope button:hover {\n background-color: var(--ui-hover, rgba(0, 0, 0, 0.05));\n color: var(--ui-ink, #17272f);\n }\n\n /* 지금 있는 자리. 선택할 곳이 아니므로 버튼으로 그리지 않는다. */\n .scope .here {\n padding: 2px 6px;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--ui-ink, #17272f);\n }\n\n .scope md-icon {\n --md-icon-size: 16px;\n font-size: 16px;\n width: 16px;\n height: 16px;\n }\n\n .scope .sep {\n color: var(--ui-line, #c9d4d9);\n font-size: 12px;\n }\n `\n ]\n\n protected get spec(): MasterSpec {\n return {\n key: 'operationalLocation',\n title: 'location',\n help: 'ops-master/operational-location',\n query: 'operationalLocations',\n mutation: 'updateMultipleOperationalLocation',\n columns: [\n ...IDENTITY,\n { name: 'level', label: 'equipment-level', type: 'select', options: withBlank(LEVEL_OPTIONS), width: 140 },\n { name: 'type', label: 'location-type', type: 'string', width: 140 },\n /*\n * 누구의 자리인가 — 한 현장에 제품이 둘 서면 재고가 이 값으로 갈린다(ADR-0081). 타입 옆에 둔다:\n * 둘 다 「이 자리가 무엇인가」이고, 타입을 적는 사람이 주인도 안다.\n */\n { name: 'system', label: 'owning-system', type: 'select', options: withBlank(SYSTEM_OPTIONS), width: 110 },\n { name: 'capacity', label: 'capacity', type: 'number', width: 90 },\n /*\n * ── 나를 누가 쓰나 ────────────────────────────────────────────────\n *\n * 값의 주인이 이 표가 아니라 **저쪽 표**다. 그래서 여기서 못 고친다 — 바꾸려면 그쪽에서\n * 이 이름을 가리키게 하거나 떼면 된다.\n *\n * 넘치면 **몇이 안 보이는지 함께 낸다.** 오류보고없이 자르면 「셋이 있다」와 「서른이 있는데\n * 셋만 보인다」가 같아 보이고, 그 칸을 보고 사람이 판단을 내린다.\n */\n /*\n * ── 여기서 도는 근무조 ────────────────────────────────────────────\n *\n * **비어 있는 것이 정상이다.** 채우는 것은 위와 다를 때뿐이고, 아래 단은 물려받는다. 공장에\n * 한 줄 적으면 그 아래 라인·셀·설비가 전부 그것으로 돈다.\n *\n * 전에는 이 사실이 설비마다 사본으로 있었다 — 한 공장 22대가 같은 값을 22벌 들었다. 3교대를\n * 2교대로 바꾸려면 22줄을 고쳐야 했고, 하나를 빠뜨리면 그 설비만 옛 근무로 계산됐다.\n */\n {\n name: 'workShiftNames',\n label: 'work-shifts-here',\n type: 'reference-list',\n reference: { queryName: 'workShifts' },\n width: 200\n },\n { name: 'staff', label: 'staff-here', type: 'list', width: 180, readonly: true },\n { name: 'equipmentHere', label: 'equipment-here', type: 'list', width: 200, readonly: true },\n { name: 'assetsHere', label: 'assets-here', type: 'list', width: 200, readonly: true },\n { name: 'occupantOverflow', label: 'not-shown', type: 'number', width: 80, readonly: true },\n /*\n * 상위 로케이션은 컬럼으로 두지 않는다 — 빵가루가 그것을 말한다. 같은 사실을 두 군데 보이면 둘이\n * 어긋날 때 어느 쪽이 맞는지 알 수 없다.\n *\n * 다만 검색에는 남긴다(hidden). 「LINE-1 아래 있는 것」을 찾는 일이 있다.\n */\n {\n /* 이름이 `parentId` 인데 담기는 것은 상위 자리의 **이름**이다(`GUNSAN`). 커널이 이름으로 주소를 잡는다. */\n name: 'parentId',\n label: 'parent-location',\n type: 'reference',\n reference: { queryName: 'operationalLocations' },\n width: 150,\n hidden: true\n },\n /*\n * 하위 개수 — 표에는 「하위」 칸이 따로 그리므로 여기서는 감춘다. 이 줄이 있어야 질의가 그 값을\n * 가져온다(`master-page-base` 의 fetchHandler 가 spec 의 컬럼 이름으로 필드를 짓는다).\n */\n { name: 'childCount', label: 'sub-locations', type: 'number', width: 0, readonly: true, hidden: true },\n ...EFFECTIVE\n ]\n }\n }\n\n /**\n * 빵가루 — 최상위부터 지금 자리까지. 빈 배열이면 최상위를 보고 있다.\n *\n * **이 값의 주인은 URL 이다.** 여기 직접 넣지 않는다 — `enter` 는 주소만 바꾸고, 바뀐 주소를\n * `pageUpdated` 가 받아 이 값을 다시 세운다. 상태를 여기 두고 주소를 따로 갱신하면 둘이 어긋나는 날이\n * 온다.\n */\n @state() private path: LocationRecord[] = []\n\n /* 이 화면은 주소 조각을 **파고들기**에 쓴다 — 목록 좁히기로 겹쳐 쓰지 않는다. */\n protected get pathSegmentNarrowsList(): boolean {\n return false\n }\n\n async pageInitialized() {\n /*\n * 주소에 자리가 담겨 있으면 여기서도 세운다.\n *\n * `pageUpdated` 만 믿으면 **주소로 바로 들어온 경우가 빠진다** — 그 호출이 이 함수보다 먼저\n * 지나간다. 실제로 `/LINE-1` 로 들어갔을 때 최상위가 나왔다. [확인함] 2026-09-01\n */\n await this.syncPath(this.lifecycle?.resourceId)\n\n await super.pageInitialized()\n }\n\n /**\n * 주소가 바뀌면 빵가루를 다시 세운다.\n *\n * 주소에는 **지금 자리 하나만** 담는다(`.../ops-master-operational-location/AISLE-01`). 조상은\n * `parentId` 를 타고 올라가 알아낸다 — 경로 전체를 주소에 늘어놓으면 이름을 고칠 때마다 주소가 깨지고,\n * 사람이 읽을 수 없게 길어진다.\n */\n async pageUpdated(changes: any, after: any, before: any) {\n if (after?.resourceId !== before?.resourceId) {\n await this.syncPath(after?.resourceId)\n }\n\n await super.pageUpdated()\n }\n\n /** 주소가 가리키는 자리로 빵가루를 맞춘다. 자리가 없으면 최상위다. */\n private async syncPath(key?: string) {\n this.path = key ? await this.pathTo(key) : []\n }\n\n /** 이 자리부터 최상위까지 거슬러 올라가며 빵가루를 만든다. */\n private async pathTo(key: string): Promise<LocationRecord[]> {\n const chain: LocationRecord[] = []\n /* 자기 자신이나 서로를 상위로 참조하는 데이터가 있으면 여기서 무한히 반복된다. 밟은 자리를 기억해 끊는다. */\n const walked = new Set<string>()\n\n let cursor: string | null | undefined = key\n\n while (cursor && !walked.has(cursor)) {\n walked.add(cursor)\n\n const node = await this.lookup(cursor)\n if (!node) break\n\n chain.unshift(node)\n cursor = node.parentId\n }\n\n return chain\n }\n\n /** 한 자리를 찾는다. 아이디로 찾고, 없으면 이름으로 찾는다 — 주소에는 사람이 읽는 이름이 온다. */\n private async lookup(key: string): Promise<LocationRecord | undefined> {\n for (const field of ['id', 'name']) {\n const { records } = await super.fetchHandler({\n page: 1,\n limit: 1,\n filters: [{ name: field, operator: 'eq', value: key }]\n } as FetchOption)\n\n if (records.length) return records[0] as LocationRecord\n }\n\n return undefined\n }\n\n protected renderScope() {\n return html`\n <div class=\"scope\">\n <button @click=${() => this.enter(null)}>\n <md-icon>home</md-icon>\n ${i18next.t('label.all-locations')}\n </button>\n ${this.path.map((node, index) =>\n index === this.path.length - 1\n ? html`<span class=\"sep\">/</span><span class=\"here\">${node.name}</span>`\n : html`<span class=\"sep\">/</span><button @click=${() => this.enter(node)}>${node.name}</button>`\n )}\n </div>\n `\n }\n\n /**\n * 그 자리로 들어간다. `null` 이면 최상위로 돌아간다.\n *\n * **주소만 바꾼다.** 빵가루도 목록도 여기서 손대지 않는다 — 셸이 주소를 물고 `pageUpdated` 를 부르고,\n * 거기서 하나로 다시 만들어진다. 여기서 함께 고치면 뒤로 가기로 들어왔을 때와 다른 길이 생긴다.\n */\n private enter(node: LocationRecord | null) {\n /*\n * 아직 저장하지 않은 줄로는 들어갈 수 없다. 표 끝의 빈 줄에도 이 버추가 붙기 때문이다.\n *\n * [확인함] 이 가드가 없을 때 화면을 새로 열면 주소가 `.../operational-location/undefined` 가 됐다.\n * 무엇이 그 클릭을 냈는지는 **아직 못 찾았다** — 화면을 새로 열 때만 나고, 앱 안에서 옮겨 갈 때는\n * 나지 않는다. 원인을 모른 채 「고쳤다」고 하지 않는다. 다만 저장 안 된 줄로 들어가지 못하는 것은\n * 원인과 무관하게 맞는 규칙이다.\n */\n if (node && !node.id) {\n return\n }\n\n navigate(node ? `${this.lifecycle?.page}/${node.name || node.id}` : `${this.lifecycle?.page}`)\n }\n\n protected configOf() {\n const config = super.configOf()\n\n const enterColumn =\n /*\n * 「하위」 칸 — 개수와 들어가기를 함께 담는다.\n *\n * ── 개수가 뜻을 나른다 ─────────────────────────────────────────────\n * 처음에는 아이콘만 두었다. 그러니 그 버튼이 아무것도 알려 주지 않았다. 선택해 보기 전에는 안에\n * 무엇이 있는지, 있기는 한지 알 수 없다. 그리고 **왼쪽 끝의 화살은 트리의 펼침 버튼으로\n * 읽힌다.** 실제로는 다른 자리로 옮겨 가는데.\n *\n * 아이콘을 갈아 봤지만(굽은 화살은 표 안에서 지저분했다) 아이콘이 풀 문제가 아니었다. 칸에\n * 머리를 달고 개수를 넣으면 그림 없이도 읽힌다. 펼침 버튼에는 머리가 없다.\n *\n * 하위\n * 4 › 안에 넷이 있다. 누르면 그리로 간다\n * ― 안이 비었다. 누를 일이 없다\n *\n * 개수는 서버가 낸다(`childCount` 필드 리졸버).\n *\n * ── 왜 거터의 기본 버튼을 쓰지 않나 ─────────────────────────────────\n * 그 거터는 아이콘만 그리고, 누름은 표의 셀 클릭 경로를 탄다. 그 경로에는 이동이 견딜 수 없는\n * 관문이 둘 있다. [확인함] 2026-09-01\n *\n * data-grid-body-click-handler.ts:18 if (this.editTarget) return\n * data-grid-body-click-handler.ts:23 (e.target).closest('ox-grid-field')\n *\n * 앞은 **다른 칸이 편집 중이면 첫 누름을 가져간다.** 값을 고치다가 옮겨 가려면 두 번 선택해야\n * 한다. 뒤는 셀 경계를 스치면 칸을 못 찾는다(아이콘 한가운데를 선택해도 안 먹혔다).\n *\n * 그래서 자기 버튼을 그린다. 진짜 `button` 이라 키보드로도 닿고 초점 표시가 붙는다. 모양은\n * `data-grid-field` 의 `button.icon` 이 갖는다 — 렌더러의 결과는 셀의 그림자 안에 그려지므로\n * 이 화면의 스타일이 닿지 않는다.\n *\n * `gutterName` 은 남긴다 — `type: 'gutter'` 는 등록된 이름이 있어야 컴파일된다. 없이 두면\n * 컬럼 컴파일이 터져 **표에 컬럼이 하나도 안 남는다**.\n *\n * main.js Cannot destructure property 'name' of 'compiled' as it is undefined\n */\n /*\n * ── 거터가 아니라 보통 칸이다 ─────────────────────────────────────────\n * 처음에 `type: 'gutter'` 로 두었다. 렌더러를 덮기 쉬워서였는데, 거터는 여백 규칙이 달라\n * (`--grid-gutter-padding` 2px 대 보통 칸 8px 10px) 경계선도 없다. 그래서 왼쪽 셋이 한 덩어리로\n * 붙어 보였다. [확인함] 2026-09-02\n *\n * # w=20 padding 2px 0 구분선 없음\n * 선택 w=30 padding 2px 0 구분선 없음\n * 하위 w=62 padding 2px 0 구분선 없음 ← 여기\n * 이름 w=160 padding 8px 0 8px 10px 구분선 1px\n *\n * 이 칸은 **개수를 담는 데이터 칸**이다. 거터는 표의 장치(순번 · 선택 · 더러움 표식)를 위한\n * 자리다. 보통 칸으로 두면 여백과 경계선을 다른 칸과 같이 받는다.\n */\n {\n type: 'string',\n name: 'childCount',\n width: 62,\n /* 폭을 끌어 바꿀 것이 없고, 개수로 정렬하려면 서버가 그 축을 내야 한다. */\n resizable: false,\n sortable: false,\n header: i18next.t('field.sub-locations'),\n record: {\n align: 'center',\n renderer: (_value: any, _column: any, record: LocationRecord) => {\n /* 표 끝의 빈 줄에는 그리지 않는다 — 갈 곳이 없다. */\n if (!record?.id) return html``\n\n const count = record.childCount ?? 0\n\n /*\n * 비었으면 **아무것도 그리지 않는다.**\n *\n * 선택해도 빈 화면이 나오는 버튼은 없는 것이 낫다. 줄표를 둘까 했지만 그것은 「아직 안 세어\n * 봤다」와 구별하려는 표시인데, 개수는 줄과 함께 오므로 그런 상태가 없다. 그리고 계층의\n * 아래쪽은 대부분 잎이라 줄표가 화면을 덮는다.\n *\n * 비우면 **들어갈 것이 있는 줄만** 눈에 띈다 — 그게 이 칸의 목적이다.\n */\n if (!count) return html``\n\n return html`<button\n class=\"icon wide\"\n title=${i18next.t('button.enter-location')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.enter(record)\n }}\n >\n <span>${count}</span>\n <md-icon>chevron_right</md-icon>\n </button>`\n }\n },\n fixed: true\n }\n\n /*\n * 표준 거터 뒤, 이름 앞에 끼운다.\n *\n * `#`(순번)과 체크박스는 **모든 마스터 화면에 같은 자리로 있는** 거터다. 그 왼쪽에 이 화면만의 칸을\n * 두었더니 체크박스가 이 화면에서만 오른쪽으로 밀렸다 — 열두 화면을 오가며 고르는 작업에서 손이\n * 매번 다른 자리를 찾는다.\n *\n * 이름 바로 앞인 이유: 「하위」는 그 줄이 무엇인지(이름)에 딸린 사실이라 이름 옆에 있어야 읽힌다.\n */\n const columns = [...config.columns]\n const firstNonGutter = columns.findIndex((column: any) => column?.type !== 'gutter')\n\n columns.splice(firstNonGutter < 0 ? columns.length : firstNonGutter, 0, enterColumn)\n\n return {\n ...config,\n columns,\n emptyNote: {\n icon: 'segment',\n title: i18next.t('text.no-location-here'),\n description: i18next.t('text.no-location-here-description')\n }\n }\n }\n\n /**\n * 지금 자리의 자식만 받는다. 페이지네이션이 그대로 동작한다 — 세는 것이 형제이므로.\n */\n async fetchHandler(options: FetchOption) {\n const filters = options.filters || []\n\n /* 검색이나 필터가 걸려 있으면 범위를 푼다 — 이름을 아는 사람은 계층을 걷지 않는다. */\n if (filters.length > 0 || this.grist?.searchText) {\n return super.fetchHandler(options)\n }\n\n const here = this.path[this.path.length - 1]\n\n return super.fetchHandler({\n ...options,\n filters: [\n ...filters,\n here\n ? /*\n * ── `parentId` 는 아이디를 담기도 하고 이름을 담기도 한다 ──────────\n * 이 칸은 외래 키가 아니라 문자열이라 쓰는 쪽이 정하고, 지금 데이터는 **이름**을 담고 있다.\n * [확인함] 2026-09-01\n *\n * sqlite3 db.sqlite \"select name, parent_id from ops_operational_location\"\n * LINE-1|PLANT-1 ← 아이디가 아니라 이름\n * CUT-01|LINE-1\n *\n * 그래서 아이디만으로 걸면 **아무것도 나오지 않는다**(실제로 그렇게 나왔다). 둘을 다 받는다 —\n * 한쪽만 받으면 규약이 바뀌는 날 화면이 오류보고없이 빈다.\n *\n * 이 두 규약이 섞여 있는 것 자체는 이 화면이 풀 문제가 아니다. 커널과 초기 데이터 쪽 결정이 필요하다.\n */\n { name: 'parentId', operator: 'in', value: [here.id, here.name].filter(Boolean) }\n : /* 최상위 — 상위가 없는 줄들. */ { name: 'parentId', operator: 'is_null', value: '' }\n ]\n } as FetchOption)\n }\n}\n"]}
|