import { wingPool } from '../db/wingDb.js' interface HnsSearchParams { q?: string type?: 'abbreviation' | 'nameKr' | 'nameEn' | 'casNumber' | 'unNumber' | 'cargoCode' sebc?: string page?: number limit?: number } export async function searchSubstances(params: HnsSearchParams) { const { q, type, sebc, page = 1, limit = 50 } = params const conditions: string[] = ["USE_YN = 'Y'"] const values: (string | number)[] = [] let paramIdx = 1 if (q && q.trim()) { const keyword = q.trim() switch (type) { case 'abbreviation': conditions.push(`ABBREVIATION ILIKE $${paramIdx}`) values.push(`%${keyword}%`) break case 'nameKr': conditions.push(`NM_KR ILIKE $${paramIdx}`) values.push(`%${keyword}%`) break case 'nameEn': conditions.push(`NM_EN ILIKE $${paramIdx}`) values.push(`%${keyword}%`) break case 'casNumber': conditions.push(`CAS_NO ILIKE $${paramIdx}`) values.push(`%${keyword}%`) break case 'unNumber': conditions.push(`UN_NO = $${paramIdx}`) values.push(keyword) break case 'cargoCode': conditions.push(`DATA->'cargoCodes' @> $${paramIdx}::jsonb`) values.push(JSON.stringify([{ code: keyword }])) break default: conditions.push(`(ABBREVIATION ILIKE $${paramIdx} OR NM_KR ILIKE $${paramIdx} OR NM_EN ILIKE $${paramIdx})`) values.push(`%${keyword}%`) } paramIdx++ } if (sebc && sebc.trim()) { conditions.push(`SEBC ILIKE $${paramIdx}`) values.push(`%${sebc.trim()}%`) paramIdx++ } const where = conditions.join(' AND ') const offset = (page - 1) * limit const countQuery = `SELECT COUNT(*) as total FROM HNS_SUBSTANCE WHERE ${where}` const dataQuery = ` SELECT SBST_SN, ABBREVIATION, NM_KR, NM_EN, UN_NO, CAS_NO, SEBC, DATA FROM HNS_SUBSTANCE WHERE ${where} ORDER BY SBST_SN LIMIT $${paramIdx} OFFSET $${paramIdx + 1} ` const [countResult, dataResult] = await Promise.all([ wingPool.query(countQuery, values), wingPool.query(dataQuery, [...values, limit, offset]), ]) return { total: parseInt(countResult.rows[0].total, 10), page, limit, items: dataResult.rows.map(row => ({ id: row.sbst_sn, abbreviation: row.abbreviation, nameKr: row.nm_kr, nameEn: row.nm_en, unNumber: row.un_no, casNumber: row.cas_no, sebc: row.sebc, ...row.data, })), } } // ============================================================ // HNS 분석 CRUD // ============================================================ interface HnsAnalysisItem { hnsAnlysSn: number acdntSn: number | null anlysNm: string acdntDtm: string | null locNm: string | null lon: number | null lat: number | null sbstNm: string | null spilQty: number | null spilUnitCd: string | null fcstHr: number | null algoCd: string | null critMdlCd: string | null windSpd: number | null windDir: string | null execSttsCd: string riskCd: string | null analystNm: string | null rsltData: Record | null regDtm: string } interface ListAnalysesInput { status?: string substance?: string search?: string acdntSn?: number page?: number limit?: number } function rowToAnalysis(r: Record): HnsAnalysisItem { return { hnsAnlysSn: r.hns_anlys_sn as number, acdntSn: (r.acdnt_sn as number) ?? null, anlysNm: r.anlys_nm as string, acdntDtm: r.acdnt_dtm as string | null, locNm: r.loc_nm as string | null, lon: r.lon ? parseFloat(r.lon as string) : null, lat: r.lat ? parseFloat(r.lat as string) : null, sbstNm: r.sbst_nm as string | null, spilQty: r.spil_qty ? parseFloat(r.spil_qty as string) : null, spilUnitCd: r.spil_unit_cd as string | null, fcstHr: r.fcst_hr as number | null, algoCd: r.algo_cd as string | null, critMdlCd: r.crit_mdl_cd as string | null, windSpd: r.wind_spd ? parseFloat(r.wind_spd as string) : null, windDir: r.wind_dir as string | null, execSttsCd: r.exec_stts_cd as string, riskCd: r.risk_cd as string | null, analystNm: r.analyst_nm as string | null, rsltData: (r.rslt_data as Record) ?? null, regDtm: r.reg_dtm as string, } } export async function listAnalyses(input: ListAnalysesInput): Promise<{ items: HnsAnalysisItem[] total: number page: number limit: number }> { const conditions: string[] = ["USE_YN = 'Y'"] const params: (string | number)[] = [] let idx = 1 if (input.status) { conditions.push(`EXEC_STTS_CD = $${idx++}`) params.push(input.status) } if (input.substance) { conditions.push(`SBST_NM ILIKE '%' || $${idx++} || '%'`) params.push(input.substance) } if (input.search) { conditions.push(`(ANLYS_NM ILIKE '%' || $${idx} || '%' OR LOC_NM ILIKE '%' || $${idx} || '%')`) params.push(input.search) idx++ } if (input.acdntSn != null) { conditions.push(`ACDNT_SN = $${idx++}`) params.push(input.acdntSn) } const whereClause = conditions.join(' AND ') const countResult = await wingPool.query( `SELECT COUNT(*) AS cnt FROM HNS_ANALYSIS WHERE ${whereClause}`, params ) const total = parseInt(countResult.rows[0].cnt as string, 10) const page = input.page ?? 1 const limit = input.limit ?? 10 const offset = (page - 1) * limit const { rows } = await wingPool.query( `SELECT HNS_ANLYS_SN, ACDNT_SN, ANLYS_NM, ACDNT_DTM, LOC_NM, LON, LAT, SBST_NM, SPIL_QTY, SPIL_UNIT_CD, FCST_HR, ALGO_CD, CRIT_MDL_CD, WIND_SPD, WIND_DIR, EXEC_STTS_CD, RISK_CD, ANALYST_NM, RSLT_DATA, REG_DTM FROM HNS_ANALYSIS WHERE ${whereClause} ORDER BY ACDNT_DTM DESC NULLS LAST LIMIT $${idx++} OFFSET $${idx}`, [...params, limit, offset] ) return { items: rows.map((r: Record) => rowToAnalysis(r)), total, page, limit, } } export async function getAnalysis(sn: number): Promise { const { rows } = await wingPool.query( `SELECT HNS_ANLYS_SN, ACDNT_SN, ANLYS_NM, ACDNT_DTM, LOC_NM, LON, LAT, SBST_NM, SPIL_QTY, SPIL_UNIT_CD, FCST_HR, ALGO_CD, CRIT_MDL_CD, WIND_SPD, WIND_DIR, TEMP, HUMID, ATM_STBL_CD, EXEC_STTS_CD, RISK_CD, ANALYST_NM, RSLT_DATA, REG_DTM FROM HNS_ANALYSIS WHERE HNS_ANLYS_SN = $1 AND USE_YN = 'Y'`, [sn] ) if (rows.length === 0) return null return rowToAnalysis(rows[0] as Record) } export async function createAnalysis(input: { anlysNm: string acdntSn?: number acdntDtm?: string locNm?: string lon?: number lat?: number sbstNm?: string spilQty?: number spilUnitCd?: string fcstHr?: number algoCd?: string critMdlCd?: string windSpd?: number windDir?: string temp?: number humid?: number atmStblCd?: string analystNm?: string }): Promise<{ hnsAnlysSn: number }> { const { rows } = await wingPool.query( `INSERT INTO HNS_ANALYSIS ( ACDNT_SN, ANLYS_NM, ACDNT_DTM, LOC_NM, LON, LAT, GEOM, LOC_DC, SBST_NM, SPIL_QTY, SPIL_UNIT_CD, FCST_HR, ALGO_CD, CRIT_MDL_CD, WIND_SPD, WIND_DIR, TEMP, HUMID, ATM_STBL_CD, ANALYST_NM, EXEC_STTS_CD ) VALUES ( $1, $2, $3, $4, $5::numeric, $6::numeric, CASE WHEN $5 IS NOT NULL AND $6 IS NOT NULL THEN ST_SetSRID(ST_MakePoint($5::double precision, $6::double precision), 4326) END, CASE WHEN $5 IS NOT NULL AND $6 IS NOT NULL THEN $5::text || ' + ' || $6::text END, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, 'PENDING' ) RETURNING HNS_ANLYS_SN`, [ input.acdntSn || null, input.anlysNm, input.acdntDtm || null, input.locNm || null, input.lon || null, input.lat || null, input.sbstNm || null, input.spilQty || null, input.spilUnitCd || 'KL', input.fcstHr || null, input.algoCd || null, input.critMdlCd || null, input.windSpd || null, input.windDir || null, input.temp || null, input.humid || null, input.atmStblCd || null, input.analystNm || null, ] ) return { hnsAnlysSn: rows[0].hns_anlys_sn } } export async function updateAnalysisResult(sn: number, input: { rsltData: Record execSttsCd?: string riskCd?: string }): Promise { await wingPool.query( `UPDATE HNS_ANALYSIS SET RSLT_DATA = $1, EXEC_STTS_CD = $2, RISK_CD = $3, MDFCN_DTM = NOW() WHERE HNS_ANLYS_SN = $4 AND USE_YN = 'Y'`, [JSON.stringify(input.rsltData), input.execSttsCd || 'COMPLETED', input.riskCd || null, sn] ) } export async function deleteAnalysis(sn: number): Promise { await wingPool.query( `UPDATE HNS_ANALYSIS SET USE_YN = 'N', MDFCN_DTM = NOW() WHERE HNS_ANLYS_SN = $1`, [sn] ) } export async function getSubstanceById(id: number) { const { rows } = await wingPool.query( `SELECT SBST_SN, ABBREVIATION, NM_KR, NM_EN, UN_NO, CAS_NO, SEBC, DATA FROM HNS_SUBSTANCE WHERE SBST_SN = $1`, [id] ) if (rows.length === 0) return null const row = rows[0] return { id: row.sbst_sn, abbreviation: row.abbreviation, nameKr: row.nm_kr, nameEn: row.nm_en, unNumber: row.un_no, casNumber: row.cas_no, sebc: row.sebc, ...row.data, } }