/* * ★ 표현을 묶고 가른다 — 목록에 나올 것과 다른 표현에서 열리는 것. * * ── 왜 (2026-08-27 · 사용자 지시, UX 레인 요청) ───────────────────────────── * 「각 표현이 많아지게 되면 관리가 어려우니, 그루핑과 정렬 기능도 필요하다.」 * 「표현 인덱스에 포함될 표현과 팝업 같은 파생 표현이 구분될 수 있어야 한다.」 * * 칸 둘을 더한다. * * group 자유 문자열. 묶는 축이 현장마다 다르다(건물별·용도별·층별) * role index(기본) · derived. 닫힌 목록이다 */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { REPRESENTATION_ROLES } from '../server/service/twin-space/representation-role.js' const dir = (...p: string[]) => join(import.meta.dirname, '..', 'server', 'service', 'twin-space', ...p) const entity = readFileSync(dir('twin-space-representation.ts'), 'utf8') const resolver = readFileSync(dir('twin-space-resolver.ts'), 'utf8') test('★ 묶는 이름은 자유 문자열이다 — 묶는 축이 현장마다 다르다', () => { assert.match(entity, /group\?: string/, 'group 칸이 있어야 한다') /* 고정 목록으로 두면 현장마다 맞지 않는다 — 값을 검사하지 않는다. */ assert.doesNotMatch(resolver, /GROUPS as readonly|REPRESENTATION_GROUPS/, '묶는 이름을 목록으로 닫지 않는다') }) test('★ 그룹 자체의 순서는 칸으로 두지 않는다 — 그 그룹의 최소 seq 가 그 순서다', () => { /* 칸을 두면 그룹 이름을 바꿀 때 두 곳을 맞춰야 하고, 어긋나면 목록 순서가 오류 없이 달라진다. */ assert.doesNotMatch(entity, /groupSeq|groupOrder/, '그룹 순서 칸을 만들지 않는다') }) test('★ role 은 닫힌 목록이고, 모르는 값은 저장하지 않는다', () => { assert.deepEqual([...REPRESENTATION_ROLES], ['index', 'derived']) /* * 모르는 값을 그냥 저장하면 화면이 그것을 어느 쪽으로도 그리지 못하고, 그 표현이 목록에서 알리지 않고 * 사라진다. 저장하는 자리에서 거절한다. */ assert.match(resolver, /REPRESENTATION_ROLES as readonly string\[\]\)\.includes\(role\)/, '닫힌 목록으로 검사한다') assert.match(resolver, /unknown representation role/, '거절 사유를 말한다') }) test('★ 비어 있으면 index 다 — 흔한 쪽을 기본으로 둔다', () => { assert.match(entity, /role\?: string/, 'role 은 선택 칸이다') assert.match(entity, /default when absent/, '비어 있을 때의 뜻이 설명에 있어야 한다') /* 공백만 있는 값은 「없음」과 같다 — 그것을 값으로 저장하면 검사를 지나며 뜻이 없는 값이 남는다. */ assert.match(resolver, /String\(rep\.role\)\.trim\(\) === ''/, '공백만 있는 값은 비운다') assert.match(resolver, /String\(rep\.group\)\.trim\(\) === ''/, '묶는 이름도 같다') }) test('★ isPrimary 에 얹지 않는다 — 다른 질문에 답하는 칸이다', () => { /* * `isPrimary` 는 「현장 현황에 나올 하나」를 정한다. `role` 은 「목록에 나오나」를 정한다. 한 칸이 * 두 질문에 답하면 한쪽을 고칠 때 다른 쪽이 오류 없이 바뀐다. */ assert.match(entity, /isPrimary\?: boolean/, 'isPrimary 는 그대로 있어야 한다') assert.match(entity, /Kept separate from isPrimary/, '왜 따로 두는지 설명에 있어야 한다') }) test('★ 조회가 두 값을 실어 보낸다 — 화면이 묶을 수 있어야 한다', () => { assert.match(resolver, /group: rep\.group, role: rep\.role/, '표현 목록에 두 값을 싣는다') assert.match(resolver, /order: \{ group: 'ASC', seq: 'ASC' \}/, '묶은 이름 다음에 순서로 정렬한다') /* 빈 값이 앞에 오는지 뒤에 오는지는 드라이버마다 다르다 — 그 순서에 기대지 않는다. */ assert.match(resolver, /드라이버마다\s*\n?\s*\*?\s*다르므로/, '그 순서에 기대지 않는다는 것을 적어 둔다') })