- 발행일
OID 트리를 낮에 고정하고 밤에 되돌렸다 — 일곱 시간짜리 결정과 서버가 정하게 된 버킷
OID 트리를 낮에 고정하고 밤에 되돌렸다
OID 검색 화면의 조직도는 노드를 누르면 그 노드를 뿌리로 다시 그립니다. 드릴다운이에요. jsTree에서 조직도로 갈아탄 뒤로 쭉 그랬습니다. 트리가 화면에 안 들어오던 시절에 폭을 절약하려고 둔 수단이었어요.
9월 4일 하루 동안 이 동작이 두 번 바뀌었습니다. 오후 2시 33분에 드릴다운을 뺐고, 밤 9시 45분에 다시 넣었어요. 이 글은 그 일곱 시간의 기록입니다. 결론만 보면 "원래대로"라서 커밋 로그에는 아무것도 안 남은 것처럼 보이는데, 되돌리면서 한 가지가 구조적으로 바뀌었습니다.
전날, 잎을 트리 밖으로 내보냈다
발단은 9월 3일 낮의 커밋(8bac18d8)입니다. 조직도는 폭 문제를 격자로 풀어 뒀는데, 그게 높이 문제를 남겼어요. 설계 문서에 남긴 수치가 이렇습니다.
DataElement아래 688건을 4열로 눕히면 172줄이고,max-h-[26rem]상자 안에서 계층은 맨 위 두 줄만 남는다. 사용자가 트리를 여는 이유(지금 어디에 있는가)가 목록에 잡아먹히는 셈이다.
그래서 잎만 매달린 노드는 트리 안에서 펼치지 않고, 그 잎을 트리 카드 아래에 별도 카드 목록으로 열기로 했습니다. 트리는 메시지·데이터 프레임·데이터 엘리먼트라는 버킷 세 개에서 끝나요. 유형 칩 네 개(전체 + 셋)가 그 버킷을 가리킵니다.
버킷 판정은 이랬습니다.
// 자식이 전부 잎인 노드인지 — 이 판정 하나가 드릴다운과 목록을 가른다
function isLeafBucket(node) {
const children = node?.children;
if (!Array.isArray(children) || children.length === 0) return false;
return children.every(
(child) => !Array.isArray(child.children) || child.children.length === 0,
);
}
버킷 이름을 적어 두지 않고 모양으로 추론한 거예요. "트리 모양이 바뀌어도 규칙이 따라간다"는 게 이유였습니다. 이 한 줄이 저녁에 문제가 됩니다.
오후 2시 33분, 모양을 고정했다
잎이 밖으로 나가고 높이 제한까지 걷어내자 계층 9개가 한눈에 들어왔습니다. 그러자 기획에서 하나를 더 짚었어요. "선택한 노드가 달라지면 화면이 달라지는 것을 하지 말고, 트리 모양을 유지하라."
말이 됩니다. 드릴다운은 폭을 절약하려는 수단이었는데 폭 문제가 이미 없어요. 남은 건 노드를 고를 때마다 그림이 통째로 바뀌는 비용뿐입니다. 뿌리가 바뀌고, 조상이 잘려 나가고, 형제 가지가 사라지고, 방금 본 그림을 잃어요.
게이트 하나로 막았습니다(d85cc692).
// 모양이 고정된 트리 — 노드를 골라도 뿌리를 갈아 끼우지 않는다(기획 결정 2026-09-04).
// 잎을 트리 밖 목록으로 내보낸 뒤 계층이 버킷에서 끝나 한 화면에 다 들어오므로, 드릴다운은
// 얻는 것 없이 고를 때마다 그림이 통째로 바뀌는 값만 치렀다.
function isFixedShapeTree() {
return document.getElementById("classTree")?.dataset.fixedTree === "true";
}
노드 클릭·경로 칩·상세의 링크가 전부 setTreeRoot()로 모이니까 거기 한 곳에서 "옮긴다"를 "고른다"로 바꿨어요. 관리 화면에서는 고른 자리를 ?tree_root=에 남겨서 저장·삭제 뒤 새로고침해도 그 자리로 돌아오게 했습니다. 이때 ?tree_root=의 뜻이 "뿌리"에서 "고른 자리"로 바뀌었어요. 스펙 11건을 모양 불변 계약으로 다시 썼습니다.
오후 4시 24분에는 등록 신청 화면의 상위 OID 트리에도 같은 계약을 폈습니다(9c220121). 커밋 메시지에 "공개·관리에 이어 마지막 화면"이라고 적었어요. 경로 바도 지웠습니다. 고른 자리는 위의 상위 OID 상자가 이미 보여 주니까요. 툴팁의 "클릭하면 펼칩니다"도 지웠습니다. 고정 뒤로는 어느 화면도 안 펼치니까.
이 시점의 설계 문서에 제가 직접 써 둔 문장이 있습니다.
⚠️ 모양을 고정하면 '들어가서 보는' 길이 통째로 없어진다. 잎 목록이 안 받는 노드는 화면에서 도달할 방법이 남지 않는다.
함정을 알고 있었어요. 다만 "지금 데이터에는 그런 손자가 없다"고 판단했습니다. dev 데이터 기준으로는 맞는 말이었습니다.
밤 9시 45분, 되돌렸다
배포 서버에 올릴 판이 정해졌습니다. 전날 낮의 잎 카드 목록 판까지.(설계 문서에는 9월 2일로 적혀 있는데, 커밋은 3일입니다.) 메시지·데이터 프레임·데이터 엘리먼트만 카드 목록이고, Standard 쪽은 옛날처럼 하위로 계속 내려가는 판이에요.
문제는 운영 데이터였습니다. 운영 트리에는 Standard › ITS › ITSK-00114 › VehicleSpeed 같은 가지가 있어요. dev에는 없던 가지입니다(운영 미러가 DataDictionary 잎만 심었거든요). 고정된 트리에서는 이 가지 아래로 내려갈 길이 없었습니다. 낮에 문서에 적어 둔 그 함정이 운영에서는 "지금" 걸리는 것이었어요.
그리고 되돌리자마자 두 번째 문제가 드러났습니다. isLeafBucket()은 "자식이 전부 잎"이면 버킷으로 봅니다. ITSK-00114는 잎 하나(VehicleSpeed)를 달고 있어요. 그래서 운영에서는 표준 노드 하나가 버킷으로 잡혀 유형 칩에 올랐습니다. 접힌 ITS도 버킷 색 큰 상자로 섰고요.
모양으로 추론하던 판정을 서버 플래그로 바꿨습니다(64478206).
# OID 검색·관리 트리에서 잎을 **트리 밖 카드 목록**으로 내보내는 버킷 — 위 세 발행 단위뿐이다
# (기획 결정 2026-09-04). 그 밖의 가지(Standard 아래 ITS 등)는 예전대로 드릴다운으로 내려간다.
# '자식이 전부 잎인 노드' 로 추론하면 운영의 `ITSK-00114` 처럼 잎 하나를 단 표준 노드까지
# 카드 버킷이 되고 유형 칩에 오른다. 트리 payload 가 `card_bucket` 으로 표시해 준다.
OID_CARD_BUCKET_DOTS = tuple(
f"{AUTO_OID_PARENT_DOT}.{arc}" for arc in AUTO_OID_BUCKET_ARCS.values()
)
function isCardBucket(node) {
return Boolean(node?.data?.card_bucket);
}
이름이 아니라 점 표기법(dot)으로 집습니다. OID 자동 부여가 같은 이유로 dot을 쓰고 있었어요. 운영의 노드 표기가 dev와 달랐거든요. 큰 상자와 유형 색도 카드 버킷에만 주고, 폭 때문에 접힌 다른 가지는 옛 파란 상자에 +N 배지로 돌아갔습니다.
| 동작 | 낮(고정) | 밤(복원) |
|---|---|---|
| 자식 있는 노드 클릭 | 선택만 옮김 | 그 노드를 뿌리로 다시 그림 |
| 카드 버킷 클릭 | 잎 카드 목록 | 잎 카드 목록 — 그대로 |
| 버킷 판정 | isLeafBucket — 자식이 전부 잎 | 서버 플래그 card_bucket |
?tree_root= | 고른 자리 | 드릴다운 뿌리(옛 뜻) |
data-fixed-tree와 isFixedShapeTree()는 삭제했습니다.
신청 화면은 반만 되돌렸다
밤 10시 13분에 등록 신청 화면도 드릴다운으로 돌렸습니다(621a0e18). 경로 바도 돌아왔어요. 다만 검색 화면과 똑같이 하진 않았습니다. 카드 버킷 셋은 내려가지 않고 상위 OID로 확정만 됩니다. 잎은 상위 OID가 될 수 없고, 이 화면에는 잎 카드 목록이 없어서 내려가면 수백 개 격자가 서거든요.
첫 그림도 손봤습니다. 신청 화면은 폼 폭이 47.5rem이라 트리 상자가 650px인데, 그 폭으로는 버킷 단이 접혀서 검색 화면(1,164px)과 첫 그림이 달랐어요.
// 단, 카드 버킷 단까지는 늘 그린다 — 그 위를 접으면 유형 셋이 보이지 않아 어느 화면에서든
// 첫 그림이 달라진다(신청 화면은 폼이 47.5rem 이라 버킷 단이 접혔다). 모자라는 폭은
// 상자의 가로 스크롤이 받는다.
const floorDepth = Math.max(1, cardBucketDepth(rootList));
for (let depth = maxDataDepth(rootList); depth >= floorDepth; depth--) {
접기의 바닥을 두니 같은 18개 노드가 서고, 모자라는 폭은 가로 스크롤이 받습니다. 오른쪽 DataElement(3) 상자가 30px 남짓 잘려 보이는 게 그 값이에요.
낮에 찍은 작성 예시 PDF의 OID 1단계 캡처도 다시 찍었습니다(732fe6f7). 그런데 캡처만 바꾸고 템플릿 글은 고정 트리 시절 그대로 둬서, 6분 뒤에 글도 고쳤어요(53323cff). 하위 수 +104가 +105로 바뀌는 식의 작은 차이였는데, 그림과 글이 다르면 예시가 아니라 오답지가 됩니다.
검증
- 단위 테스트
CardBucketFlagTests— iso부터ITSK-00114 › VehicleSpeed까지 심고, 플래그가 참인 노드가 정확히{Message, DataFrame, DataElement}인지,ITS·ITSK-00114·DataDictionary가 거짓인지 확인합니다. 낮의 추론 판정으로는 이 테스트가 깨집니다. - e2e
oid-search-tree-drilldown.spec.js10건 — 낮에 모양 불변으로 고쳐 쓴 것을 드릴다운 계약으로 되돌리고, "Standard 아래 표준 노드는 카드 목록이 아니라 드릴다운으로 내려간다"(유형 칩이 4개인지)를 더했습니다. - 관리 화면
admin/oid-state-label.spec.js8건 — 잎 판정을 이름이 아니라data-oid-bucket속성으로 집도록 바꿨습니다. - 신청 화면은 스펙이 없어 스크립트로 실측했습니다.
Standard › ITS › ITSK-00114로 내려가며 상위 OID가1.2.410.200053.1.2.114로 확정되는 것, DataDictionary에서 Message를 누르면 뿌리는 그대로인 채 상위만 바뀌는 것. 이후 회귀 잠금 스펙oid-request-parent-tree.spec.js를 추가했습니다. - 운영 가지 다섯 개(
Regulation·ITS·ITSK-00001·ITSK-00114·VehicleSpeed)를 dev에 손으로 심어 확인했습니다.
남은 것 · 한계
- 하루에 두 번 바뀐 건 요구가 흔들려서가 아니라 데이터를 한쪽만 봐서입니다. 낮의 결정은 dev 데이터 기준으로는 옳았어요. 함정을 문서에 적어 두고도 "지금 데이터에는 없다"로 넘겼는데, 그 "지금 데이터"가 dev였습니다. 운영 트리 JSON을 먼저 봤으면 오후 2시의 커밋이 없었을 겁니다.
- 운영 가지를 dev에 심은 건 손으로 한 겁니다. 시드에도 미러 명령에도 없어요. dev를 새로 만들면 다시 사라지고, 그러면 위 검증의 절반은 다시 못 돕니다.
?tree_root=의 뜻이 하루 안에 두 번 바뀌었습니다. 낮에 "고른 자리"가 됐다가 밤에 "뿌리"로 돌아왔어요. 그 사이에 누가 북마크했을 리는 없지만, 같은 파라미터에 다른 뜻을 얹었던 흔적이 스펙과 문서에 남아 있습니다.- 신청 화면과 검색 화면의 트리는 여전히 배선이 다릅니다. 렌더러만 같이 쓰고 클릭 처리는 각자예요. 그래서 검색 쪽을 고정할 때 신청 쪽은 별도 커밋이 필요했고, 되돌릴 때도 마찬가지였습니다. 이건 조직도로 갈아탈 때 "같은 데이터에 UI가 두 벌"이라고 적어 둔 부채가 그대로 있는 겁니다.
- 신청 화면 첫 그림의 30px 잘림은 고친 게 아니라 받아들인 겁니다.
관련 글: OID 트리를 jsTree에서 조직도로 갈아탔다 · 선반영이 드러낸 도메인 문제 두 케이스 · dev의 OID 번호를 운영과 같게 심었다 · 운영 PDF에서 한글이 통째로 빠졌다