289 lines
9.0 KiB
JavaScript
289 lines
9.0 KiB
JavaScript
|
|
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);
|
||
|
|
});
|