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 { 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) => ({ 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 { 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; 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) => ({ category: e.ctgr_nm as string, icon: e.icon as string, count: e.qty as number, })), contacts: contactRows.map((c: Record) => ({ 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 { 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) => ({ 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) => ({ 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 { 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) => ({ 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(), })); }