gc-wing/scripts/prepare-legacy.mjs

289 lines
9.0 KiB
JavaScript
Raw Normal View 히스토리

2026-02-15 11:22:38 +09:00
import fs from "node:fs/promises";
import path from "node:path";
import vm from "node:vm";
import xlsx from "xlsx";
const ROOT = process.cwd();
const LEGACY_DIR = path.join(ROOT, "legacy");
const OUT_DIR = path.join(ROOT, "apps", "web", "public", "data", "legacy");
const PERMITTED_XLSX = path.join(LEGACY_DIR, "AIS_permitted_Chinese_2025_정리완료_최종.xlsx");
const CHECKLIST_XLSX = path.join(LEGACY_DIR, "항적점검표_본선_부속선_운반선.xlsx");
const FLEET_906_JSX = path.join(LEGACY_DIR, "선단구성_906척_어업수역 (1).jsx");
function toStr(x) {
return (x ?? "").toString().trim();
}
function toNumOrNull(x) {
const s = toStr(x);
if (!s) return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
}
function shipCodeFromChecklistSheet(sheetName) {
if (sheetName.includes("본선")) return "PT";
if (sheetName.includes("부속선")) return "PT-S";
if (sheetName.includes("운반선")) return "FC";
return null;
}
function extractArrayLiteral(txt, marker) {
const markerIdx = txt.indexOf(marker);
if (markerIdx < 0) throw new Error(`Marker not found: ${marker}`);
const start = txt.indexOf("[", markerIdx);
if (start < 0) throw new Error(`'[' not found after marker: ${marker}`);
let depth = 0;
let inString = false;
let esc = false;
for (let i = start; i < txt.length; i += 1) {
const ch = txt[i];
if (inString) {
if (esc) {
esc = false;
continue;
}
if (ch === "\\") {
esc = true;
continue;
}
if (ch === "\"") {
inString = false;
}
continue;
}
if (ch === "\"") {
inString = true;
continue;
}
if (ch === "[") depth += 1;
if (ch === "]") depth -= 1;
if (depth === 0) return txt.slice(start, i + 1);
}
throw new Error(`Unterminated array literal for marker: ${marker}`);
}
function readPermittedListXlsx(filePath) {
const wb = xlsx.readFile(filePath, { cellDates: true });
const sheetName = wb.SheetNames[0];
const ws = wb.Sheets[sheetName];
const rows = xlsx.utils.sheet_to_json(ws, { defval: "", raw: false });
const byPermitNo = new Map();
for (const r of rows) {
const permitNo = toStr(r.permit_no);
if (!permitNo) continue;
const mmsi = toNumOrNull(r.target_id);
const prev = byPermitNo.get(permitNo);
if (prev) {
if (mmsi && !prev.mmsiList.includes(mmsi)) prev.mmsiList.push(mmsi);
continue;
}
byPermitNo.set(permitNo, {
permitNo,
shipNameRoman: toStr(r.ship_name),
ton: toNumOrNull(r.ton),
callSign: toStr(r.call_sign),
shipPower: toNumOrNull(r.ship_power),
shipLen: toNumOrNull(r.ship_len),
shipWidth: toNumOrNull(r.ship_width),
shipDept: toNumOrNull(r.ship_dept),
workSeaArea: toStr(r.work_sea_area),
workTerm1: toStr(r.work_term1),
workTerm2: toStr(r.work_term2),
quota: toStr(r.quota),
shipCode: toStr(r.ship_code),
mmsiList: mmsi ? [mmsi] : [],
sources: { permittedList: true, checklist: false, fleet906: false },
ownerCn: null,
ownerRoman: null,
shipNameCn: null,
pairPermitNo: null,
pairShipNameCn: null,
checklistSheet: null,
});
}
return Array.from(byPermitNo.values());
}
function readChecklistXlsx(filePath) {
const wb = xlsx.readFile(filePath, { cellDates: true });
const out = [];
for (const sheetName of wb.SheetNames) {
const code = shipCodeFromChecklistSheet(sheetName);
if (!code) continue;
const ws = wb.Sheets[sheetName];
const rows = xlsx.utils.sheet_to_json(ws, { defval: "", raw: false, range: 2 });
for (const r of rows) {
const permitNo = toStr(r["허가번호"]);
if (!permitNo) continue;
out.push({
permitNo,
shipCode: code,
shipNameCn: toStr(r["선박명\n(중국)"]),
shipNameRoman: toStr(r["선박명\n(로마)"]),
ton: toNumOrNull(r["톤수"]),
ownerCn: toStr(r["소유주\n(중국)"]),
ownerRoman: toStr(r["소유주\n(로마)"]),
pairPermitNo: toStr(r["짝 허가번호"]),
pairShipNameCn: toStr(r["짝 선박명"]),
workSeaArea: toStr(r["허가수역"]),
checklistSheet: sheetName,
});
}
}
return out;
}
async function readFleet906(filePath) {
const txt = await fs.readFile(filePath, "utf-8");
const arrLit = extractArrayLiteral(txt, "const D=");
const D = vm.runInNewContext(`(${arrLit})`, {}, { timeout: 10_000 });
if (!Array.isArray(D)) throw new Error("Fleet906 D is not an array");
const out = [];
function pushVessel(v, shipCode, own, ownEn, extra = {}) {
if (!Array.isArray(v) || v.length < 4) return;
out.push({
permitNo: toStr(v[0]),
shipNameCn: toStr(v[1]),
shipNameRoman: toStr(v[2]),
ton: toNumOrNull(v[3]),
shipCode,
ownerCn: toStr(own),
ownerRoman: toStr(ownEn),
...extra,
});
}
for (const r of D) {
if (!Array.isArray(r) || r.length < 9) continue;
const [own, ownEn, pairs, gn, ot, ps, fc, upt, upts] = r;
if (Array.isArray(pairs)) {
for (const p of pairs) {
if (!Array.isArray(p) || p.length < 2) continue;
const main = p[0];
const sub = p[1];
const mainPermitNo = Array.isArray(main) ? toStr(main[0]) : "";
const subPermitNo = Array.isArray(sub) ? toStr(sub[0]) : "";
pushVessel(main, "PT", own, ownEn, { pairPermitNo: subPermitNo });
pushVessel(sub, "PT-S", own, ownEn, { pairPermitNo: mainPermitNo });
}
}
if (Array.isArray(gn)) for (const v of gn) pushVessel(v, "GN", own, ownEn);
if (Array.isArray(ot)) for (const v of ot) pushVessel(v, "OT", own, ownEn);
if (Array.isArray(ps)) for (const v of ps) pushVessel(v, "PS", own, ownEn);
if (Array.isArray(fc)) for (const v of fc) pushVessel(v, "FC", own, ownEn);
if (Array.isArray(upt)) for (const v of upt) pushVessel(v, "PT", own, ownEn);
if (Array.isArray(upts)) for (const v of upts) pushVessel(v, "PT-S", own, ownEn);
}
return out;
}
async function main() {
await fs.mkdir(OUT_DIR, { recursive: true });
const permitted = readPermittedListXlsx(PERMITTED_XLSX);
const checklist = readChecklistXlsx(CHECKLIST_XLSX);
const fleet906 = await readFleet906(FLEET_906_JSX);
const byPermitNo = new Map();
for (const v of permitted) byPermitNo.set(v.permitNo, v);
for (const c of checklist) {
const prev = byPermitNo.get(c.permitNo);
if (!prev) {
byPermitNo.set(c.permitNo, {
permitNo: c.permitNo,
shipNameRoman: c.shipNameRoman,
ton: c.ton,
callSign: "",
shipPower: null,
shipLen: null,
shipWidth: null,
shipDept: null,
workSeaArea: c.workSeaArea,
workTerm1: "",
workTerm2: "",
quota: "",
shipCode: c.shipCode,
mmsiList: [],
sources: { permittedList: false, checklist: true, fleet906: false },
ownerCn: c.ownerCn || null,
ownerRoman: c.ownerRoman || null,
shipNameCn: c.shipNameCn || null,
pairPermitNo: c.pairPermitNo || null,
pairShipNameCn: c.pairShipNameCn || null,
checklistSheet: c.checklistSheet || null,
});
continue;
}
prev.sources.checklist = true;
prev.shipCode = prev.shipCode || c.shipCode;
prev.shipNameCn = prev.shipNameCn || c.shipNameCn || null;
prev.shipNameRoman = prev.shipNameRoman || c.shipNameRoman || "";
prev.ton = prev.ton ?? c.ton ?? null;
prev.ownerCn = prev.ownerCn || c.ownerCn || null;
prev.ownerRoman = prev.ownerRoman || c.ownerRoman || null;
prev.pairPermitNo = prev.pairPermitNo || c.pairPermitNo || null;
prev.pairShipNameCn = prev.pairShipNameCn || c.pairShipNameCn || null;
prev.workSeaArea = prev.workSeaArea || c.workSeaArea || "";
prev.checklistSheet = prev.checklistSheet || c.checklistSheet || null;
}
for (const f of fleet906) {
const prev = byPermitNo.get(f.permitNo);
if (!prev) continue;
prev.sources.fleet906 = true;
prev.shipNameCn = prev.shipNameCn || f.shipNameCn || null;
prev.shipNameRoman = prev.shipNameRoman || f.shipNameRoman || "";
prev.ton = prev.ton ?? f.ton ?? null;
prev.shipCode = prev.shipCode || f.shipCode || "";
prev.ownerCn = prev.ownerCn || f.ownerCn || null;
prev.ownerRoman = prev.ownerRoman || f.ownerRoman || null;
prev.pairPermitNo = prev.pairPermitNo || f.pairPermitNo || null;
}
const vessels = Array.from(byPermitNo.values()).filter((v) => v && v.permitNo);
const out = {
generatedAt: new Date().toISOString(),
counts: {
permittedList: permitted.length,
checklist: checklist.length,
fleet906: fleet906.length,
merged: vessels.length,
},
vessels,
};
const outPath = path.join(OUT_DIR, "chinese-permitted.v1.json");
await fs.writeFile(outPath, JSON.stringify(out), "utf-8");
// eslint-disable-next-line no-console
console.log(`Wrote ${vessels.length} vessels -> ${path.relative(ROOT, outPath)}`);
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});