Skip to content

Commit 8f2e4d0

Browse files
author
์ž„์„ ๊ทœ
committed
fix: update public data API handling
1 parent fff17a8 commit 8f2e4d0

23 files changed

Lines changed: 397 additions & 260 deletions

โ€Ž.gitignoreโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public/firewater/*.json
2929

3030
# API Keys
3131
.env
32+
.dev.vars
33+
worker/.dev.vars
3234

3335
# Worker
3436
worker/node_modules

โ€Žsrc/components/FireAnalysis.tsxโ€Ž

Lines changed: 32 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react';
2-
import { fetchFireInfo, isStaleDataError } from '../services/apiClient';
2+
import { fetchAnnualFireStats, isStaleDataError } from '../services/apiClient';
33

44
/* โ”€โ”€โ”€ ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ โ”€โ”€โ”€ */
55
const PALETTE = [
@@ -9,22 +9,16 @@ const PALETTE = [
99
];
1010

1111
/* โ”€โ”€โ”€ ๋‚ ์งœ ํ—ฌํผ โ”€โ”€โ”€ */
12+
const LATEST_AVAILABLE_YEAR = 2024;
13+
1214
function getRecentYears(count: number): string[] {
1315
const years: string[] = [];
14-
const now = new Date();
1516
for (let i = 0; i < count; i++) {
16-
years.push(String(now.getFullYear() - i));
17+
years.push(String(LATEST_AVAILABLE_YEAR - i));
1718
}
1819
return years;
1920
}
2021

21-
function getDateRange(year: string): { searchStDt: string; searchEdDt: string } {
22-
return {
23-
searchStDt: `${year}0101`,
24-
searchEdDt: `${year}1231`,
25-
};
26-
}
27-
2822
/* โ”€โ”€โ”€ ๋„๋„› ์ฐจํŠธ โ”€โ”€โ”€ */
2923
function DonutChart({ data, labelKey, valueKey, title }: { data: any[]; labelKey: string; valueKey: string; title: string }) {
3024
const total = data.reduce((s, d) => s + (Number(d[valueKey]) || 0), 0);
@@ -63,7 +57,6 @@ function DonutChart({ data, labelKey, valueKey, title }: { data: any[]; labelKey
6357
</div>
6458
);
6559
}
66-
6760
/* โ”€โ”€โ”€ ์ˆ˜ํ‰ ๋ฐ” ์ฐจํŠธ โ”€โ”€โ”€ */
6861
function HBarChart({ data, labelKey, valueKey }: { data: any[]; labelKey: string; valueKey: string }) {
6962
const max = Math.max(...data.map(d => Number(d[valueKey]) || 0), 1);
@@ -140,7 +133,7 @@ function StatCard({ icon, iconColor, label, value, sub, loading }: {
140133
/* โ•โ•โ•โ•โ•โ•โ• ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ โ•โ•โ•โ•โ•โ•โ• */
141134
export default function FireAnalysis() {
142135
const years = getRecentYears(6);
143-
const [selectedYear, setSelectedYear] = useState(years[1] || years[0]); // ์ž‘๋…„ ๊ธฐ๋ณธ
136+
const [selectedYear, setSelectedYear] = useState(years[0]);
144137
const [loading, setLoading] = useState(true);
145138
const [apiError, setApiError] = useState<string | null>(null);
146139
const [warning, setWarning] = useState<string | null>(null);
@@ -156,105 +149,34 @@ export default function FireAnalysis() {
156149
const fetchAll = useCallback(async (forceRefresh = false) => {
157150
setLoading(true);
158151
setApiError(null);
159-
const range = getDateRange(selectedYear);
160152

161153
try {
162154
setWarning(null);
163-
const results = await Promise.allSettled([
164-
fetchFireInfo('sido-summary', range, forceRefresh), // 0: ์‹œ๋„๋‹จ์œ„ ํ™”์žฌ๋ฐœ์ƒํ˜„ํ™ฉ
165-
fetchFireInfo('cause', range, forceRefresh), // 1: ๋ฐœํ™”์š”์ธ๋ณ„
166-
fetchFireInfo('place', range, forceRefresh), // 2: ํ™”์žฌ์žฅ์†Œ๋ณ„
167-
fetchFireInfo('sido-casualty', range, forceRefresh), // 3: ์‹œ๋„๋ณ„ ์ธ๋ช…ํ”ผํ•ด
168-
fetchFireInfo('building', range, forceRefresh), // 4: ๊ฑด๋ฌผ๊ตฌ์กฐ๋ณ„
169-
fetchFireInfo('property', range, forceRefresh), // 5: ์žฌ์‚ฐํ”ผํ•ด
170-
]);
171-
172-
let hasStaleData = false;
173-
let staleMessage = '';
174-
175-
const processedResults = results.map(r => {
176-
if (r.status === 'fulfilled') return r;
177-
if (r.status === 'rejected' && isStaleDataError(r.reason)) {
178-
hasStaleData = true;
179-
staleMessage = r.reason.message;
180-
const t = r.reason.cachedAt ? new Date(r.reason.cachedAt).toLocaleTimeString() : '';
181-
staleMessage = `${r.reason.message}${t ? ` (๋งˆ์ง€๋ง‰ ์„ฑ๊ณต ์‹œ๊ฐ: ${t})` : ''}`;
182-
return { status: 'fulfilled', value: r.reason.cachedData } as PromiseFulfilledResult<any>;
183-
}
184-
return r;
185-
});
186-
187-
// ์ „์ฒด ์‹คํŒจ ์—ฌ๋ถ€ ํ™•์ธ
188-
const allFailed = processedResults.every(r => r.status === 'rejected');
189-
if (allFailed) {
190-
const firstErr = (processedResults[0] as PromiseRejectedResult).reason;
191-
setApiError(firstErr?.message || 'ํ™”์žฌ์ •๋ณด API์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
192-
setLoading(false);
193-
return;
194-
}
195-
if (hasStaleData) {
196-
setWarning(staleMessage);
197-
}
198-
199-
// ์‹œ๋„ ์š”์•ฝ โ†’ ์ „๊ตญ ํ•ฉ์‚ฐ
200-
if (processedResults[0].status === 'fulfilled') {
201-
const items = processedResults[0].value?.items || [];
202-
let total = 0, death = 0, injury = 0, propertyDmg = 0, selfExtinguish = 0, falseReport = 0;
203-
const sidoArr: any[] = [];
204-
items.forEach((it: any) => {
205-
const fires = num(it.fireCnt || it.ํ™”์žฌ์ ‘์ˆ˜๊ฑด์ˆ˜);
206-
const d = num(it.deathCnt || it.์‚ฌ๋ง์ž์ˆ˜);
207-
const inj = num(it.injuryCnt || it.๋ถ€์ƒ์ž์ˆ˜);
208-
const prop = num(it.realEstateDmg || it.๋ถ€๋™์‚ฐํ”ผํ•ด๊ธˆ์•ก) + num(it.movablePropertyDmg || it.๋™์‚ฐํ”ผํ•ด๊ธˆ์•ก);
209-
const self = num(it.selfExtinguishCnt || it.์ž์ฒด์ง„ํ™”๊ฑด);
210-
const fal = num(it.falseReportCnt || it.ํ—ˆ์œ„์‹ ๊ณ ๊ฑด์ˆ˜);
211-
total += fires; death += d; injury += inj; propertyDmg += prop; selfExtinguish += self; falseReport += fal;
212-
if (it.sidoNm || it.์‹œ๋„๋ช…) {
213-
sidoArr.push({ name: it.sidoNm || it.์‹œ๋„๋ช…, fires, death: d, injury: inj, property: prop });
214-
}
215-
});
216-
setSummary({ total, death, injury, propertyDmg, selfExtinguish, falseReport });
217-
setSidoData(sidoArr.sort((a, b) => b.fires - a.fires));
155+
let data;
156+
try {
157+
data = await fetchAnnualFireStats(selectedYear, forceRefresh);
158+
} catch (err) {
159+
if (!isStaleDataError(err)) throw err;
160+
data = err.cachedData as Awaited<ReturnType<typeof fetchAnnualFireStats>>;
161+
const t = err.cachedAt ? new Date(err.cachedAt).toLocaleTimeString() : '';
162+
setWarning(`${err.message}${t ? ` (๋งˆ์ง€๋ง‰ ์„ฑ๊ณต ์‹œ๊ฐ: ${t})` : ''}`);
218163
}
219164

220-
// ๋ฐœํ™”์š”์ธ๋ณ„
221-
if (processedResults[1].status === 'fulfilled') {
222-
const items = processedResults[1].value?.items || [];
223-
setCauseData(items.map((it: any) => ({
224-
cause: it.igntnFctrNm || it.๋ฐœํ™”์š”์ธ || '๊ธฐํƒ€',
225-
count: num(it.fireCnt || it.ํ™”์žฌ๊ฑด์ˆ˜),
226-
})).filter((x: any) => x.count > 0).sort((a: any, b: any) => b.count - a.count));
227-
}
228-
229-
// ํ™”์žฌ์žฅ์†Œ๋ณ„
230-
if (processedResults[2].status === 'fulfilled') {
231-
const items = processedResults[2].value?.items || [];
232-
setPlaceData(items.map((it: any) => ({
233-
place: it.firePlceNm || it.ํ™”์žฌ์žฅ์†Œ || '๊ธฐํƒ€',
234-
count: num(it.fireCnt || it.ํ™”์žฌ๊ฑด์ˆ˜),
235-
})).filter((x: any) => x.count > 0).sort((a: any, b: any) => b.count - a.count));
236-
}
237-
238-
// ์‹œ๋„๋ณ„ ์ธ๋ช…ํ”ผํ•ด
239-
if (processedResults[3].status === 'fulfilled') {
240-
const items = processedResults[3].value?.items || [];
241-
setCasualtyData(items.map((it: any) => ({
242-
sido: it.sidoNm || it.์‹œ๋„๋ช… || '๊ธฐํƒ€',
243-
death: num(it.deathCnt || it.์‚ฌ๋ง์ž์ˆ˜),
244-
injury: num(it.injuryCnt || it.๋ถ€์ƒ์ž์ˆ˜),
245-
})).filter((x: any) => (x.death + x.injury) > 0).sort((a: any, b: any) => (b.death + b.injury) - (a.death + a.injury)));
246-
}
247-
248-
// ๊ฑด๋ฌผ๊ตฌ์กฐ๋ณ„
249-
if (processedResults[4].status === 'fulfilled') {
250-
const items = processedResults[4].value?.items || [];
251-
setBuildingData(items.map((it: any) => ({
252-
structure: it.bldgStrcNm || it.๊ฑด๋ฌผ๊ตฌ์กฐ || '๊ธฐํƒ€',
253-
count: num(it.fireCnt || it.ํ™”์žฌ๊ฑด์ˆ˜),
254-
})).filter((x: any) => x.count > 0).sort((a: any, b: any) => b.count - a.count));
255-
}
256-
257-
// ์žฌ์‚ฐํ”ผํ•ด โ€” ํ•ฉ์‚ฐ์€ ์œ„์—์„œ ์ด๋ฏธ ์ฒ˜๋ฆฌ
165+
setSummary({
166+
total: data.summary.totalFires,
167+
death: data.summary.totalDeaths,
168+
injury: data.summary.totalInjuries,
169+
propertyDmg: data.summary.totalPropertyDamage,
170+
selfExtinguish: 0,
171+
falseReport: 0,
172+
});
173+
setSidoData(data.bySido.map(it => ({ name: it.name, fires: it.count, death: 0, injury: 0, property: 0 })));
174+
setCauseData(data.byCause.map(it => ({ cause: it.name || '๊ธฐํƒ€', count: it.count })));
175+
setPlaceData(data.byPlace.map(it => ({ place: it.name || '๊ธฐํƒ€', count: it.count })));
176+
setCasualtyData(data.casualtiesBySido
177+
.map(it => ({ sido: it.name || '๊ธฐํƒ€', death: it.deaths, injury: it.injuries }))
178+
.filter(it => it.death + it.injury > 0));
179+
setBuildingData(data.byFireType.map(it => ({ structure: it.name || '๊ธฐํƒ€', count: it.count })));
258180
} catch (e: any) {
259181
console.error('ํ™”์žฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์˜ค๋ฅ˜:', e);
260182
setApiError(e?.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
@@ -273,7 +195,7 @@ export default function FireAnalysis() {
273195
<div className="flex items-center justify-between flex-wrap gap-3">
274196
<div>
275197
<h2 className="text-2xl font-extrabold text-on-surface font-headline">๐Ÿ”ฅ ํ™”์žฌ ๋ถ„์„</h2>
276-
<p className="text-sm text-on-surface-variant mt-1">์†Œ๋ฐฉ์ฒญ ํ™”์žฌ์ •๋ณด์„œ๋น„์Šค ยท ์ „๊ตญ ๋ฐ์ดํ„ฐ</p>
198+
<p className="text-sm text-on-surface-variant mt-1">์†Œ๋ฐฉ์ฒญ ์—ฐ๊ฐ„ํ™”์žฌํ†ต๊ณ„ ยท ์ „๊ตญ ๋ฐ์ดํ„ฐ</p>
277199
</div>
278200
<div className="flex items-center gap-3">
279201
<select
@@ -299,8 +221,8 @@ export default function FireAnalysis() {
299221
<StatCard icon="payments" iconColor="text-amber-400" label="์žฌ์‚ฐ ํ”ผํ•ด"
300222
value={summary.propertyDmg > 100000000 ? `${(summary.propertyDmg / 100000000).toFixed(1)}์–ต` : `${(summary.propertyDmg / 10000).toFixed(0)}๋งŒ`}
301223
sub="์›" loading={loading} />
302-
<StatCard icon="fire_extinguisher" iconColor="text-green-400" label="์ž์ฒด ์ง„ํ™”" value={summary.selfExtinguish} loading={loading} />
303-
<StatCard icon="report" iconColor="text-gray-400" label="ํ—ˆ์œ„ ์‹ ๊ณ " value={summary.falseReport} loading={loading} />
224+
<StatCard icon="fire_extinguisher" iconColor="text-green-400" label="์ž์ฒด ์ง„ํ™”" value="-" loading={loading} />
225+
<StatCard icon="report" iconColor="text-gray-400" label="ํ—ˆ์œ„ ์‹ ๊ณ " value="-" loading={loading} />
304226
</div>
305227

306228
{/* API ์—๋Ÿฌ ๋ฐฐ๋„ˆ */}
@@ -334,7 +256,7 @@ export default function FireAnalysis() {
334256
<span className="material-symbols-outlined text-5xl text-on-surface-variant/30 mb-3 block">info</span>
335257
<h3 className="text-lg font-bold text-on-surface mb-2">{selectedYear}๋…„ ํ™”์žฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ์—†์Šต๋‹ˆ๋‹ค</h3>
336258
<p className="text-sm text-on-surface-variant max-w-lg mx-auto">
337-
์†Œ๋ฐฉ์ฒญ ํ™”์žฌ ํ†ต๊ณ„๋Š” ๋ณดํ†ต ์ „๋…„๋„๊นŒ์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์—ฐ๋„ ์„ ํƒ์—์„œ ๋” ์ด์ „ ์—ฐ๋„๋ฅผ ์„ ํƒํ•ด ๋ณด์„ธ์š”.
259+
์†Œ๋ฐฉ์ฒญ ์—ฐ๊ฐ„ํ™”์žฌํ†ต๊ณ„๋Š” ํ˜„์žฌ 2015~2024๋…„ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์—ฐ๋„ ์„ ํƒ์—์„œ ๋” ์ด์ „ ์—ฐ๋„๋ฅผ ์„ ํƒํ•ด ๋ณด์„ธ์š”.
338260
</p>
339261
</div>
340262
)}
@@ -447,8 +369,3 @@ export default function FireAnalysis() {
447369
</div>
448370
);
449371
}
450-
451-
/* โ”€โ”€โ”€ ํ—ฌํผ โ”€โ”€โ”€ */
452-
function num(v: any): number {
453-
return parseInt(v) || 0;
454-
}

โ€Žsrc/components/FireDamageView.tsxโ€Ž

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const PALETTE = [
1010

1111
/* โ”€โ”€โ”€ ์‹œ๋„ ๋ชฉ๋ก โ”€โ”€โ”€ */
1212
const SIDO_LIST = [
13-
'์ „์ฒด', '์„œ์šธ', '๋ถ€์‚ฐ', '๋Œ€๊ตฌ', '์ธ์ฒœ', '๊ด‘์ฃผ', '๋Œ€์ „', '์šธ์‚ฐ', '์„ธ์ข…',
14-
'๊ฒฝ๊ธฐ', '๊ฐ•์›', '์ถฉ๋ถ', '์ถฉ๋‚จ', '์ „๋ถ', '์ „๋‚จ', '๊ฒฝ๋ถ', '๊ฒฝ๋‚จ', '์ œ์ฃผ',
13+
'์„œ์šธํŠน๋ณ„์‹œ', '๋ถ€์‚ฐ๊ด‘์—ญ์‹œ', '๋Œ€๊ตฌ๊ด‘์—ญ์‹œ', '์ธ์ฒœ๊ด‘์—ญ์‹œ', '๊ด‘์ฃผ๊ด‘์—ญ์‹œ', '๋Œ€์ „๊ด‘์—ญ์‹œ', '์šธ์‚ฐ๊ด‘์—ญ์‹œ',
14+
'์„ธ์ข…ํŠน๋ณ„์ž์น˜์‹œ', '๊ฒฝ๊ธฐ๋„', '๊ฐ•์›ํŠน๋ณ„์ž์น˜๋„', '์ถฉ์ฒญ๋ถ๋„', '์ถฉ์ฒญ๋‚จ๋„', '์ „๋ถํŠน๋ณ„์ž์น˜๋„',
15+
'์ „๋ผ๋‚จ๋„', '๊ฒฝ์ƒ๋ถ๋„', '๊ฒฝ์ƒ๋‚จ๋„', '์ œ์ฃผํŠน๋ณ„์ž์น˜๋„',
1516
];
1617

1718
/* โ”€โ”€โ”€ ์œ ํ‹ธ โ”€โ”€โ”€ */
@@ -110,7 +111,7 @@ export default function FireDamageView() {
110111
const [items, setItems] = useState<FireDamageItem[]>([]);
111112
const [totalCount, setTotalCount] = useState(0);
112113
const [page, setPage] = useState(1);
113-
const [selectedSido, setSelectedSido] = useState('์ „์ฒด');
114+
const [selectedSido, setSelectedSido] = useState('์„œ์šธํŠน๋ณ„์‹œ');
114115
const PAGE_SIZE = 50;
115116

116117
const fetchData = useCallback(async (forceRefresh = false) => {
@@ -123,10 +124,8 @@ export default function FireDamageView() {
123124
const params: Record<string, string> = {
124125
pageNo: String(page),
125126
numOfRows: String(PAGE_SIZE),
127+
sidoNm: selectedSido,
126128
};
127-
if (selectedSido !== '์ „์ฒด') {
128-
params.lawAddrName = selectedSido;
129-
}
130129
res = await fetchFireDamage(params, forceRefresh);
131130
} catch (e: unknown) {
132131
if (isStaleDataError(e)) {

โ€Žsrc/services/apiClient.tsโ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ export async function fetchFireObjectFireSys(ctpvNm: string, numOfRows = '100',
552552
// โ•โ•โ•โ•โ•โ•โ• ์ง€์—ญ๋ณ„ ํ™”์žฌํ”ผํ•ด ํ˜„ํ™ฉ (1์ผ) โ•โ•โ•โ•โ•โ•โ•
553553
export interface FireDamageItem { ocrnYmdhh: string; gutFsttOgidNm: string; deadPercnt: string; injrdprPercnt: string; prptDmgSbttAmt: string; lawAddrName: string; }
554554
export interface FireDamageResponse { items: FireDamageItem[]; totalCount: number; pageNo: number; numOfRows: number; error?: string; errorCode?: string; }
555-
export async function fetchFireDamage(params?: { pageNo?: string; numOfRows?: string; lawAddrName?: string; }, forceRefresh?: boolean): Promise<FireDamageResponse> {
555+
export async function fetchFireDamage(params?: { pageNo?: string; numOfRows?: string; lawAddrName?: string; sidoNm?: string; startYmd?: string; endYmd?: string; }, forceRefresh?: boolean): Promise<FireDamageResponse> {
556556
return apiFetch<FireDamageResponse>('/api/fire-damage', params, { cacheTtlMs: 1000 * 60 * 60 * 24, forceRefresh });
557557
}
558558

โ€Žworker/.dev.varsโ€Ž

Lines changed: 0 additions & 3 deletions
This file was deleted.

โ€Žworker/src/index.tsโ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,13 @@ export default {
8686

8787
try {
8888
const cacheUrl = new URL(url.toString());
89-
cacheUrl.searchParams.delete('_t'); // ๋ธŒ๋ผ?? ์บ์‹œ๋ฒ„์Šค???๋ผ๋ฏธํ„ฐ ๋ฌด์‹œ
90-
89+
cacheUrl.searchParams.delete('_t'); // ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ๋ฒ„์Šคํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฌด์‹œ
90+
91+
// ์ „์—ญ ์—ฃ์ง€ ์บ์‹œ ๋ฒ„์ „ โ€” ๊ณผ๊ฑฐ์— ๋นˆ ๋ฐฐ์—ด/์—๋Ÿฌ ์‘๋‹ต์ด ์ •์ƒ์ฒ˜๋Ÿผ ์บ์‹ฑ๋œ ๊ฒƒ์„ ์ผ๊ด„ ๋ฌดํšจํ™”
92+
cacheUrl.searchParams.set('_ev', '2');
93+
9194
if (path.startsWith('/api/fire-annual/')) {
92-
cacheUrl.searchParams.set('_cv', '3'); // ๊ธฐ์กด ์บ์‹œ ๋ฒ„์ „ ๊ด€???
95+
cacheUrl.searchParams.set('_cv', '3'); // ๊ธฐ์กด ์บ์‹œ ๋ฒ„์ „ ๊ด€๋ฆฌ์šฉ
9396
}
9497

9598
const cacheKey = new Request(cacheUrl.toString(), { method: 'GET' });

โ€Žworker/src/routes/ambulance.tsโ€Ž

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
* GET /api/ambulance?Q0=์„œ์šธํŠน๋ณ„์‹œ
66
*/
77

8+
import { encodeServiceKey, findPublicDataError } from './publicData';
9+
810
const BASE = 'https://apis.data.go.kr/B552657/AmblInfoInqireService';
911

1012
export async function handleAmbulance(url: URL, apiKey: string): Promise<{ data: unknown; cacheTtl: number }> {
1113
const sido = url.searchParams.get('Q0') || '์„œ์šธํŠน๋ณ„์‹œ';
12-
const qs = `serviceKey=${apiKey}&pageNo=1&numOfRows=1000&Q0=${encodeURIComponent(sido)}`;
14+
const serviceKey = encodeServiceKey(apiKey, 'AMBULANCE_API_KEY');
15+
const qs = `serviceKey=${serviceKey}&pageNo=1&numOfRows=1000&Q0=${encodeURIComponent(sido)}`;
1316
const apiUrl = `${BASE}/getAmblListInfoInqire?${qs}`;
14-
17+
1518
const res = await fetch(apiUrl, { headers: { 'User-Agent': '119-helper-worker/1.0' } });
19+
const text = await res.text();
1620
if (!res.ok) {
17-
throw new Error(`Ambulance API failed with status ${res.status}`);
21+
throw new Error(`Ambulance API ${res.status}: ${text.replace(/\s+/g, ' ').slice(0, 140)}`);
1822
}
19-
20-
const text = await res.text();
23+
24+
const upstreamError = findPublicDataError(text);
25+
if (upstreamError) throw new Error(`Ambulance: ${upstreamError}`);
26+
2127
return { data: { xml: text }, cacheTtl: 86400 }; // 1์ผ ์บ์‹œ
2228
}

โ€Žworker/src/routes/annualFireStats.tsโ€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* Worker์—์„œ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ง‘๊ณ„ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ์— ์š”์•ฝ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
77
*/
88

9+
import { encodeServiceKey } from './publicData';
10+
911
const BASE = 'https://api.odcloud.kr/api/15060386/v1';
1012

1113
// ์—ฐ๋„๋ณ„ UDDI ๋งคํ•‘
@@ -139,8 +141,10 @@ export async function handleAnnualFireStats(
139141
throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ๋„์ž…๋‹ˆ๋‹ค: ${year} (2015~2024 ๊ฐ€๋Šฅ)`);
140142
}
141143

144+
const serviceKey = encodeServiceKey(apiKey, 'ANNUAL_FIRE_API_KEY');
145+
142146
// ๋จผ์ € ์ด ๊ฑด์ˆ˜ ํ™•์ธ
143-
const countUrl = `${BASE}/${uddi}?serviceKey=${encodeURIComponent(apiKey)}&page=1&perPage=1`;
147+
const countUrl = `${BASE}/${uddi}?serviceKey=${serviceKey}&page=1&perPage=1`;
144148
const countRes = await fetch(countUrl, {
145149
headers: { 'User-Agent': '119-helper-worker/1.0' },
146150
});
@@ -159,7 +163,7 @@ export async function handleAnnualFireStats(
159163

160164
// ๋ณ‘๋ ฌ๋กœ ๋ชจ๋“  ํŽ˜์ด์ง€ ์š”์ฒญ (Workers ์œ ๋ฃŒ ํ”Œ๋žœ 30s ์ œํ•œ ๋‚ด)
161165
const fetchPromises = Array.from({ length: totalPages }, async (_, i) => {
162-
const pageUrl = `${BASE}/${uddi}?serviceKey=${encodeURIComponent(apiKey)}&page=${i + 1}&perPage=${perPage}`;
166+
const pageUrl = `${BASE}/${uddi}?serviceKey=${serviceKey}&page=${i + 1}&perPage=${perPage}`;
163167
const res = await fetch(pageUrl, {
164168
headers: { 'User-Agent': '119-helper-worker/1.0' },
165169
});

โ€Žworker/src/routes/consumerHazard.tsโ€Ž

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
55
* GET /api/consumer-hazard
66
*/
77

8+
import { encodeServiceKey, parsePublicDataJson } from './publicData';
9+
810
const BASE = 'https://apis.data.go.kr/B551919/open-api/harm/reception';
911

1012
export async function handleConsumerHazard(url: URL, apiKey: string): Promise<{ data: unknown; cacheTtl: number }> {
11-
const qs = `serviceKey=${apiKey}&pageNo=1&numOfRows=100&apiFormat=json`;
13+
const serviceKey = encodeServiceKey(apiKey, 'CONSUMER_HAZARD_API_KEY');
14+
const qs = `serviceKey=${serviceKey}&pageNo=1&numOfRows=100&apiFormat=json`;
1215
const apiUrl = `${BASE}?${qs}`;
13-
16+
1417
const res = await fetch(apiUrl, { headers: { 'User-Agent': '119-helper-worker/1.0' } });
18+
const text = await res.text();
1519
if (!res.ok) {
16-
throw new Error(`Consumer Hazard API failed with status ${res.status}`);
20+
throw new Error(`Consumer Hazard API ${res.status}: ${text.replace(/\s+/g, ' ').slice(0, 140)}`);
1721
}
18-
19-
const data = await res.json();
22+
23+
const data = parsePublicDataJson(text, 'ConsumerHazard');
2024
return { data, cacheTtl: 86400 }; // 1์ผ ์บ์‹œ
2125
}

0 commit comments

Comments
ย (0)