feat(B03): 부속 파일 카드에 파일 속 값 표시·래스터 썸네일 점선 테두리
개수만 보이던 부속 파일 카드에 파일 안의 중요한 값을 함께 보임. - .shx — 도형 종류(폴리라인 등)와 범위 크기를 머리글에서 읽음. - .dbf — 속성 이름 목록을 머리글 서술자에서 읽음(앞 4개). - .cpg — 같은 세트 .dbf 의 첫 글자 속성을 그 인코딩으로 읽어 견본으로 보임. - .prj — 중앙자오선(투영 매개변수)과 길이 단위. proj 문자열 변환을 쓰지 않아 정보 손실 경고가 나지 않음. - 카드 표시 줄 수를 2 → 3 으로 늘리고, 길이 단위 표기를 m 으로 통일. - GeoTIFF 썸네일에 다른 미리보기와 같은 점선 테두리. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,9 +99,33 @@ export function readThumbnail(metadata: Record<string, unknown> | undefined): st
|
||||
return typeof raw === "string" && raw.startsWith("data:image/") ? raw : null;
|
||||
}
|
||||
|
||||
/** SHP/SHX 머리글의 도형 종류 코드 — 카드에 「무엇이 들었나」로 보인다. */
|
||||
const SHAPE_TYPE_LABELS: Record<number, string> = {
|
||||
0: "빈 도형",
|
||||
1: "점",
|
||||
3: "폴리라인",
|
||||
5: "폴리곤",
|
||||
8: "다중점",
|
||||
11: "점(3D)",
|
||||
13: "폴리라인(3D)",
|
||||
15: "폴리곤(3D)",
|
||||
18: "다중점(3D)",
|
||||
21: "점(M)",
|
||||
23: "폴리라인(M)",
|
||||
25: "폴리곤(M)",
|
||||
28: "다중점(M)",
|
||||
31: "복합면",
|
||||
};
|
||||
|
||||
/** 129.002890277778 → 「129.0029°E」. */
|
||||
function meridianLabel(degrees: number): string {
|
||||
const rounded = Math.abs(Math.round(degrees * 10000) / 10000);
|
||||
return `${rounded}°${degrees < 0 ? "W" : "E"}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 그릴 도형이 없는 부속 파일(shx·dbf·cpg·prj·tfw)에서 **한눈에 구분되는 값**을 뽑는다
|
||||
* (2026-09-04 사용자 지시). 그림 대신 이 값을 큰 글씨로 보인다.
|
||||
* 그릴 도형이 없는 부속 파일(shx·dbf·cpg·prj·tfw)에서 **파일 안의 중요한 값**을 뽑는다
|
||||
* (2026-09-04 사용자 지시 — 개수만이 아니라 내용이 보이게). 첫 줄은 굵게, 나머지는 작게.
|
||||
*/
|
||||
export function readFacts(metadata: Record<string, unknown> | undefined): string[] {
|
||||
if (!metadata) return [];
|
||||
@@ -109,15 +133,38 @@ export function readFacts(metadata: Record<string, unknown> | undefined): string
|
||||
const number = (value: unknown): number | null => finite(value);
|
||||
const text = (value: unknown): string | null =>
|
||||
typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
const round = (value: number): string => Math.round(value).toLocaleString();
|
||||
|
||||
// SHX — 도형 종류·개수와 그 도형이 덮는 범위.
|
||||
const shapes = number(metadata.shape_count);
|
||||
if (shapes !== null) facts.push(`도형 ${shapes.toLocaleString()}개`);
|
||||
if (shapes !== null) {
|
||||
const kind = SHAPE_TYPE_LABELS[number(metadata.shape_type) ?? -1];
|
||||
const count = `도형 ${shapes.toLocaleString()}개`;
|
||||
facts.push(kind ? `${kind} · ${count}` : count);
|
||||
const width = number(metadata.extent_width_m);
|
||||
const height = number(metadata.extent_height_m);
|
||||
if (width !== null && height !== null) facts.push(`범위 ${round(width)}×${round(height)} m`);
|
||||
}
|
||||
|
||||
// DBF — 레코드·속성 개수와 속성 이름(무엇이 든 표인지).
|
||||
const records = number(metadata.record_count);
|
||||
if (records !== null) facts.push(`레코드 ${records.toLocaleString()}개`);
|
||||
const fields = number(metadata.field_count);
|
||||
if (fields !== null) facts.push(`속성 ${fields}개`);
|
||||
if (records !== null) {
|
||||
const count = `레코드 ${records.toLocaleString()}개`;
|
||||
facts.push(fields !== null ? `${count} · 속성 ${fields}개` : count);
|
||||
}
|
||||
const names = Array.isArray(metadata.field_names)
|
||||
? metadata.field_names.filter((name): name is string => typeof name === "string")
|
||||
: [];
|
||||
if (names.length) facts.push(names.slice(0, 4).join(", ") + (names.length > 4 ? " …" : ""));
|
||||
|
||||
// CPG — 인코딩 이름과 그 인코딩으로 읽은 글자 견본(깨지면 눈에 보인다).
|
||||
const encoding = text(metadata.encoding);
|
||||
if (encoding) facts.push(`인코딩 ${encoding}`);
|
||||
const sample = text(metadata.encoding_sample);
|
||||
if (sample) facts.push(`견본 「${sample}」`);
|
||||
|
||||
// TFW — 픽셀 크기와 회전.
|
||||
const pixelX = number(metadata.pixel_size_x);
|
||||
const pixelY = number(metadata.pixel_size_y);
|
||||
if (pixelX !== null && pixelY !== null) {
|
||||
@@ -126,12 +173,26 @@ export function readFacts(metadata: Record<string, unknown> | undefined): string
|
||||
(number(metadata.rotation_x) ?? 0) !== 0 || (number(metadata.rotation_y) ?? 0) !== 0;
|
||||
facts.push(rotated ? "회전 있음" : "회전 없음");
|
||||
}
|
||||
// PRJ 는 좌표계 이름이 곧 구분값이다.
|
||||
|
||||
// PRJ — 좌표계 이름·EPSG 와 원점(중앙자오선)·길이 단위.
|
||||
const name = text(metadata.name);
|
||||
const epsgLabel = readCrsLabel(metadata);
|
||||
if (name) facts.push(epsgLabel ? `${name} (EPSG:${epsgLabel})` : name);
|
||||
else if (epsgLabel && facts.length === 0) facts.push(`EPSG:${epsgLabel}`);
|
||||
return facts.slice(0, 2);
|
||||
const meridian = number(metadata.central_meridian_deg);
|
||||
const unit = text(metadata.unit_name);
|
||||
const origin = [
|
||||
meridian === null ? null : `중앙자오선 ${meridianLabel(meridian)}`,
|
||||
unit ? `단위 ${/^met(er|re)s?$/i.test(unit) ? "m" : unit}` : null,
|
||||
].filter((part): part is string => part !== null);
|
||||
if (origin.length) facts.push(origin.join(" · "));
|
||||
const vertical = metadata.vertical_crs as Record<string, unknown> | null | undefined;
|
||||
if (vertical && typeof vertical === "object") {
|
||||
const verticalName = text(vertical.name);
|
||||
if (verticalName) facts.push(`높이 기준 ${verticalName}`);
|
||||
}
|
||||
|
||||
return facts.slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user