Skip to content

Commit 13293c1

Browse files
committed
fix(ingest): use server time for repeated forest fire events
1 parent bf0699f commit 13293c1

File tree

2 files changed

+70
-26
lines changed

2 files changed

+70
-26
lines changed

src/modules/ingest/app/sources/forest-fire-info.source.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,46 @@ describe('ForestFireInfoSource', () => {
4545
expect(result.events[0].occurredAt).toBe('2025-01-01T21:00:00.000Z');
4646
expect(result.events[0].geo).toEqual({ lat: 38.2, lng: 128.6 });
4747
});
48+
49+
it('should use server time when fire already emitted', async () => {
50+
vi.useFakeTimers();
51+
vi.setSystemTime(new Date('2025-01-02T00:00:00.000Z'));
52+
53+
const responseBody = {
54+
frfrInfoList: [
55+
{
56+
frfr_info_id: 'fire-1',
57+
frfr_prgrs_stcd_str: '진화중',
58+
frfr_step_issu_cd: '2단계',
59+
frfr_sttmn_addr: '강원도 속초시',
60+
frfr_frng_dtm: '2025-01-01 06:00:00',
61+
frfr_lctn_xcrd: '128.6',
62+
frfr_lctn_ycrd: '38.2',
63+
},
64+
],
65+
};
66+
67+
const fetchMock = vi.fn().mockImplementation(() =>
68+
Promise.resolve(
69+
new Response(JSON.stringify(responseBody), {
70+
status: 200,
71+
headers: { 'Content-Type': 'application/json' },
72+
}),
73+
),
74+
);
75+
vi.stubGlobal('fetch', fetchMock);
76+
77+
const state = JSON.stringify({
78+
seen: {
79+
'fire-1|reported|unknown': '2025-01-01T23:00:00.000Z',
80+
},
81+
highLevelSent: {},
82+
});
83+
84+
const source = new ForestFireInfoSource();
85+
const result = await source.run(state);
86+
87+
expect(result.events).toHaveLength(1);
88+
expect(result.events[0].occurredAt).toBe('2025-01-02T00:00:00.000Z');
89+
});
4890
});

src/modules/ingest/app/sources/forest-fire-info.source.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { z } from 'zod';
22
import { logger } from '@/core/logger';
3-
import type { EventPayload } from '@/modules/events/domain/entity/event.entity';
43
import { EventKinds, EventLevels, EventSources } from '@/modules/events/domain/event.enums';
54
import type { Source, SourceEvent, SourceRunResult } from '../../domain/port/source.interface';
65
import { fetchWithTimeout } from './_shared/fetch-with-timeout';
@@ -86,6 +85,7 @@ export class ForestFireInfoSource implements Source {
8685

8786
const previousState = parseState(state);
8887
const seen = new Map<string, string>(Object.entries(previousState.seen));
88+
const seenFireIds = buildSeenFireIdSet(seen);
8989
const highLevelSent = new Map<string, HighLevelEntry>(Object.entries(previousState.highLevelSent));
9090
const now = new Date();
9191
const nowMs = now.getTime();
@@ -105,7 +105,8 @@ export class ForestFireInfoSource implements Source {
105105
const baseLevel = isStepLevelEnabled(progressStatus) ? mapStepLevel(stepLabel) : EventLevels.Info;
106106
const uniqueKey = buildUniqueKey(fireId, progressStatus, stepLabel);
107107

108-
const occurredAt = resolveOccurredAt(item);
108+
const resolvedOccurredAt = resolveOccurredAt(item);
109+
const occurredAt = seenFireIds.has(fireId) ? nowIso : resolvedOccurredAt;
109110
if (isTooOld(occurredAt, nowMs, EVENT_MAX_AGE_MS)) {
110111
continue;
111112
}
@@ -115,7 +116,7 @@ export class ForestFireInfoSource implements Source {
115116
const level = shouldBoost ? baseLevel : EventLevels.Info;
116117

117118
if (shouldEmitEvent(seen.get(uniqueKey), nowMs, STATE_TTL_MS)) {
118-
events.push(buildEvent(item, occurredAt, progressLabel, progressStatus, stepLabel, level));
119+
events.push(buildEvent(item, occurredAt, progressLabel, stepLabel, level));
119120
}
120121

121122
if (baseLevel !== EventLevels.Info) {
@@ -124,6 +125,7 @@ export class ForestFireInfoSource implements Source {
124125
}
125126

126127
seen.set(uniqueKey, nowIso);
128+
seenFireIds.add(fireId);
127129
}
128130

129131
pruneTimedMap(seen, nowMs, STATE_TTL_MS);
@@ -138,7 +140,6 @@ const buildEvent = (
138140
item: ForestFireItem,
139141
occurredAt: string | null,
140142
progressLabel: string | null,
141-
progressStatus: ProgressStatus,
142143
stepLabel: string | null,
143144
level: EventLevels,
144145
): SourceEvent => {
@@ -154,7 +155,7 @@ const buildEvent = (
154155
regionText,
155156
geo,
156157
level,
157-
payload: buildPayload(item, progressLabel, progressStatus, stepLabel),
158+
payload: item,
158159
};
159160
};
160161

@@ -204,34 +205,35 @@ const buildBody = (
204205
return lines.length > 0 ? lines.join('\n') : null;
205206
};
206207

207-
const buildPayload = (
208-
item: ForestFireItem,
209-
progressLabel: string | null,
210-
progressStatus: ProgressStatus,
211-
stepLabel: string | null,
212-
): EventPayload => {
213-
return {
214-
fireInfoId: normalizeText(item.frfr_info_id),
215-
progressCode: normalizeText(item.frfr_prgrs_stcd),
216-
progressLabel,
217-
progressStatus,
218-
stepLabel,
219-
statementDate: normalizeText(item.frfr_sttmn_dt),
220-
fireAt: normalizeText(item.frfr_frng_dtm),
221-
endAt: normalizeText(item.potfr_end_dtm),
222-
address: normalizeText(item.frfr_sttmn_addr),
223-
coordX: parseCoordinate(item.frfr_lctn_xcrd),
224-
coordY: parseCoordinate(item.frfr_lctn_ycrd),
225-
};
226-
};
227-
228208
const buildUniqueKey = (fireId: string, progressStatus: ProgressStatus, stepLabel: string | null): string => {
229209
const progressKey = progressStatus;
230210
const stepKey = stepLabel ?? 'unknown';
231211

232212
return `${fireId}|${progressKey}|${stepKey}`;
233213
};
234214

215+
function buildSeenFireIdSet(seen: Map<string, string>): Set<string> {
216+
const seenFireIds = new Set<string>();
217+
for (const key of seen.keys()) {
218+
const fireId = extractFireIdFromKey(key);
219+
if (fireId) {
220+
seenFireIds.add(fireId);
221+
}
222+
}
223+
return seenFireIds;
224+
}
225+
226+
function extractFireIdFromKey(key: string): string | null {
227+
const trimmed = key.trim();
228+
if (!trimmed) {
229+
return null;
230+
}
231+
232+
const [fireId] = trimmed.split('|');
233+
const normalized = fireId?.trim();
234+
return normalized ? normalized : null;
235+
}
236+
235237
const resolveOccurredAt = (item: ForestFireItem): string | null => {
236238
const occurredAt = parseKstDateTime(item.frfr_frng_dtm);
237239
if (occurredAt) {

0 commit comments

Comments
 (0)