feat(abut): 교대 복합명(A1-완도 등)과 가져오기 문제 목록
교대 이름을 전체 명칭으로 해석하고, 엑셀 가져오기 문제를 목록으로 모은다. 교각 이름과의 중복을 저장 파일 검사에서도 막는다(checkStoredAbutments). 검사 스크립트 verify-abut-names · verify-abut-workflow 포함. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c23173a1a
commit
3922b330d5
8 files changed
+610
-13
No files matched your search
@@ -0,0 +1,35 @@
|
||||
import type { Issue } from '../types';
|
||||
|
||||
export interface AbutImportIssueGroup {
|
||||
name?: string;
|
||||
primary: Issue[];
|
||||
related: Issue[];
|
||||
}
|
||||
|
||||
/** Keep root errors visible while retaining every dependent row in a disclosure. */
|
||||
export function groupAbutImportIssues(issues: readonly Issue[]): AbutImportIssueGroup[] {
|
||||
const groups: { name?: string; issues: Issue[] }[] = [];
|
||||
const byName = new Map<string, { name: string; issues: Issue[] }>();
|
||||
for (const issue of issues) {
|
||||
if (issue.pier === undefined) {
|
||||
groups.push({ issues: [issue] });
|
||||
continue;
|
||||
}
|
||||
let group = byName.get(issue.pier);
|
||||
if (!group) {
|
||||
group = { name: issue.pier, issues: [] };
|
||||
byName.set(issue.pier, group);
|
||||
groups.push(group);
|
||||
}
|
||||
group.issues.push(issue);
|
||||
}
|
||||
return groups.map(({ name, issues: items }) => {
|
||||
const primary = items.filter((issue) => issue.code !== 'AP04');
|
||||
const related = items.filter((issue) => issue.code === 'AP04');
|
||||
if (!primary.length && related.length) primary.push({
|
||||
...related[0],
|
||||
message: `교대 ${name ?? ''}: 교대 정보 행이 없어 IP·받침 ${related.length}개 행을 연결하지 못했습니다. 전체 교대명을 확인하세요.`,
|
||||
});
|
||||
return { name, primary, related };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Shared display-name policy for Excel input and saved project records. */
|
||||
export function abutNameError(value: unknown): string | null {
|
||||
if (typeof value !== 'string' || !value.trim()) return '교대명을 입력하세요.';
|
||||
if (/[\u0000-\u001f\u007f-\u009f]/u.test(value)) return '교대명에는 줄바꿈·제어 문자를 사용할 수 없습니다.';
|
||||
return null;
|
||||
}
|
||||
@@ -9,11 +9,11 @@ export interface StructureRef {
|
||||
type?: 'A1' | 'A2';
|
||||
}
|
||||
|
||||
const numberOf = (name: string) => Number(name.replace(/\D/g, '')) || 0;
|
||||
const nameOrder = new Intl.Collator('ko', { numeric: true });
|
||||
|
||||
export function structureOrder(abutments: { name: string; type: 'A1' | 'A2' }[], piers: string[]): StructureRef[] {
|
||||
const start = abutments.filter((a) => a.type === 'A1').sort((a, b) => numberOf(a.name) - numberOf(b.name));
|
||||
const end = abutments.filter((a) => a.type === 'A2').sort((a, b) => numberOf(a.name) - numberOf(b.name));
|
||||
const start = abutments.filter((a) => a.type === 'A1').sort((a, b) => nameOrder.compare(a.name, b.name));
|
||||
const end = abutments.filter((a) => a.type === 'A2').sort((a, b) => nameOrder.compare(a.name, b.name));
|
||||
const others = abutments.filter((a) => a.type !== 'A1' && a.type !== 'A2');
|
||||
return [
|
||||
...start.map((a) => ({ kind: 'abut' as const, name: a.name, type: a.type })),
|
||||
|
||||
+19
-6
@@ -9,6 +9,7 @@ import * as XLSX from 'xlsx';
|
||||
import { parseDms } from '../dms';
|
||||
import { swapXYAzimuth } from '../coordinate-frame';
|
||||
import type { Issue } from '../types';
|
||||
import { abutNameError } from './name';
|
||||
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
|
||||
import type { AbutShape } from './shape';
|
||||
import type { AbutBearingRule } from './bearing-table';
|
||||
@@ -104,7 +105,7 @@ function readBlock(grid: Grid, header: number): { cells: Cell[]; row: number }[]
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
export function parseAbutments(wb: XLSX.WorkBook, reservedNames: readonly string[] = []): AbutParseResult {
|
||||
const issues: Issue[] = [];
|
||||
const info: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
const ips: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
@@ -124,12 +125,24 @@ export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
}
|
||||
|
||||
const abutments: AbutRecord[] = [];
|
||||
const counts = new Map<string, number>();
|
||||
for (const rec of info) {
|
||||
const name = String(rec.cells[0] ?? '').trim();
|
||||
counts.set(name, (counts.get(name) ?? 0) + 1);
|
||||
}
|
||||
const reserved = new Set(reservedNames.map((name) => name.trim()));
|
||||
for (const rec of info) {
|
||||
const c = rec.cells;
|
||||
const name = String(c[0] ?? '').trim();
|
||||
const rawName = String(c[0] ?? '');
|
||||
const name = rawName.trim();
|
||||
const where = `${rec.sheet} ${rec.row}행`;
|
||||
if (!/^A\d+$/.test(name)) {
|
||||
issues.push({ code: 'AP01', level: 'E', sheet: rec.sheet, message: `${where}: 교대명 "${String(c[0] ?? '')}"이 올바르지 않습니다 (A1, A2 …).` });
|
||||
const nameError = abutNameError(rawName);
|
||||
if (nameError) {
|
||||
issues.push({ code: 'AP01', level: 'E', pier: name, sheet: rec.sheet, message: `${where}: ${nameError}` });
|
||||
continue;
|
||||
}
|
||||
if ((counts.get(name) ?? 0) > 1 || reserved.has(name)) {
|
||||
issues.push({ code: 'AP09', level: 'E', pier: name, sheet: rec.sheet, message: `${where}: 구조물명 "${name}"이 중복되었습니다. 교각·교대마다 서로 다른 이름을 사용하세요.` });
|
||||
continue;
|
||||
}
|
||||
const type = String(c[1] ?? '').trim();
|
||||
@@ -165,7 +178,7 @@ export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
const name = String(rec.cells[0] ?? '').trim();
|
||||
const ab = abutments.find((a) => a.name === name);
|
||||
if (!ab) {
|
||||
issues.push({ code: 'AP04', level: 'E', sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
issues.push({ code: 'AP04', level: 'E', pier: name, sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
const point = { e: num(rec.cells[3]), n: num(rec.cells[2]) };
|
||||
@@ -180,7 +193,7 @@ export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
const name = String(c[0] ?? '').trim();
|
||||
const ab = abutments.find((a) => a.name === name);
|
||||
if (!ab) {
|
||||
issues.push({ code: 'AP04', level: 'E', sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
issues.push({ code: 'AP04', level: 'E', pier: name, sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
const h = num(c[8]);
|
||||
|
||||
+12
-4
@@ -2,6 +2,7 @@ import type { FckByMember, Material } from './rebar/common';
|
||||
import { checkStoredGroundModel, type GroundModelFile } from './ground-model';
|
||||
import type { PierInput } from './types';
|
||||
import type { AbutRecord } from './abut/parse';
|
||||
import { abutNameError } from './abut/name';
|
||||
import { normalizeBearingAzimuths } from './coordinate-frame';
|
||||
|
||||
/** Project file schema (spec §8). Bump only with a migration. */
|
||||
@@ -74,8 +75,9 @@ export function parseProject(text: string): ProjectFile {
|
||||
}
|
||||
}
|
||||
if (obj.groundModel !== undefined) checkStoredGroundModel(obj.groundModel);
|
||||
if (obj.abutments !== undefined) checkStoredAbutments(obj.abutments);
|
||||
if (obj.abutDefaults !== undefined) checkStoredAbutments(obj.abutDefaults);
|
||||
const pierNames = obj.piers.map((p) => p?.input?.name).filter((name): name is string => typeof name === 'string');
|
||||
if (obj.abutments !== undefined) checkStoredAbutments(obj.abutments, pierNames);
|
||||
if (obj.abutDefaults !== undefined) checkStoredAbutments(obj.abutDefaults, pierNames);
|
||||
obj.piers.forEach((p, i) => {
|
||||
if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`);
|
||||
const input = p.input;
|
||||
@@ -111,11 +113,17 @@ const finite = (v: unknown) => typeof v === 'number' && Number.isFinite(v);
|
||||
const isPoint = (v: unknown) => !!v && typeof v === 'object' && finite((v as { e?: unknown }).e) && finite((v as { n?: unknown }).n);
|
||||
|
||||
/** 교대 기록 check (AB): refuses a damaged abutment record with a message that names it. */
|
||||
export function checkStoredAbutments(list: unknown): void {
|
||||
export function checkStoredAbutments(list: unknown, reservedNames: readonly string[] = []): void {
|
||||
if (!Array.isArray(list)) throw new Error('교대 목록 형식이 맞지 않습니다.');
|
||||
const names = new Set(reservedNames.map((name) => name.trim()));
|
||||
list.forEach((a: Partial<AbutRecord>, i) => {
|
||||
const who = `교대 ${i + 1}(${String(a?.name)})`;
|
||||
if (!a || typeof a.name !== 'string' || !/^A\d+$/.test(a.name)) throw new Error(`${who}: 교대명이 맞지 않습니다.`);
|
||||
const nameError = abutNameError(a?.name);
|
||||
if (nameError) throw new Error(`${who}: ${nameError}`);
|
||||
const name = a.name!.trim();
|
||||
if (names.has(name)) throw new Error(`${who}: 구조물명 "${name}"이 중복되었습니다.`);
|
||||
names.add(name);
|
||||
a.name = name;
|
||||
if (a.type !== 'A1' && a.type !== 'A2') throw new Error(`${who}: 형식은 A1 또는 A2여야 합니다.`);
|
||||
for (const key of ['seatEl', 'foundationBottomEl', 'groundEl', 'pileLengthMm', 'skewDeg', 'slopeI'] as const) {
|
||||
if (!finite(a[key])) throw new Error(`${who}: ${key} 값이 맞지 않습니다.`);
|
||||
|
||||
Reference in new issue
Block a user