11import { useState , useEffect , useCallback } from 'react' ;
2- import { fetchFireInfo , isStaleDataError } from '../services/apiClient' ;
2+ import { fetchAnnualFireStats , isStaleDataError } from '../services/apiClient' ;
33
44/* โโโ ์์ ํ๋ ํธ โโโ */
55const PALETTE = [
@@ -9,22 +9,16 @@ const PALETTE = [
99] ;
1010
1111/* โโโ ๋ ์ง ํฌํผ โโโ */
12+ const LATEST_AVAILABLE_YEAR = 2024 ;
13+
1214function 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/* โโโ ๋๋ ์ฐจํธ โโโ */
2923function 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/* โโโ ์ํ ๋ฐ ์ฐจํธ โโโ */
6861function 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/* โโโโโโโ ๋ฉ์ธ ์ปดํฌ๋ํธ โโโโโโโ */
141134export 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- }
0 commit comments