Files
Pier_v1/tests/abut-name.test.ts
T
ypshimandClaude Haiku 5.5 3922b330d5 feat(abut): 교대 복합명(A1-완도 등)과 가져오기 문제 목록
교대 이름을 전체 명칭으로 해석하고, 엑셀 가져오기 문제를 목록으로 모은다.
교각 이름과의 중복을 저장 파일 검사에서도 막는다(checkStoredAbutments).
검사 스크립트 verify-abut-names · verify-abut-workflow 포함.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-06 13:14:53 +09:00

129 lines
6.5 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as XLSX from 'xlsx';
import { describe, expect, it } from 'vitest';
import { parseAbutments } from '../src/core/abut/parse';
import { checkStoredAbutments, parseProject, serializeProject, SCHEMA_VERSION } from '../src/core/project';
import { abutNameError } from '../src/core/abut/name';
import { groupAbutImportIssues } from '../src/core/abut/import-issues';
import { structureOrder } from '../src/core/abut/order';
import { loadP1 } from './helpers';
const file = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
function workbook(name: string) {
const wb = XLSX.read(readFileSync(file), { type: 'buffer' });
for (const sheet of wb.SheetNames) {
const rows = XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[sheet], { header: 1, defval: null });
for (const row of rows) if (row[0] === 'A1') row[0] = name;
wb.Sheets[sheet] = XLSX.utils.aoa_to_sheet(rows);
}
return wb;
}
describe('full abutment display names are preserved across import and project reopen', () => {
it.each(['A1-완도', 'A2-완도', '완도 A1 (상행) #2'])('connects all IPs and bearings for %s', (name) => {
const original = parseAbutments(workbook('A1')).abutments[0];
const result = parseAbutments(workbook(name));
expect(result.issues).toEqual([]);
expect(result.abutments).toEqual([{ ...original, name }]);
expect(result.abutments[0].type).toBe('A1'); // The name must not choose the structural type.
});
it('uses the same trimmed full name in info, IP and bearing tables', () => {
const result = parseAbutments(workbook(' A1-완도 '));
expect(result.issues).toEqual([]);
expect(result.abutments[0].name).toBe('A1-완도');
expect(result.abutments[0].bearings).toHaveLength(5);
});
it('reopens full names and imported defaults without losing identity', () => {
const base = parseAbutments(workbook('A1')).abutments[0];
const records = [{ ...base, name: 'A1-완도' }, { ...base, name: 'A2-완도', type: 'A2' as const }];
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: '복합명', bridge: '완도', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: '입력.xlsx', sheets: ['Pier', 'IP', 'Bearing'] },
piers: [{ id: 'P1', input: loadP1() }],
abutments: records,
abutDefaults: records,
history: [],
});
const reopened = parseProject(text);
expect(reopened.abutments).toEqual(records);
expect(reopened.abutDefaults).toEqual(records);
});
});
describe('ambiguous and damaged names remain errors', () => {
it.each(['', ' ', 'A1\n완도', 'A1\u0000완도', null, 123])('rejects an empty, non-string or control-character name: %s', (name) => {
expect(abutNameError(name)).not.toBeNull();
const rec = parseAbutments(workbook('A1')).abutments[0];
expect(() => checkStoredAbutments([{ ...rec, name }])).toThrow();
});
it('rejects duplicate info rows instead of connecting to the first definition', () => {
const wb = workbook('A1-완도');
const rows = wb.SheetNames.flatMap((sheet) => XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[sheet], { header: 1, defval: null }));
const info = rows.find((row) => row[0] === 'A1-완도' && row[1] === 'A1')!;
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([['교대명', '형식'], info]), '중복정보');
const result = parseAbutments(wb);
expect(result.abutments).toEqual([]);
expect(result.issues.filter((issue) => issue.code === 'AP09')).toHaveLength(2);
const rec = parseAbutments(workbook('A1')).abutments[0];
expect(() => checkStoredAbutments([{ ...rec, name: 'A1-완도' }, { ...rec, name: ' A1-완도 ' }])).toThrow('중복');
});
it('rejects collisions with pier names in input and project validation', () => {
expect(parseAbutments(workbook('P1'), ['P1']).issues.some((issue) => issue.code === 'AP09')).toBe(true);
const rec = parseAbutments(workbook('A1')).abutments[0];
expect(() => checkStoredAbutments([{ ...rec, name: 'P1' }], ['P1'])).toThrow('중복');
});
it('rejects invalid workbook names and groups every dependent row under the root', () => {
const result = parseAbutments(workbook('A1\n완도'));
expect(result.abutments).toEqual([]);
const groups = groupAbutImportIssues(result.issues);
expect(groups).toHaveLength(1);
expect(groups[0].primary.map((issue) => issue.code)).toEqual(['AP01']);
expect(groups[0].related).toHaveLength(8); // Three IPs and five bearings remain traceable.
});
it('keeps genuine orphan references visible without dropping any rows', () => {
const wb = workbook('A1-완도');
for (const sheet of wb.SheetNames) {
const rows = XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[sheet], { header: 1, defval: null });
for (const row of rows) if (row[0] === 'A1-완도' && row[1] === 'A1') row[0] = '다른 교대';
wb.Sheets[sheet] = XLSX.utils.aoa_to_sheet(rows);
}
const result = parseAbutments(wb);
const groups = groupAbutImportIssues(result.issues);
const missing = groups.find((group) => group.name === 'A1-완도')!;
expect(missing.primary[0].code).toBe('AP04');
expect(missing.primary[0].message).toContain('8개 행');
expect(missing.related).toHaveLength(8);
expect(groups.some((group) => group.name === '다른 교대' && group.primary.some((issue) => issue.code === 'AP05'))).toBe(true);
});
it('does not merge errors for different names or promote warnings to errors', () => {
const groups = groupAbutImportIssues([
{ code: 'AP02', level: 'E', pier: 'A1-완도', message: '형식 오류' },
{ code: 'AP04', level: 'E', pier: 'A1-완도', message: 'IP 3행' },
{ code: 'AP04', level: 'E', pier: 'A2-완도', message: 'IP 4행' },
{ code: 'AP07', level: 'W', pier: 'A3-완도', message: '높이 차이' },
]);
expect(groups.map((group) => group.name)).toEqual(['A1-완도', 'A2-완도', 'A3-완도']);
expect(groups[2].primary[0].level).toBe('W');
expect(groups[2].related).toEqual([]);
});
it('sorts full names naturally inside type groups, ignoring suffix numbers as pier indices', () => {
const records = [
{ name: 'A2-1', type: 'A1' as const },
{ name: 'A1-20', type: 'A1' as const },
{ name: '완도 종점', type: 'A2' as const },
];
expect(structureOrder(records, ['P1']).map((entry) => entry.name)).toEqual(['A1-20', 'A2-1', 'P1', '완도 종점']);
expect(records[0].name).toBe('A2-1');
});
});