feat(incidents): 통합 분석 패널 HNS/구난 연동 및 사고 목록 wing.ACDNT 전환
- 우측 패널에 HNS 대기확산/긴급구난 완료 이력 목록 및 체크박스 연동 - incidents 목록에 hasHnsCompleted/hasRescueCompleted 플래그 추가 - hns/rescue 목록 API에 acdntSn 필터 추가 - /gsc/accidents 셀렉트박스 소스를 gsc.tgs_acdnt_info → wing.ACDNT 로 전환 - gsc → wing.ACDNT 동기화 마이그레이션 032 추가
This commit is contained in:
부모
0daae3c807
커밋
1f66723060
@ -8,43 +8,18 @@ export interface GscAccidentListItem {
|
|||||||
lon: number | null;
|
lon: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACDNT_ASORT_CODES = [
|
|
||||||
'055001001',
|
|
||||||
'055001002',
|
|
||||||
'055001003',
|
|
||||||
'055001004',
|
|
||||||
'055001005',
|
|
||||||
'055001006',
|
|
||||||
'055003001',
|
|
||||||
'055003002',
|
|
||||||
'055003003',
|
|
||||||
'055003004',
|
|
||||||
'055003005',
|
|
||||||
'055004003',
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function listGscAccidents(limit = 20): Promise<GscAccidentListItem[]> {
|
export async function listGscAccidents(limit = 20): Promise<GscAccidentListItem[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT DISTINCT ON (a.acdnt_mng_no)
|
SELECT
|
||||||
a.acdnt_mng_no AS "acdntMngNo",
|
ACDNT_CD AS "acdntMngNo",
|
||||||
a.acdnt_title AS "pollNm",
|
ACDNT_NM AS "pollNm",
|
||||||
to_char(a.rcept_dt, 'YYYY-MM-DD"T"HH24:MI') AS "pollDate",
|
to_char(OCCRN_DTM, 'YYYY-MM-DD"T"HH24:MI') AS "pollDate",
|
||||||
a.rcept_dt AS "rceptDt",
|
LAT AS "lat",
|
||||||
b.la AS "lat",
|
LNG AS "lon"
|
||||||
b.lo AS "lon"
|
FROM wing.ACDNT
|
||||||
FROM gsc.tgs_acdnt_info AS a
|
WHERE ACDNT_NM IS NOT NULL
|
||||||
LEFT JOIN gsc.tgs_acdnt_lc AS b
|
ORDER BY OCCRN_DTM DESC NULLS LAST
|
||||||
ON a.acdnt_mng_no = b.acdnt_mng_no
|
LIMIT $1
|
||||||
WHERE a.acdnt_asort_code = ANY($1::varchar[])
|
|
||||||
AND a.acdnt_title IS NOT NULL
|
|
||||||
ORDER BY a.acdnt_mng_no, b.acdnt_lc_sn ASC
|
|
||||||
`;
|
|
||||||
|
|
||||||
const orderedSql = `
|
|
||||||
SELECT "acdntMngNo", "pollNm", "pollDate", "lat", "lon"
|
|
||||||
FROM (${sql}) t
|
|
||||||
ORDER BY t."rceptDt" DESC NULLS LAST
|
|
||||||
LIMIT $2
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await wingPool.query<{
|
const result = await wingPool.query<{
|
||||||
@ -53,7 +28,7 @@ export async function listGscAccidents(limit = 20): Promise<GscAccidentListItem[
|
|||||||
pollDate: string | null;
|
pollDate: string | null;
|
||||||
lat: string | null;
|
lat: string | null;
|
||||||
lon: string | null;
|
lon: string | null;
|
||||||
}>(orderedSql, [ACDNT_ASORT_CODES, limit]);
|
}>(sql, [limit]);
|
||||||
|
|
||||||
return result.rows.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
acdntMngNo: row.acdntMngNo,
|
acdntMngNo: row.acdntMngNo,
|
||||||
|
|||||||
@ -12,11 +12,13 @@ const router = express.Router()
|
|||||||
// GET /api/hns/analyses — 분석 목록
|
// GET /api/hns/analyses — 분석 목록
|
||||||
router.get('/analyses', requireAuth, requirePermission('hns', 'READ'), async (req, res) => {
|
router.get('/analyses', requireAuth, requirePermission('hns', 'READ'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { status, substance, search } = req.query
|
const { status, substance, search, acdntSn } = req.query
|
||||||
|
const acdntSnNum = acdntSn ? parseInt(acdntSn as string, 10) : undefined
|
||||||
const items = await listAnalyses({
|
const items = await listAnalyses({
|
||||||
status: status as string | undefined,
|
status: status as string | undefined,
|
||||||
substance: substance as string | undefined,
|
substance: substance as string | undefined,
|
||||||
search: search as string | undefined,
|
search: search as string | undefined,
|
||||||
|
acdntSn: acdntSnNum && !Number.isNaN(acdntSnNum) ? acdntSnNum : undefined,
|
||||||
})
|
})
|
||||||
res.json(items)
|
res.json(items)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -94,6 +94,7 @@ export async function searchSubstances(params: HnsSearchParams) {
|
|||||||
|
|
||||||
interface HnsAnalysisItem {
|
interface HnsAnalysisItem {
|
||||||
hnsAnlysSn: number
|
hnsAnlysSn: number
|
||||||
|
acdntSn: number | null
|
||||||
anlysNm: string
|
anlysNm: string
|
||||||
acdntDtm: string | null
|
acdntDtm: string | null
|
||||||
locNm: string | null
|
locNm: string | null
|
||||||
@ -118,11 +119,13 @@ interface ListAnalysesInput {
|
|||||||
status?: string
|
status?: string
|
||||||
substance?: string
|
substance?: string
|
||||||
search?: string
|
search?: string
|
||||||
|
acdntSn?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowToAnalysis(r: Record<string, unknown>): HnsAnalysisItem {
|
function rowToAnalysis(r: Record<string, unknown>): HnsAnalysisItem {
|
||||||
return {
|
return {
|
||||||
hnsAnlysSn: r.hns_anlys_sn as number,
|
hnsAnlysSn: r.hns_anlys_sn as number,
|
||||||
|
acdntSn: (r.acdnt_sn as number) ?? null,
|
||||||
anlysNm: r.anlys_nm as string,
|
anlysNm: r.anlys_nm as string,
|
||||||
acdntDtm: r.acdnt_dtm as string | null,
|
acdntDtm: r.acdnt_dtm as string | null,
|
||||||
locNm: r.loc_nm as string | null,
|
locNm: r.loc_nm as string | null,
|
||||||
@ -146,7 +149,7 @@ function rowToAnalysis(r: Record<string, unknown>): HnsAnalysisItem {
|
|||||||
|
|
||||||
export async function listAnalyses(input: ListAnalysesInput): Promise<HnsAnalysisItem[]> {
|
export async function listAnalyses(input: ListAnalysesInput): Promise<HnsAnalysisItem[]> {
|
||||||
const conditions: string[] = ["USE_YN = 'Y'"]
|
const conditions: string[] = ["USE_YN = 'Y'"]
|
||||||
const params: string[] = []
|
const params: (string | number)[] = []
|
||||||
let idx = 1
|
let idx = 1
|
||||||
|
|
||||||
if (input.status) {
|
if (input.status) {
|
||||||
@ -162,9 +165,13 @@ export async function listAnalyses(input: ListAnalysesInput): Promise<HnsAnalysi
|
|||||||
params.push(input.search)
|
params.push(input.search)
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
if (input.acdntSn != null) {
|
||||||
|
conditions.push(`ACDNT_SN = $${idx++}`)
|
||||||
|
params.push(input.acdntSn)
|
||||||
|
}
|
||||||
|
|
||||||
const { rows } = await wingPool.query(
|
const { rows } = await wingPool.query(
|
||||||
`SELECT HNS_ANLYS_SN, ANLYS_NM, ACDNT_DTM, LOC_NM, LON, LAT,
|
`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,
|
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,
|
WIND_SPD, WIND_DIR, EXEC_STTS_CD, RISK_CD, ANALYST_NM,
|
||||||
RSLT_DATA, REG_DTM
|
RSLT_DATA, REG_DTM
|
||||||
@ -179,7 +186,7 @@ export async function listAnalyses(input: ListAnalysesInput): Promise<HnsAnalysi
|
|||||||
|
|
||||||
export async function getAnalysis(sn: number): Promise<HnsAnalysisItem | null> {
|
export async function getAnalysis(sn: number): Promise<HnsAnalysisItem | null> {
|
||||||
const { rows } = await wingPool.query(
|
const { rows } = await wingPool.query(
|
||||||
`SELECT HNS_ANLYS_SN, ANLYS_NM, ACDNT_DTM, LOC_NM, LON, LAT,
|
`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,
|
SBST_NM, SPIL_QTY, SPIL_UNIT_CD, FCST_HR, ALGO_CD, CRIT_MDL_CD,
|
||||||
WIND_SPD, WIND_DIR, TEMP, HUMID, ATM_STBL_CD,
|
WIND_SPD, WIND_DIR, TEMP, HUMID, ATM_STBL_CD,
|
||||||
EXEC_STTS_CD, RISK_CD, ANALYST_NM,
|
EXEC_STTS_CD, RISK_CD, ANALYST_NM,
|
||||||
|
|||||||
@ -25,6 +25,8 @@ interface IncidentListItem {
|
|||||||
spilUnitCd: string | null;
|
spilUnitCd: string | null;
|
||||||
fcstHr: number | null;
|
fcstHr: number | null;
|
||||||
hasPredCompleted: boolean;
|
hasPredCompleted: boolean;
|
||||||
|
hasHnsCompleted: boolean;
|
||||||
|
hasRescueCompleted: boolean;
|
||||||
mediaCnt: number;
|
mediaCnt: number;
|
||||||
hasImgAnalysis: boolean;
|
hasImgAnalysis: boolean;
|
||||||
}
|
}
|
||||||
@ -118,6 +120,18 @@ export async function listIncidents(filters: {
|
|||||||
SELECT 1 FROM wing.PRED_EXEC pe
|
SELECT 1 FROM wing.PRED_EXEC pe
|
||||||
WHERE pe.ACDNT_SN = a.ACDNT_SN AND pe.EXEC_STTS_CD = 'COMPLETED'
|
WHERE pe.ACDNT_SN = a.ACDNT_SN AND pe.EXEC_STTS_CD = 'COMPLETED'
|
||||||
) AS has_pred_completed,
|
) AS has_pred_completed,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM wing.HNS_ANALYSIS h
|
||||||
|
WHERE h.ACDNT_SN = a.ACDNT_SN
|
||||||
|
AND h.EXEC_STTS_CD = 'COMPLETED'
|
||||||
|
AND h.USE_YN = 'Y'
|
||||||
|
) AS has_hns_completed,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM wing.RESCUE_OPS r
|
||||||
|
WHERE r.ACDNT_SN = a.ACDNT_SN
|
||||||
|
AND r.STTS_CD = 'RESOLVED'
|
||||||
|
AND r.USE_YN = 'Y'
|
||||||
|
) AS has_rescue_completed,
|
||||||
COALESCE(m.PHOTO_CNT, 0) + COALESCE(m.VIDEO_CNT, 0)
|
COALESCE(m.PHOTO_CNT, 0) + COALESCE(m.VIDEO_CNT, 0)
|
||||||
+ COALESCE(m.SAT_CNT, 0) + COALESCE(m.CCTV_CNT, 0) AS media_cnt
|
+ COALESCE(m.SAT_CNT, 0) + COALESCE(m.CCTV_CNT, 0) AS media_cnt
|
||||||
FROM wing.ACDNT a
|
FROM wing.ACDNT a
|
||||||
@ -157,6 +171,8 @@ export async function listIncidents(filters: {
|
|||||||
spilUnitCd: (r.spil_unit_cd as string) ?? null,
|
spilUnitCd: (r.spil_unit_cd as string) ?? null,
|
||||||
fcstHr: (r.fcst_hr as number) ?? null,
|
fcstHr: (r.fcst_hr as number) ?? null,
|
||||||
hasPredCompleted: r.has_pred_completed as boolean,
|
hasPredCompleted: r.has_pred_completed as boolean,
|
||||||
|
hasHnsCompleted: r.has_hns_completed as boolean,
|
||||||
|
hasRescueCompleted: r.has_rescue_completed as boolean,
|
||||||
mediaCnt: Number(r.media_cnt),
|
mediaCnt: Number(r.media_cnt),
|
||||||
hasImgAnalysis: (r.has_img_analysis as boolean) ?? false,
|
hasImgAnalysis: (r.has_img_analysis as boolean) ?? false,
|
||||||
}));
|
}));
|
||||||
@ -177,6 +193,18 @@ export async function getIncident(acdntSn: number): Promise<IncidentDetail | nul
|
|||||||
SELECT 1 FROM wing.PRED_EXEC pe
|
SELECT 1 FROM wing.PRED_EXEC pe
|
||||||
WHERE pe.ACDNT_SN = a.ACDNT_SN AND pe.EXEC_STTS_CD = 'COMPLETED'
|
WHERE pe.ACDNT_SN = a.ACDNT_SN AND pe.EXEC_STTS_CD = 'COMPLETED'
|
||||||
) AS has_pred_completed,
|
) AS has_pred_completed,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM wing.HNS_ANALYSIS h
|
||||||
|
WHERE h.ACDNT_SN = a.ACDNT_SN
|
||||||
|
AND h.EXEC_STTS_CD = 'COMPLETED'
|
||||||
|
AND h.USE_YN = 'Y'
|
||||||
|
) AS has_hns_completed,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM wing.RESCUE_OPS r
|
||||||
|
WHERE r.ACDNT_SN = a.ACDNT_SN
|
||||||
|
AND r.STTS_CD = 'RESOLVED'
|
||||||
|
AND r.USE_YN = 'Y'
|
||||||
|
) AS has_rescue_completed,
|
||||||
COALESCE(m.PHOTO_CNT, 0) + COALESCE(m.VIDEO_CNT, 0)
|
COALESCE(m.PHOTO_CNT, 0) + COALESCE(m.VIDEO_CNT, 0)
|
||||||
+ COALESCE(m.SAT_CNT, 0) + COALESCE(m.CCTV_CNT, 0) AS media_cnt
|
+ COALESCE(m.SAT_CNT, 0) + COALESCE(m.CCTV_CNT, 0) AS media_cnt
|
||||||
FROM wing.ACDNT a
|
FROM wing.ACDNT a
|
||||||
@ -222,6 +250,8 @@ export async function getIncident(acdntSn: number): Promise<IncidentDetail | nul
|
|||||||
spilUnitCd: (r.spil_unit_cd as string) ?? null,
|
spilUnitCd: (r.spil_unit_cd as string) ?? null,
|
||||||
fcstHr: (r.fcst_hr as number) ?? null,
|
fcstHr: (r.fcst_hr as number) ?? null,
|
||||||
hasPredCompleted: r.has_pred_completed as boolean,
|
hasPredCompleted: r.has_pred_completed as boolean,
|
||||||
|
hasHnsCompleted: r.has_hns_completed as boolean,
|
||||||
|
hasRescueCompleted: r.has_rescue_completed as boolean,
|
||||||
mediaCnt: Number(r.media_cnt),
|
mediaCnt: Number(r.media_cnt),
|
||||||
hasImgAnalysis: (r.has_img_analysis as boolean) ?? false,
|
hasImgAnalysis: (r.has_img_analysis as boolean) ?? false,
|
||||||
predictions,
|
predictions,
|
||||||
|
|||||||
@ -10,11 +10,13 @@ const router = express.Router();
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
router.get('/ops', requireAuth, requirePermission('rescue', 'READ'), async (req, res) => {
|
router.get('/ops', requireAuth, requirePermission('rescue', 'READ'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { sttsCd, acdntTpCd, search } = req.query;
|
const { sttsCd, acdntTpCd, search, acdntSn } = req.query;
|
||||||
|
const acdntSnNum = acdntSn ? parseInt(acdntSn as string, 10) : undefined;
|
||||||
const items = await listOps({
|
const items = await listOps({
|
||||||
sttsCd: sttsCd as string | undefined,
|
sttsCd: sttsCd as string | undefined,
|
||||||
acdntTpCd: acdntTpCd as string | undefined,
|
acdntTpCd: acdntTpCd as string | undefined,
|
||||||
search: search as string | undefined,
|
search: search as string | undefined,
|
||||||
|
acdntSn: acdntSnNum && !Number.isNaN(acdntSnNum) ? acdntSnNum : undefined,
|
||||||
});
|
});
|
||||||
res.json(items);
|
res.json(items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -59,6 +59,7 @@ interface ListOpsInput {
|
|||||||
sttsCd?: string;
|
sttsCd?: string;
|
||||||
acdntTpCd?: string;
|
acdntTpCd?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
acdntSn?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@ -82,6 +83,10 @@ export async function listOps(input?: ListOpsInput): Promise<RescueOpsListItem[]
|
|||||||
conditions.push(`VESSEL_NM ILIKE '%' || $${idx++} || '%'`);
|
conditions.push(`VESSEL_NM ILIKE '%' || $${idx++} || '%'`);
|
||||||
params.push(input.search);
|
params.push(input.search);
|
||||||
}
|
}
|
||||||
|
if (input?.acdntSn != null) {
|
||||||
|
conditions.push(`ACDNT_SN = $${idx++}`);
|
||||||
|
params.push(input.acdntSn);
|
||||||
|
}
|
||||||
|
|
||||||
const where = 'WHERE ' + conditions.join(' AND ');
|
const where = 'WHERE ' + conditions.join(' AND ');
|
||||||
|
|
||||||
|
|||||||
118
database/migration/032_sync_gsc_accidents_to_wing.sql
Normal file
118
database/migration/032_sync_gsc_accidents_to_wing.sql
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 032: gsc.tgs_acdnt_info → wing.ACDNT 동기화 (2026-04-10 이후)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 목적
|
||||||
|
-- 3개 예측 탭(유출유확산예측 / HNS 대기확산 / 긴급구난)의 사고
|
||||||
|
-- 선택 셀렉트박스에 노출되는 gsc 사고 레코드를 wing.ACDNT에
|
||||||
|
-- 이관하여 wing 운영 로직과 동일한 사고 마스터를 공유한다.
|
||||||
|
--
|
||||||
|
-- 필터 정책 (backend/src/gsc/gscAccidentsService.ts 의 listGscAccidents 와 동일)
|
||||||
|
-- - acdnt_asort_code IN (12개 코드)
|
||||||
|
-- - acdnt_title IS NOT NULL
|
||||||
|
-- - 좌표(tgs_acdnt_lc.la, lo) 존재
|
||||||
|
-- - rcept_dt >= '2026-04-10' (본 이관 추가 조건)
|
||||||
|
--
|
||||||
|
-- ACDNT_CD 생성 규칙
|
||||||
|
-- 'INC-YYYY-NNNN' (YYYY = rcept_dt 의 연도, NNNN = 해당 연도 내 순번 4자리)
|
||||||
|
-- 기존 wing.ACDNT 에 이미 부여된 'INC-YYYY-NNNN' 중 같은 연도의 최대 순번을
|
||||||
|
-- 구해 이어서 증가시킨다.
|
||||||
|
--
|
||||||
|
-- 중복 방지
|
||||||
|
-- (ACDNT_NM = acdnt_title, OCCRN_DTM = rcept_dt) 조합이 이미 존재하면 제외.
|
||||||
|
-- acdnt_mng_no 를 별도 컬럼으로 보관하지 않으므로 이 조합을 자연 키로 사용.
|
||||||
|
--
|
||||||
|
-- ACDNT_TP_CD
|
||||||
|
-- gsc.tcm_code.code_nm 으로 치환 (JOIN: tcm_code.code = acdnt_asort_code)
|
||||||
|
-- 매핑 누락 시 원본 코드값으로 폴백.
|
||||||
|
--
|
||||||
|
-- 사전 확인 쿼리 (실행 전 참고)
|
||||||
|
-- SELECT COUNT(DISTINCT a.acdnt_mng_no)
|
||||||
|
-- FROM gsc.tgs_acdnt_info a JOIN gsc.tgs_acdnt_lc b USING (acdnt_mng_no)
|
||||||
|
-- WHERE a.acdnt_asort_code = ANY(ARRAY[
|
||||||
|
-- '055001001','055001002','055001003','055001004','055001005','055001006',
|
||||||
|
-- '055003001','055003002','055003003','055003004','055003005','055004003'
|
||||||
|
-- ]::varchar[])
|
||||||
|
-- AND a.acdnt_title IS NOT NULL
|
||||||
|
-- AND a.rcept_dt >= '2026-04-10';
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
WITH src AS (
|
||||||
|
SELECT DISTINCT ON (a.acdnt_mng_no)
|
||||||
|
a.acdnt_mng_no,
|
||||||
|
a.acdnt_title,
|
||||||
|
a.acdnt_asort_code,
|
||||||
|
a.rcept_dt,
|
||||||
|
b.la,
|
||||||
|
b.lo
|
||||||
|
FROM gsc.tgs_acdnt_info AS a
|
||||||
|
JOIN gsc.tgs_acdnt_lc AS b ON a.acdnt_mng_no = b.acdnt_mng_no
|
||||||
|
WHERE a.acdnt_asort_code = ANY(ARRAY[
|
||||||
|
'055001001','055001002','055001003','055001004','055001005','055001006',
|
||||||
|
'055003001','055003002','055003003','055003004','055003005','055004003'
|
||||||
|
]::varchar[])
|
||||||
|
AND a.acdnt_title IS NOT NULL
|
||||||
|
AND a.rcept_dt >= '2026-04-10'::timestamptz
|
||||||
|
AND b.la IS NOT NULL AND b.lo IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM wing.ACDNT w
|
||||||
|
WHERE w.ACDNT_NM = a.acdnt_title
|
||||||
|
AND w.OCCRN_DTM = a.rcept_dt
|
||||||
|
)
|
||||||
|
ORDER BY a.acdnt_mng_no, b.acdnt_lc_sn ASC
|
||||||
|
),
|
||||||
|
numbered AS (
|
||||||
|
SELECT
|
||||||
|
src.*,
|
||||||
|
EXTRACT(YEAR FROM src.rcept_dt)::int AS yr,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY EXTRACT(YEAR FROM src.rcept_dt)
|
||||||
|
ORDER BY src.rcept_dt ASC, src.acdnt_mng_no ASC
|
||||||
|
) AS rn_in_year
|
||||||
|
FROM src
|
||||||
|
),
|
||||||
|
year_max AS (
|
||||||
|
SELECT
|
||||||
|
(split_part(ACDNT_CD, '-', 2))::int AS yr,
|
||||||
|
MAX((split_part(ACDNT_CD, '-', 3))::int) AS max_seq
|
||||||
|
FROM wing.ACDNT
|
||||||
|
WHERE ACDNT_CD ~ '^INC-[0-9]{4}-[0-9]+$'
|
||||||
|
GROUP BY split_part(ACDNT_CD, '-', 2)
|
||||||
|
)
|
||||||
|
INSERT INTO wing.ACDNT (
|
||||||
|
ACDNT_CD, ACDNT_NM, ACDNT_TP_CD, ACDNT_STTS_CD,
|
||||||
|
LAT, LNG, LOC_GEOM, LOC_DC, OCCRN_DTM, REG_DTM, MDFCN_DTM
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'INC-' || lpad(n.yr::text, 4, '0') || '-' ||
|
||||||
|
lpad((COALESCE(ym.max_seq, 0) + n.rn_in_year)::text, 4, '0') AS ACDNT_CD,
|
||||||
|
n.acdnt_title AS ACDNT_NM,
|
||||||
|
COALESCE(c.code_nm, n.acdnt_asort_code) AS ACDNT_TP_CD,
|
||||||
|
'ACTIVE' AS ACDNT_STTS_CD,
|
||||||
|
n.la::numeric AS LAT,
|
||||||
|
n.lo::numeric AS LNG,
|
||||||
|
ST_SetSRID(ST_MakePoint(n.lo::float8, n.la::float8), 4326) AS LOC_GEOM,
|
||||||
|
NULL AS LOC_DC,
|
||||||
|
n.rcept_dt AS OCCRN_DTM,
|
||||||
|
NOW(), NOW()
|
||||||
|
FROM numbered n
|
||||||
|
LEFT JOIN year_max ym ON ym.yr = n.yr
|
||||||
|
LEFT JOIN gsc.tcm_code c ON c.code = n.acdnt_asort_code
|
||||||
|
ORDER BY n.rcept_dt ASC, n.acdnt_mng_no ASC;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 사후 검증 (필요 시 주석 해제 실행)
|
||||||
|
-- SELECT COUNT(*) FROM wing.ACDNT WHERE OCCRN_DTM >= '2026-04-10';
|
||||||
|
--
|
||||||
|
-- SELECT ACDNT_CD, ACDNT_NM, ACDNT_TP_CD, ST_AsText(LOC_GEOM), OCCRN_DTM
|
||||||
|
-- FROM wing.ACDNT
|
||||||
|
-- WHERE OCCRN_DTM >= '2026-04-10'
|
||||||
|
-- ORDER BY ACDNT_CD DESC
|
||||||
|
-- LIMIT 20;
|
||||||
|
--
|
||||||
|
-- SELECT ACDNT_TP_CD, COUNT(*)
|
||||||
|
-- FROM wing.ACDNT
|
||||||
|
-- WHERE OCCRN_DTM >= '2026-04-10'
|
||||||
|
-- GROUP BY 1
|
||||||
|
-- ORDER BY 2 DESC;
|
||||||
|
-- ============================================================
|
||||||
@ -6,6 +6,7 @@ import { api } from '@common/services/api';
|
|||||||
|
|
||||||
export interface HnsAnalysisItem {
|
export interface HnsAnalysisItem {
|
||||||
hnsAnlysSn: number;
|
hnsAnlysSn: number;
|
||||||
|
acdntSn: number | null;
|
||||||
anlysNm: string;
|
anlysNm: string;
|
||||||
acdntDtm: string | null;
|
acdntDtm: string | null;
|
||||||
locNm: string | null;
|
locNm: string | null;
|
||||||
@ -50,6 +51,7 @@ export async function fetchHnsAnalyses(params?: {
|
|||||||
status?: string;
|
status?: string;
|
||||||
substance?: string;
|
substance?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
acdntSn?: number;
|
||||||
}): Promise<HnsAnalysisItem[]> {
|
}): Promise<HnsAnalysisItem[]> {
|
||||||
const response = await api.get<HnsAnalysisItem[]>('/hns/analyses', { params });
|
const response = await api.get<HnsAnalysisItem[]>('/hns/analyses', { params });
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@ -12,6 +12,10 @@ import type {
|
|||||||
} from '@tabs/prediction/services/predictionApi';
|
} from '@tabs/prediction/services/predictionApi';
|
||||||
import { fetchNearbyOrgs } from '../services/incidentsApi';
|
import { fetchNearbyOrgs } from '../services/incidentsApi';
|
||||||
import type { NearbyOrgItem } from '../services/incidentsApi';
|
import type { NearbyOrgItem } from '../services/incidentsApi';
|
||||||
|
import { fetchHnsAnalyses } from '@tabs/hns/services/hnsApi';
|
||||||
|
import type { HnsAnalysisItem } from '@tabs/hns/services/hnsApi';
|
||||||
|
import { fetchRescueOps } from '@tabs/rescue/services/rescueApi';
|
||||||
|
import type { RescueOpsItem } from '@tabs/rescue/services/rescueApi';
|
||||||
|
|
||||||
export type ViewMode = 'overlay' | 'split2' | 'split3';
|
export type ViewMode = 'overlay' | 'split2' | 'split3';
|
||||||
|
|
||||||
@ -35,6 +39,12 @@ interface IncidentsRightPanelProps {
|
|||||||
onCheckedPredsChange?: (
|
onCheckedPredsChange?: (
|
||||||
checked: Array<{ id: string; acdntSn: number; predRunSn: number | null; occurredAt: string }>,
|
checked: Array<{ id: string; acdntSn: number; predRunSn: number | null; occurredAt: string }>,
|
||||||
) => void;
|
) => void;
|
||||||
|
onCheckedHnsChange?: (
|
||||||
|
checked: Array<{ id: string; hnsAnlysSn: number; acdntSn: number | null }>,
|
||||||
|
) => void;
|
||||||
|
onCheckedRescueChange?: (
|
||||||
|
checked: Array<{ id: string; rescueOpsSn: number; acdntSn: number | null }>,
|
||||||
|
) => void;
|
||||||
onSensitiveDataChange?: (
|
onSensitiveDataChange?: (
|
||||||
geojson: SensitiveResourceFeatureCollection | null,
|
geojson: SensitiveResourceFeatureCollection | null,
|
||||||
checkedCategories: Set<string>,
|
checkedCategories: Set<string>,
|
||||||
@ -115,23 +125,21 @@ function getActiveModels(p: PredictionAnalysis): string {
|
|||||||
return models || '분석중';
|
return models || '분석중';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── HNS/구난 섹션 (미개발, 고정 구조만 유지) ────── */
|
/* ── 섹션 메타 (색상/아이콘) ────── */
|
||||||
const STATIC_SECTIONS = [
|
const SECTION_META = {
|
||||||
{
|
hns: {
|
||||||
key: 'hns',
|
|
||||||
icon: '🧪',
|
icon: '🧪',
|
||||||
title: 'HNS 대기확산',
|
title: 'HNS 대기확산',
|
||||||
color: 'var(--color-accent)',
|
color: 'var(--color-warning)',
|
||||||
colorRgb: '6,182,212',
|
colorRgb: '249,115,22',
|
||||||
},
|
},
|
||||||
{
|
rescue: {
|
||||||
key: 'rsc',
|
|
||||||
icon: '🚨',
|
icon: '🚨',
|
||||||
title: '긴급구난',
|
title: '긴급구난',
|
||||||
color: 'var(--color-accent)',
|
color: 'var(--color-accent)',
|
||||||
colorRgb: '6,182,212',
|
colorRgb: '6,182,212',
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
|
|
||||||
/* ── Component ───────────────────────────────────── */
|
/* ── Component ───────────────────────────────────── */
|
||||||
|
|
||||||
@ -143,11 +151,17 @@ export function IncidentsRightPanel({
|
|||||||
analysisActive,
|
analysisActive,
|
||||||
onCloseAnalysis,
|
onCloseAnalysis,
|
||||||
onCheckedPredsChange,
|
onCheckedPredsChange,
|
||||||
|
onCheckedHnsChange,
|
||||||
|
onCheckedRescueChange,
|
||||||
onSensitiveDataChange,
|
onSensitiveDataChange,
|
||||||
selectedVessel,
|
selectedVessel,
|
||||||
}: IncidentsRightPanelProps) {
|
}: IncidentsRightPanelProps) {
|
||||||
const [predItems, setPredItems] = useState<PredictionAnalysis[]>([]);
|
const [predItems, setPredItems] = useState<PredictionAnalysis[]>([]);
|
||||||
const [checkedPredIds, setCheckedPredIds] = useState<Set<string>>(new Set());
|
const [checkedPredIds, setCheckedPredIds] = useState<Set<string>>(new Set());
|
||||||
|
const [hnsItems, setHnsItems] = useState<HnsAnalysisItem[]>([]);
|
||||||
|
const [checkedHnsIds, setCheckedHnsIds] = useState<Set<string>>(new Set());
|
||||||
|
const [rescueItems, setRescueItems] = useState<RescueOpsItem[]>([]);
|
||||||
|
const [checkedRescueIds, setCheckedRescueIds] = useState<Set<string>>(new Set());
|
||||||
const [sensCategories, setSensCategories] = useState<SensitiveResourceCategory[]>([]);
|
const [sensCategories, setSensCategories] = useState<SensitiveResourceCategory[]>([]);
|
||||||
const [checkedSensCategories, setCheckedSensCategories] = useState<Set<string>>(new Set());
|
const [checkedSensCategories, setCheckedSensCategories] = useState<Set<string>>(new Set());
|
||||||
const [sensitiveGeojson, setSensitiveGeojson] =
|
const [sensitiveGeojson, setSensitiveGeojson] =
|
||||||
@ -160,9 +174,13 @@ export function IncidentsRightPanel({
|
|||||||
if (!incident) {
|
if (!incident) {
|
||||||
void Promise.resolve().then(() => {
|
void Promise.resolve().then(() => {
|
||||||
setPredItems([]);
|
setPredItems([]);
|
||||||
|
setHnsItems([]);
|
||||||
|
setRescueItems([]);
|
||||||
setSensCategories([]);
|
setSensCategories([]);
|
||||||
setSensitiveGeojson(null);
|
setSensitiveGeojson(null);
|
||||||
onCheckedPredsChange?.([]);
|
onCheckedPredsChange?.([]);
|
||||||
|
onCheckedHnsChange?.([]);
|
||||||
|
onCheckedRescueChange?.([]);
|
||||||
onSensitiveDataChange?.(null, new Set(), []);
|
onSensitiveDataChange?.(null, new Set(), []);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -183,6 +201,34 @@ export function IncidentsRightPanel({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => setPredItems([]));
|
.catch(() => setPredItems([]));
|
||||||
|
fetchHnsAnalyses({ acdntSn, status: 'COMPLETED' })
|
||||||
|
.then((items) => {
|
||||||
|
setHnsItems(items);
|
||||||
|
const allIds = new Set(items.map((i) => String(i.hnsAnlysSn)));
|
||||||
|
setCheckedHnsIds(allIds);
|
||||||
|
onCheckedHnsChange?.(
|
||||||
|
items.map((h) => ({
|
||||||
|
id: String(h.hnsAnlysSn),
|
||||||
|
hnsAnlysSn: h.hnsAnlysSn,
|
||||||
|
acdntSn: h.acdntSn,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => setHnsItems([]));
|
||||||
|
fetchRescueOps({ acdntSn, sttsCd: 'RESOLVED' })
|
||||||
|
.then((items) => {
|
||||||
|
setRescueItems(items);
|
||||||
|
const allIds = new Set(items.map((i) => String(i.rescueOpsSn)));
|
||||||
|
setCheckedRescueIds(allIds);
|
||||||
|
onCheckedRescueChange?.(
|
||||||
|
items.map((r) => ({
|
||||||
|
id: String(r.rescueOpsSn),
|
||||||
|
rescueOpsSn: r.rescueOpsSn,
|
||||||
|
acdntSn: r.acdntSn,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => setRescueItems([]));
|
||||||
Promise.all([fetchSensitiveResources(acdntSn), fetchSensitiveResourcesGeojson(acdntSn)])
|
Promise.all([fetchSensitiveResources(acdntSn), fetchSensitiveResourcesGeojson(acdntSn)])
|
||||||
.then(([cats, geojson]) => {
|
.then(([cats, geojson]) => {
|
||||||
const allCategories = new Set(cats.map((c) => c.category));
|
const allCategories = new Set(cats.map((c) => c.category));
|
||||||
@ -276,6 +322,76 @@ export function IncidentsRightPanel({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleHnsItem = (id: string) => {
|
||||||
|
setCheckedHnsIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
onCheckedHnsChange?.(
|
||||||
|
hnsItems
|
||||||
|
.filter((h) => next.has(String(h.hnsAnlysSn)))
|
||||||
|
.map((h) => ({ id: String(h.hnsAnlysSn), hnsAnlysSn: h.hnsAnlysSn, acdntSn: h.acdntSn })),
|
||||||
|
);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHnsItem = (id: string) => {
|
||||||
|
setHnsItems((prev) => {
|
||||||
|
const next = prev.filter((h) => String(h.hnsAnlysSn) !== id);
|
||||||
|
onCheckedHnsChange?.(
|
||||||
|
next
|
||||||
|
.filter((h) => checkedHnsIds.has(String(h.hnsAnlysSn)))
|
||||||
|
.map((h) => ({ id: String(h.hnsAnlysSn), hnsAnlysSn: h.hnsAnlysSn, acdntSn: h.acdntSn })),
|
||||||
|
);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setCheckedHnsIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRescueItem = (id: string) => {
|
||||||
|
setCheckedRescueIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
onCheckedRescueChange?.(
|
||||||
|
rescueItems
|
||||||
|
.filter((r) => next.has(String(r.rescueOpsSn)))
|
||||||
|
.map((r) => ({
|
||||||
|
id: String(r.rescueOpsSn),
|
||||||
|
rescueOpsSn: r.rescueOpsSn,
|
||||||
|
acdntSn: r.acdntSn,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRescueItem = (id: string) => {
|
||||||
|
setRescueItems((prev) => {
|
||||||
|
const next = prev.filter((r) => String(r.rescueOpsSn) !== id);
|
||||||
|
onCheckedRescueChange?.(
|
||||||
|
next
|
||||||
|
.filter((r) => checkedRescueIds.has(String(r.rescueOpsSn)))
|
||||||
|
.map((r) => ({
|
||||||
|
id: String(r.rescueOpsSn),
|
||||||
|
rescueOpsSn: r.rescueOpsSn,
|
||||||
|
acdntSn: r.acdntSn,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setCheckedRescueIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/* 유출유 섹션을 AnalysisSection 형태로 변환 (통합 분석 실행 콜백용) */
|
/* 유출유 섹션을 AnalysisSection 형태로 변환 (통합 분석 실행 콜백용) */
|
||||||
const oilSection: AnalysisSection = {
|
const oilSection: AnalysisSection = {
|
||||||
key: 'oil',
|
key: 'oil',
|
||||||
@ -297,6 +413,48 @@ export function IncidentsRightPanel({
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hnsSection: AnalysisSection = {
|
||||||
|
key: 'hns',
|
||||||
|
icon: SECTION_META.hns.icon,
|
||||||
|
title: SECTION_META.hns.title,
|
||||||
|
color: SECTION_META.hns.color,
|
||||||
|
colorRgb: SECTION_META.hns.colorRgb,
|
||||||
|
totalLabel: `전체 ${hnsItems.length}건`,
|
||||||
|
items: hnsItems.map((h) => {
|
||||||
|
const id = String(h.hnsAnlysSn);
|
||||||
|
const dateStr = h.regDtm ? h.regDtm.slice(0, 10) : '';
|
||||||
|
const sbst = h.sbstNm || 'HNS';
|
||||||
|
const sub = [h.algoCd, h.fcstHr != null ? `${h.fcstHr}h` : null].filter(Boolean).join(' · ');
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: `${dateStr} ${sbst} 대기확산`.trim(),
|
||||||
|
sub: sub || '-',
|
||||||
|
checked: checkedHnsIds.has(id),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const rescueSection: AnalysisSection = {
|
||||||
|
key: 'rescue',
|
||||||
|
icon: SECTION_META.rescue.icon,
|
||||||
|
title: SECTION_META.rescue.title,
|
||||||
|
color: SECTION_META.rescue.color,
|
||||||
|
colorRgb: SECTION_META.rescue.colorRgb,
|
||||||
|
totalLabel: `전체 ${rescueItems.length}건`,
|
||||||
|
items: rescueItems.map((r) => {
|
||||||
|
const id = String(r.rescueOpsSn);
|
||||||
|
const dateStr = r.regDtm ? r.regDtm.slice(0, 10) : '';
|
||||||
|
const vessel = r.vesselNm || '선박';
|
||||||
|
const sub = [r.acdntTpCd, r.commanderNm].filter(Boolean).join(' · ');
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: `${dateStr} ${vessel} 긴급구난`.trim(),
|
||||||
|
sub: sub || '-',
|
||||||
|
checked: checkedRescueIds.has(id),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if (!incident) {
|
if (!incident) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-full h-full">
|
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-full h-full">
|
||||||
@ -395,32 +553,76 @@ export function IncidentsRightPanel({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* HNS 대기확산 / 긴급구난 섹션 (미개발 - 구조 유지) */}
|
{/* HNS 대기확산 / 긴급구난 섹션 */}
|
||||||
{STATIC_SECTIONS.map((sec) => (
|
{[
|
||||||
<div key={sec.key} className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
{ sec: hnsSection, onToggle: toggleHnsItem, onRemove: removeHnsItem },
|
||||||
<div className="flex items-center justify-between mb-2">
|
{ sec: rescueSection, onToggle: toggleRescueItem, onRemove: removeRescueItem },
|
||||||
<div className="flex items-center gap-1.5">
|
].map(({ sec, onToggle, onRemove }) => {
|
||||||
{/* <span className="text-body-2">{sec.icon}</span> */}
|
const checkedCount = sec.items.filter((it) => it.checked).length;
|
||||||
<span className="text-caption">{sec.title}</span>
|
return (
|
||||||
|
<div key={sec.key} className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-caption">{sec.title}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="text-caption font-semibold cursor-pointer"
|
||||||
|
style={{
|
||||||
|
padding: '3px 10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--stroke-default)',
|
||||||
|
color: sec.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
조회
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{sec.items.length === 0 ? (
|
||||||
|
<div className="text-caption text-fg-disabled text-center py-1.5">
|
||||||
|
예측 실행 이력이 없습니다
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sec.items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
style={{
|
||||||
|
padding: '5px 8px',
|
||||||
|
border: '1px solid var(--stroke-default)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={item.checked}
|
||||||
|
onChange={() => onToggle(item.id)}
|
||||||
|
className="shrink-0"
|
||||||
|
style={{ accentColor: sec.color }}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-caption font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-fg-disabled font-mono text-caption">{item.sub}</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
onClick={() => onRemove(item.id)}
|
||||||
|
title="제거"
|
||||||
|
className="text-caption cursor-pointer text-fg-disabled shrink-0"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 mt-1.5 text-caption text-fg-disabled">
|
||||||
|
선택: <b style={{ color: sec.color }}>{checkedCount}건</b> · {sec.totalLabel}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className="text-caption font-semibold cursor-pointer"
|
|
||||||
style={{
|
|
||||||
padding: '3px 10px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid var(--stroke-default)',
|
|
||||||
color: sec.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
조회
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-caption text-fg-disabled text-center py-1.5">준비 중입니다</div>
|
);
|
||||||
<div className="flex items-center gap-1.5 mt-1.5 text-caption text-fg-disabled">
|
})}
|
||||||
선택: <b style={{ color: sec.color }}>0건</b> · 전체 0건
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 민감자원 */}
|
{/* 민감자원 */}
|
||||||
<div className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
<div className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||||||
|
|||||||
@ -25,6 +25,8 @@ export interface IncidentListItem {
|
|||||||
spilUnitCd: string | null;
|
spilUnitCd: string | null;
|
||||||
fcstHr: number | null;
|
fcstHr: number | null;
|
||||||
hasPredCompleted: boolean;
|
hasPredCompleted: boolean;
|
||||||
|
hasHnsCompleted: boolean;
|
||||||
|
hasRescueCompleted: boolean;
|
||||||
mediaCnt: number;
|
mediaCnt: number;
|
||||||
hasImgAnalysis: boolean;
|
hasImgAnalysis: boolean;
|
||||||
}
|
}
|
||||||
@ -112,7 +114,10 @@ function toCompat(item: IncidentListItem): IncidentCompat {
|
|||||||
location: { lat: item.lat, lon: item.lng },
|
location: { lat: item.lat, lon: item.lng },
|
||||||
causeType: item.acdntTpCd,
|
causeType: item.acdntTpCd,
|
||||||
oilType: item.oilTpCd ?? undefined,
|
oilType: item.oilTpCd ?? undefined,
|
||||||
prediction: item.hasPredCompleted ? '예측완료' : undefined,
|
prediction:
|
||||||
|
item.hasPredCompleted || item.hasHnsCompleted || item.hasRescueCompleted
|
||||||
|
? '예측완료'
|
||||||
|
: undefined,
|
||||||
mediaCount: item.mediaCnt,
|
mediaCount: item.mediaCnt,
|
||||||
hasImgAnalysis: item.hasImgAnalysis || undefined,
|
hasImgAnalysis: item.hasImgAnalysis || undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export async function fetchRescueOps(params?: {
|
|||||||
sttsCd?: string;
|
sttsCd?: string;
|
||||||
acdntTpCd?: string;
|
acdntTpCd?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
acdntSn?: number;
|
||||||
}): Promise<RescueOpsItem[]> {
|
}): Promise<RescueOpsItem[]> {
|
||||||
const response = await api.get<RescueOpsItem[]>('/rescue/ops', { params });
|
const response = await api.get<RescueOpsItem[]>('/rescue/ops', { params });
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user