wing-ops/backend/src/assets/assetsService.ts
2026-03-25 18:17:42 +09:00

333 lines
10 KiB
TypeScript

import { wingPool } from '../db/wingDb.js';
// ============================================================
// 인터페이스
// ============================================================
interface EquipItem {
category: string;
icon: string;
count: number;
}
interface ContactItem {
role: string;
name: string;
phone: string;
}
interface OrgListItem {
orgSn: number;
orgTp: string;
jrsdNm: string;
areaNm: string;
orgNm: string;
addr: string;
tel: string;
lat: number;
lng: number;
pinSize: string;
vesselCnt: number;
skimmerCnt: number;
pumpCnt: number;
vehicleCnt: number;
sprayerCnt: number;
totalAssets: number;
}
interface OrgDetail extends OrgListItem {
equipment: EquipItem[];
contacts: ContactItem[];
}
interface UploadLogItem {
logSn: number;
fileNm: string;
uploaderNm: string;
uploadCnt: number;
regDtm: string;
}
// ============================================================
// 기관 목록 조회
// ============================================================
export async function listOrganizations(filters: {
orgTp?: string;
jrsd?: string;
search?: string;
}): Promise<OrgListItem[]> {
const conditions: string[] = ["USE_YN = 'Y'"];
const params: unknown[] = [];
let idx = 1;
if (filters.orgTp) {
conditions.push(`ORG_TP = $${idx++}`);
params.push(filters.orgTp);
}
if (filters.jrsd) {
conditions.push(`JRSD_NM LIKE '%' || $${idx++} || '%'`);
params.push(filters.jrsd);
}
if (filters.search) {
conditions.push(`(ORG_NM LIKE '%' || $${idx++} || '%' OR ADDR LIKE '%' || $${idx++} || '%')`);
params.push(filters.search, filters.search);
}
const sql = `
SELECT ORG_SN, ORG_TP, JRSD_NM, AREA_NM, ORG_NM, ADDR, TEL,
LAT, LNG, PIN_SIZE,
VESSEL_CNT, SKIMMER_CNT, PUMP_CNT, VEHICLE_CNT, SPRAYER_CNT, TOTAL_ASSETS
FROM wing.ASSET_ORG
WHERE ${conditions.join(' AND ')}
ORDER BY JRSD_NM, ORG_TP, ORG_NM
`;
const { rows } = await wingPool.query(sql, params);
return rows.map((r: Record<string, unknown>) => ({
orgSn: r.org_sn as number,
orgTp: r.org_tp as string,
jrsdNm: r.jrsd_nm as string,
areaNm: r.area_nm as string,
orgNm: r.org_nm as string,
addr: r.addr as string,
tel: r.tel as string,
lat: parseFloat(r.lat as string),
lng: parseFloat(r.lng as string),
pinSize: r.pin_size as string,
vesselCnt: r.vessel_cnt as number,
skimmerCnt: r.skimmer_cnt as number,
pumpCnt: r.pump_cnt as number,
vehicleCnt: r.vehicle_cnt as number,
sprayerCnt: r.sprayer_cnt as number,
totalAssets: r.total_assets as number,
}));
}
// ============================================================
// 기관 상세 조회 (장비 + 담당자 포함)
// ============================================================
export async function getOrganization(orgSn: number): Promise<OrgDetail | null> {
const orgSql = `
SELECT ORG_SN, ORG_TP, JRSD_NM, AREA_NM, ORG_NM, ADDR, TEL,
LAT, LNG, PIN_SIZE,
VESSEL_CNT, SKIMMER_CNT, PUMP_CNT, VEHICLE_CNT, SPRAYER_CNT, TOTAL_ASSETS
FROM wing.ASSET_ORG
WHERE ORG_SN = $1 AND USE_YN = 'Y'
`;
const { rows: orgRows } = await wingPool.query(orgSql, [orgSn]);
if (orgRows.length === 0) return null;
const r = orgRows[0] as Record<string, unknown>;
const equipSql = `
SELECT CTGR_NM, ICON, QTY FROM wing.ASSET_EQUIP
WHERE ORG_SN = $1 ORDER BY SORT_ORD, EQUIP_SN
`;
const { rows: equipRows } = await wingPool.query(equipSql, [orgSn]);
const contactSql = `
SELECT ROLE_NM, CONTACT_NM, TEL FROM wing.ASSET_CONTACT
WHERE ORG_SN = $1 ORDER BY SORT_ORD, CONTACT_SN
`;
const { rows: contactRows } = await wingPool.query(contactSql, [orgSn]);
return {
orgSn: r.org_sn as number,
orgTp: r.org_tp as string,
jrsdNm: r.jrsd_nm as string,
areaNm: r.area_nm as string,
orgNm: r.org_nm as string,
addr: r.addr as string,
tel: r.tel as string,
lat: parseFloat(r.lat as string),
lng: parseFloat(r.lng as string),
pinSize: r.pin_size as string,
vesselCnt: r.vessel_cnt as number,
skimmerCnt: r.skimmer_cnt as number,
pumpCnt: r.pump_cnt as number,
vehicleCnt: r.vehicle_cnt as number,
sprayerCnt: r.sprayer_cnt as number,
totalAssets: r.total_assets as number,
equipment: equipRows.map((e: Record<string, unknown>) => ({
category: e.ctgr_nm as string,
icon: e.icon as string,
count: e.qty as number,
})),
contacts: contactRows.map((c: Record<string, unknown>) => ({
role: c.role_nm as string,
name: c.contact_nm as string,
phone: c.tel as string,
})),
};
}
// ============================================================
// 근처 기관 조회 (PostGIS ST_DWithin)
// ============================================================
export interface NearbyOrgItem extends OrgListItem {
distanceNm: number;
}
export async function listNearbyOrganizations(
lat: number,
lng: number,
radiusNm: number,
): Promise<NearbyOrgItem[]> {
const radiusMeters = radiusNm * 1852;
const sql = `
SELECT ORG_SN, ORG_TP, JRSD_NM, AREA_NM, ORG_NM, ADDR, TEL,
LAT, LNG, PIN_SIZE,
VESSEL_CNT, SKIMMER_CNT, PUMP_CNT, VEHICLE_CNT, SPRAYER_CNT, TOTAL_ASSETS,
ST_Distance(GEOM::geography, ST_SetSRID(ST_MakePoint($2, $1), 4326)::geography) / 1852.0 AS distance_nm
FROM wing.ASSET_ORG
WHERE USE_YN = 'Y'
AND GEOM IS NOT NULL
AND ST_DWithin(GEOM::geography, ST_SetSRID(ST_MakePoint($2, $1), 4326)::geography, $3)
ORDER BY distance_nm
`;
const { rows } = await wingPool.query(sql, [lat, lng, radiusMeters]);
return rows.map((r: Record<string, unknown>) => ({
orgSn: r.org_sn as number,
orgTp: r.org_tp as string,
jrsdNm: r.jrsd_nm as string,
areaNm: r.area_nm as string,
orgNm: r.org_nm as string,
addr: r.addr as string,
tel: r.tel as string,
lat: parseFloat(r.lat as string),
lng: parseFloat(r.lng as string),
pinSize: r.pin_size as string,
vesselCnt: r.vessel_cnt as number,
skimmerCnt: r.skimmer_cnt as number,
pumpCnt: r.pump_cnt as number,
vehicleCnt: r.vehicle_cnt as number,
sprayerCnt: r.sprayer_cnt as number,
totalAssets: r.total_assets as number,
distanceNm: parseFloat(r.distance_nm as string),
}));
}
// ============================================================
// 선박보험(유류오염보장계약) 조회
// ============================================================
interface InsuranceListItem {
insSn: number;
shipNo: string;
shipNm: string;
callSign: string;
imoNo: string;
shipTp: string;
shipTpDetail: string;
ownerNm: string;
grossTon: string;
insurerNm: string;
liabilityYn: string;
oilPollutionYn: string;
fuelOilYn: string;
wreckRemovalYn: string;
validStart: string;
validEnd: string;
issueOrg: string;
}
export async function listInsurance(filters: {
search?: string;
shipTp?: string;
issueOrg?: string;
insurer?: string;
page?: number;
limit?: number;
}): Promise<{ rows: InsuranceListItem[]; total: number }> {
const conditions: string[] = [];
const params: unknown[] = [];
let idx = 1;
if (filters.search) {
conditions.push(`(ship_nm LIKE '%' || $${idx} || '%' OR call_sign LIKE '%' || $${idx} || '%' OR imo_no LIKE '%' || $${idx} || '%' OR owner_nm LIKE '%' || $${idx} || '%')`);
params.push(filters.search);
idx++;
}
if (filters.shipTp) {
conditions.push(`ship_tp = $${idx++}`);
params.push(filters.shipTp);
}
if (filters.issueOrg) {
conditions.push(`issue_org LIKE '%' || $${idx++} || '%'`);
params.push(filters.issueOrg);
}
if (filters.insurer) {
conditions.push(`insurer_nm LIKE '%' || $${idx++} || '%'`);
params.push(filters.insurer);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const limit = Math.min(filters.limit || 50, 200);
const offset = ((filters.page || 1) - 1) * limit;
const countSql = `SELECT COUNT(*) AS cnt FROM wing.SHIP_INSURANCE ${where}`;
const { rows: countRows } = await wingPool.query(countSql, params);
const total = parseInt(countRows[0].cnt as string, 10);
const sql = `
SELECT ins_sn, ship_no, ship_nm, call_sign, imo_no, ship_tp, ship_tp_detail,
owner_nm, gross_ton, insurer_nm,
liability_yn, oil_pollution_yn, fuel_oil_yn, wreck_removal_yn,
valid_start, valid_end, issue_org
FROM wing.SHIP_INSURANCE
${where}
ORDER BY valid_end DESC, ins_sn
LIMIT $${idx++} OFFSET $${idx++}
`;
params.push(limit, offset);
const { rows } = await wingPool.query(sql, params);
return {
total,
rows: rows.map((r: Record<string, unknown>) => ({
insSn: r.ins_sn as number,
shipNo: r.ship_no as string,
shipNm: r.ship_nm as string,
callSign: r.call_sign as string,
imoNo: r.imo_no as string,
shipTp: r.ship_tp as string,
shipTpDetail: r.ship_tp_detail as string,
ownerNm: r.owner_nm as string,
grossTon: r.gross_ton as string,
insurerNm: r.insurer_nm as string,
liabilityYn: r.liability_yn as string,
oilPollutionYn: r.oil_pollution_yn as string,
fuelOilYn: r.fuel_oil_yn as string,
wreckRemovalYn: r.wreck_removal_yn as string,
validStart: (r.valid_start as Date)?.toISOString().slice(0, 10) ?? '',
validEnd: (r.valid_end as Date)?.toISOString().slice(0, 10) ?? '',
issueOrg: r.issue_org as string,
})),
};
}
// ============================================================
// 업로드 이력 조회
// ============================================================
export async function listUploadLogs(limit = 20): Promise<UploadLogItem[]> {
const sql = `
SELECT LOG_SN, FILE_NM, UPLOADER_NM, UPLOAD_CNT, REG_DTM
FROM wing.ASSET_UPLOAD_LOG
ORDER BY REG_DTM DESC
LIMIT $1
`;
const { rows } = await wingPool.query(sql, [limit]);
return rows.map((r: Record<string, unknown>) => ({
logSn: r.log_sn as number,
fileNm: r.file_nm as string,
uploaderNm: r.uploader_nm as string,
uploadCnt: r.upload_cnt as number,
regDtm: (r.reg_dtm as Date).toISOString(),
}));
}