wing-ops/backend/src/scat/scatService.ts
leedano 9881b99ee7 feat(scat): Pre-SCAT 해안조사 UI 개선 + WeatherRightPanel 정리
SCAT 좌측패널 리팩토링, 해안조사 뷰 기능 보강, 기상 우측패널 중복 코드 제거

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:22:20 +09:00

272 lines
7.9 KiB
TypeScript

import { wingPool } from '../db/wingDb.js';
// ============================================================
// 인터페이스
// ============================================================
interface ZoneItem {
cstSrvyZoneSn: number;
zoneCd: string;
zoneNm: string;
jrsdNm: string;
sectCnt: number;
latCenter: number;
lngCenter: number;
latRange: number;
lngRange: number;
}
interface SectionListItem {
cstSectSn: number;
sectCd: string;
sectNm: string;
cstTpCd: string;
esiCd: string;
esiNum: number;
lenM: number;
snstvtCd: string;
srvySttsCd: string;
lat: number;
lng: number;
tags: string[];
zoneCd: string;
zoneNm: string;
jrsdNm: string;
}
interface SectionDetail {
cstSectSn: number;
sectCd: string;
sectNm: string;
cstTpCd: string;
esiCd: string;
esiNum: number;
lenM: number;
snstvtCd: string;
srvySttsCd: string;
lat: number;
lng: number;
tags: string[];
geomJson: Record<string, unknown> | null;
zoneCd: string;
zoneNm: string;
jrsdNm: string;
shoreTp: string | null;
accessDc: string | null;
accessPt: string | null;
sensitiveInfo: { t: string; v: string }[];
cleanupMethods: string[];
endCriteria: string[];
notes: string[];
}
// ============================================================
// 관할청 목록 조회
// ============================================================
export async function listOffices(): Promise<string[]> {
const sql = `
SELECT DISTINCT OFFICE_CD
FROM wing.CST_SRVY_ZONE
WHERE USE_YN = 'Y' AND OFFICE_CD IS NOT NULL
ORDER BY OFFICE_CD
`;
const { rows } = await wingPool.query(sql);
return rows.map((r: Record<string, unknown>) => r.office_cd as string);
}
// ============================================================
// 관할서 목록 조회
// ============================================================
export async function listJurisdictions(officeCd?: string): Promise<string[]> {
const conditions: string[] = ["USE_YN = 'Y'", 'JRSD_NM IS NOT NULL'];
const params: unknown[] = [];
let idx = 1;
if (officeCd) {
conditions.push(`OFFICE_CD = $${idx++}`);
params.push(officeCd);
}
const sql = `
SELECT DISTINCT JRSD_NM
FROM wing.CST_SRVY_ZONE
WHERE ${conditions.join(' AND ')}
ORDER BY JRSD_NM
`;
const { rows } = await wingPool.query(sql, params);
return rows.map((r: Record<string, unknown>) => r.jrsd_nm as string);
}
// ============================================================
// 조사구역 목록 조회
// ============================================================
export async function listZones(filters?: {
jurisdiction?: string;
officeCd?: string;
}): Promise<ZoneItem[]> {
const conditions: string[] = ["USE_YN = 'Y'"];
const params: unknown[] = [];
let idx = 1;
if (filters?.jurisdiction) {
conditions.push(`JRSD_NM ILIKE '%' || $${idx++} || '%'`);
params.push(filters.jurisdiction);
}
if (filters?.officeCd) {
conditions.push(`OFFICE_CD = $${idx++}`);
params.push(filters.officeCd);
}
const where = 'WHERE ' + conditions.join(' AND ');
const sql = `
SELECT CST_SRVY_ZONE_SN, ZONE_CD, ZONE_NM, JRSD_NM,
SECT_CNT, LAT_CENTER, LNG_CENTER, LAT_RANGE, LNG_RANGE
FROM wing.CST_SRVY_ZONE
${where}
ORDER BY CST_SRVY_ZONE_SN
`;
const { rows } = await wingPool.query(sql, params);
return rows.map((r: Record<string, unknown>) => ({
cstSrvyZoneSn: r.cst_srvy_zone_sn as number,
zoneCd: r.zone_cd as string,
zoneNm: r.zone_nm as string,
jrsdNm: r.jrsd_nm as string,
sectCnt: r.sect_cnt as number,
latCenter: parseFloat(r.lat_center as string),
lngCenter: parseFloat(r.lng_center as string),
latRange: parseFloat(r.lat_range as string),
lngRange: parseFloat(r.lng_range as string),
}));
}
// ============================================================
// 해안구간 목록 조회 (필터링)
// ============================================================
export async function listSections(filters: {
zone?: string;
status?: string;
sensitivity?: string;
jurisdiction?: string;
search?: string;
officeCd?: string;
}): Promise<SectionListItem[]> {
const conditions: string[] = [];
const params: unknown[] = [];
let idx = 1;
if (filters.zone) {
conditions.push(`z.ZONE_CD = $${idx++}`);
params.push(filters.zone);
}
if (filters.status) {
conditions.push(`s.SRVY_STTS_CD = $${idx++}`);
params.push(filters.status);
}
if (filters.sensitivity) {
conditions.push(`s.SNSTVT_CD = $${idx++}`);
params.push(filters.sensitivity);
}
if (filters.jurisdiction) {
conditions.push(`z.JRSD_NM ILIKE '%' || $${idx++} || '%'`);
params.push(filters.jurisdiction);
}
if (filters.search) {
conditions.push(`s.SECT_NM ILIKE '%' || $${idx++} || '%'`);
params.push(filters.search);
}
if (filters.officeCd) {
conditions.push(`z.OFFICE_CD = $${idx++}`);
params.push(filters.officeCd);
}
conditions.push("s.USE_YN = 'Y'");
conditions.push("z.USE_YN = 'Y'");
const where = 'WHERE ' + conditions.join(' AND ');
const sql = `
SELECT s.CST_SECT_SN, s.SECT_CD, s.SECT_NM, s.CST_TP_CD,
s.ESI_CD, s.ESI_NUM, s.LEN_M, s.SNSTVT_CD, s.SRVY_STTS_CD,
s.LAT, s.LNG, s.TAGS,
z.ZONE_CD, z.ZONE_NM, z.JRSD_NM
FROM wing.CST_SECT s
JOIN wing.CST_SRVY_ZONE z ON z.CST_SRVY_ZONE_SN = s.CST_SRVY_ZONE_SN
${where}
ORDER BY s.CST_SECT_SN
`;
const { rows } = await wingPool.query(sql, params);
return rows.map((r: Record<string, unknown>) => ({
cstSectSn: r.cst_sect_sn as number,
sectCd: r.sect_cd as string,
sectNm: r.sect_nm as string,
cstTpCd: r.cst_tp_cd as string,
esiCd: r.esi_cd as string,
esiNum: r.esi_num as number,
lenM: r.len_m as number,
snstvtCd: r.snstvt_cd as string,
srvySttsCd: r.srvy_stts_cd as string,
lat: parseFloat(r.lat as string),
lng: parseFloat(r.lng as string),
tags: (r.tags as string[]) ?? [],
zoneCd: r.zone_cd as string,
zoneNm: r.zone_nm as string,
jrsdNm: r.jrsd_nm as string,
}));
}
// ============================================================
// 해안구간 단건 상세 조회 (JSONB 포함)
// ============================================================
export async function getSection(sn: number): Promise<SectionDetail | null> {
const sql = `
SELECT s.CST_SECT_SN, s.SECT_CD, s.SECT_NM, s.CST_TP_CD,
s.ESI_CD, s.ESI_NUM, s.LEN_M, s.SNSTVT_CD, s.SRVY_STTS_CD,
s.LAT, s.LNG, s.TAGS,
ST_AsGeoJSON(s.GEOM)::jsonb AS geom_json,
s.SHORE_TP, s.ACCESS_DC, s.ACCESS_PT,
s.SENSITIVE_INFO, s.CLEANUP_METHODS, s.END_CRITERIA, s.NOTES,
z.ZONE_CD, z.ZONE_NM, z.JRSD_NM
FROM wing.CST_SECT s
JOIN wing.CST_SRVY_ZONE z ON z.CST_SRVY_ZONE_SN = s.CST_SRVY_ZONE_SN
WHERE s.CST_SECT_SN = $1 AND s.USE_YN = 'Y'
`;
const { rows } = await wingPool.query(sql, [sn]);
if (rows.length === 0) return null;
const r = rows[0] as Record<string, unknown>;
return {
cstSectSn: r.cst_sect_sn as number,
sectCd: r.sect_cd as string,
sectNm: r.sect_nm as string,
cstTpCd: r.cst_tp_cd as string,
esiCd: r.esi_cd as string,
esiNum: r.esi_num as number,
lenM: r.len_m as number,
snstvtCd: r.snstvt_cd as string,
srvySttsCd: r.srvy_stts_cd as string,
lat: parseFloat(r.lat as string),
lng: parseFloat(r.lng as string),
tags: (r.tags as string[]) ?? [],
geomJson: (r.geom_json as Record<string, unknown>) ?? null,
zoneCd: r.zone_cd as string,
zoneNm: r.zone_nm as string,
jrsdNm: r.jrsd_nm as string,
shoreTp: (r.shore_tp as string) ?? null,
accessDc: (r.access_dc as string) ?? null,
accessPt: (r.access_pt as string) ?? null,
sensitiveInfo: (r.sensitive_info as { t: string; v: string }[]) ?? [],
cleanupMethods: (r.cleanup_methods as string[]) ?? [],
endCriteria: (r.end_criteria as string[]) ?? [],
notes: (r.notes as string[]) ?? [],
};
}