333 lines
10 KiB
TypeScript
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(),
|
|
}));
|
|
}
|