feat(import): 엑셀의 프로젝트명 · 교량명을 불러오기 창에 자동 입력

새 입력 양식(운곡교AA.xlsx, 운곡교(완도,광주)데이터)의 '형상정보' B1 '프로젝트명:' → C1,
B2 '교량명:' → C2를 읽어 불러오기 창의 프로젝트명 · 교량명 칸을 채운다.
- core/project-info.ts: 모든 시트의 앞 20행 × 10열에서 라벨을 찾고, 같은 칸 ':' 뒤나 오른쪽 4칸 안의 값을 씀.
  라벨이 없는 엑셀(운곡교P1입력.xlsx)은 아무것도 읽지 않음
- 칸이 비었거나 앞서 자동으로 채운 값일 때만 채움. 사용자가 입력한 값은 덮어쓰지 않음(샘플 이름은 자동 값으로 취급)
- 파일 줄에 읽은 위치 표시: "프로젝트명 · 교량명: 엑셀 형상정보!C1 · 형상정보!C2"
- 테스트 4개, 브라우저: AA 자동 입력 → 직접 고친 프로젝트명은 다음 파일에서도 유지 → 이름 없는 샘플은 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 16:29:11 +09:00
1 parent bff1502ecf
commit c9e1cc6eaa
3 files changed
+113 -2

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
/**
* 프로젝트명 · 교량명 from the input workbook (2026-11-06). Pure.
*
* The new input form (운곡교AA.xlsx, 운곡교(완도,광주)데이터) has them at the top of '형상정보': B1 '프로젝트명:' → C1,
* B2 '교량명:' → C2. The label is searched in the first 20 rows × 10 columns of every sheet (form layouts move), and the
* value is the text after the colon in the same cell, else the first non-empty cell to its right (up to 4 columns).
* Workbooks without the labels (운곡교P1입력.xlsx) give nothing, and the dialog keeps what the user typed.
*/
import * as XLSX from 'xlsx';
export interface ProjectInfo {
projectName?: string;
bridgeName?: string;
/** Where the values were read, e.g. "형상정보!C1 · C2". */
source?: string;
}
const LABELS: { key: 'projectName' | 'bridgeName'; re: RegExp }[] = [
{ key: 'projectName', re: /^\s*프로젝트\s*명\s*[::]?\s*(.*)$/ },
{ key: 'bridgeName', re: /^\s*교량\s*명\s*[::]?\s*(.*)$/ },
];
const ROWS = 20, COLS = 10, LOOK_RIGHT = 4;
export function readProjectInfo(wb: XLSX.WorkBook): ProjectInfo {
const info: ProjectInfo = {};
const where: string[] = [];
for (const sheetName of wb.SheetNames) {
const ws = wb.Sheets[sheetName];
if (!ws) continue;
const at = (r: number, c: number) => {
const cell = ws[XLSX.utils.encode_cell({ r, c })];
const v = cell?.v;
return v === undefined || v === null ? '' : String(v).trim();
};
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const text = at(r, c);
if (!text) continue;
for (const { key, re } of LABELS) {
if (info[key] !== undefined) continue;
const m = re.exec(text);
if (!m) continue;
let value = m[1].trim(), col = c;
for (let k = 1; !value && k <= LOOK_RIGHT; k++) { value = at(r, c + k); col = c + k; }
// A neighbouring label is not a value (e.g. an empty form row).
if (!value || LABELS.some((l) => l.re.test(value))) continue;
info[key] = value;
where.push(`${sheetName}!${XLSX.utils.encode_cell({ r, c: col })}`);
}
}
if (info.projectName !== undefined && info.bridgeName !== undefined) break;
}
if (where.length) info.source = where.join(' · ');
return info;
}