191 lines
7.2 KiB
React
191 lines
7.2 KiB
React
|
|
import { useState, useCallback } from 'react';
|
||
|
|
import { fetchTyphoonList, fetchTyphoonDetail } from '@/api/weatherApi';
|
||
|
|
|
||
|
|
const LIMIT = 10;
|
||
|
|
const currentYear = new Date().getFullYear();
|
||
|
|
|
||
|
|
const yearOptions = Array.from({ length: currentYear - 2000 + 1 }, (_, i) => currentYear - i);
|
||
|
|
const monthOptions = Array.from({ length: 12 }, (_, i) => String(i + 1).padStart(2, '0'));
|
||
|
|
|
||
|
|
export default function TyphoonInfo() {
|
||
|
|
const [year, setYear] = useState(String(currentYear));
|
||
|
|
const [month, setMonth] = useState('');
|
||
|
|
const [list, setList] = useState([]);
|
||
|
|
const [page, setPage] = useState(1);
|
||
|
|
const [totalPage, setTotalPage] = useState(0);
|
||
|
|
const [isLoading, setIsLoading] = useState(false);
|
||
|
|
const [error, setError] = useState(null);
|
||
|
|
const [expandedIndex, setExpandedIndex] = useState(null);
|
||
|
|
const [detailData, setDetailData] = useState([]);
|
||
|
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
||
|
|
|
||
|
|
const search = useCallback(async (targetPage) => {
|
||
|
|
if (!year) return;
|
||
|
|
|
||
|
|
setIsLoading(true);
|
||
|
|
setError(null);
|
||
|
|
setExpandedIndex(null);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await fetchTyphoonList({
|
||
|
|
typhoonBeginningYear: year,
|
||
|
|
typhoonBeginningMonth: month,
|
||
|
|
page: targetPage,
|
||
|
|
limit: LIMIT,
|
||
|
|
});
|
||
|
|
setList(result.list);
|
||
|
|
setTotalPage(result.totalPage);
|
||
|
|
setPage(targetPage);
|
||
|
|
} catch (err) {
|
||
|
|
setError('태풍정보 조회 중 오류가 발생했습니다.');
|
||
|
|
setList([]);
|
||
|
|
setTotalPage(0);
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}, [year, month]);
|
||
|
|
|
||
|
|
const handleSearch = () => {
|
||
|
|
search(1);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handlePageChange = (newPage) => {
|
||
|
|
search(newPage);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleToggle = useCallback(async (idx, item) => {
|
||
|
|
if (expandedIndex === idx) {
|
||
|
|
setExpandedIndex(null);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setExpandedIndex(idx);
|
||
|
|
setIsDetailLoading(true);
|
||
|
|
setDetailData([]);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const data = await fetchTyphoonDetail({
|
||
|
|
typhoonSequence: item.typhoonSequence,
|
||
|
|
year: item.typhoonBeginningYear || year,
|
||
|
|
});
|
||
|
|
setDetailData(data);
|
||
|
|
} catch {
|
||
|
|
setDetailData([]);
|
||
|
|
} finally {
|
||
|
|
setIsDetailLoading(false);
|
||
|
|
}
|
||
|
|
}, [expandedIndex, year]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="tabWrap is-active">
|
||
|
|
<div className="tabTop">
|
||
|
|
<div className="title">태풍정보</div>
|
||
|
|
<div className="formGroup">
|
||
|
|
<ul>
|
||
|
|
<li>
|
||
|
|
<label>
|
||
|
|
<span>연도</span>
|
||
|
|
<select value={year} onChange={(e) => setYear(e.target.value)}>
|
||
|
|
{yearOptions.map((y) => (
|
||
|
|
<option key={y} value={y}>{y}</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
<label>
|
||
|
|
<span>월</span>
|
||
|
|
<select value={month} onChange={(e) => setMonth(e.target.value)}>
|
||
|
|
<option value="">전체</option>
|
||
|
|
{monthOptions.map((m) => (
|
||
|
|
<option key={m} value={m}>{m}</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
</li>
|
||
|
|
<li className="fgBtn">
|
||
|
|
<button type="button" className="schBtn" onClick={handleSearch}>검색</button>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="tabBtm">
|
||
|
|
{isLoading && <div className="loading">조회 중...</div>}
|
||
|
|
|
||
|
|
{error && <div className="error">{error}</div>}
|
||
|
|
|
||
|
|
{!isLoading && !error && list.length === 0 && (
|
||
|
|
<div className="empty">검색 결과가 없습니다.</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{!isLoading && list.length > 0 && (
|
||
|
|
<>
|
||
|
|
<ul className="colList lineSB">
|
||
|
|
{list.map((item, idx) => {
|
||
|
|
const isExpanded = expandedIndex === idx;
|
||
|
|
const status = item.typhoonEndTime ? '종료' : '진행중';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<li key={idx} style={isExpanded ? { flexDirection: 'column', alignItems: 'stretch' } : undefined}>
|
||
|
|
<a
|
||
|
|
href="#"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
handleToggle(idx, item);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span className="title">
|
||
|
|
{(page - 1) * LIMIT + idx + 1}. {item.typhoonName} ({status})
|
||
|
|
</span>
|
||
|
|
<span className="meta">
|
||
|
|
발생일시 {item.typhoonBeginningTime} / 종료일시 {item.typhoonEndTime || '-'}
|
||
|
|
</span>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
{isExpanded && (
|
||
|
|
<div className={`acdListBox${isExpanded ? ' open' : ''}`}>
|
||
|
|
{isDetailLoading && <div className="loading">상세 조회 중...</div>}
|
||
|
|
|
||
|
|
{!isDetailLoading && detailData.length === 0 && (
|
||
|
|
<div className="empty">진행정보가 없습니다.</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{!isDetailLoading && detailData.length > 0 && (
|
||
|
|
<ul className="acdList">
|
||
|
|
{detailData.map((d, dIdx) => (
|
||
|
|
<li key={dIdx}>
|
||
|
|
<span>발표순서: {d.presentationSequence}</span>
|
||
|
|
<span>시간간격: {d.timeInterval}</span>
|
||
|
|
<span>발표시간: {d.presentationTime}</span>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</li>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
{totalPage > 1 && (
|
||
|
|
<div className="pagination">
|
||
|
|
<button type="button" className={page <= 1 ? 'disabled' : ''} disabled={page <= 1} onClick={() => handlePageChange(page - 1)}><</button>
|
||
|
|
{page > 3 && <button type="button" onClick={() => handlePageChange(1)}>1</button>}
|
||
|
|
{page > 4 && <span className="ellipsis">...</span>}
|
||
|
|
{Array.from({ length: 5 }, (_, i) => page - 2 + i)
|
||
|
|
.filter((p) => p >= 1 && p <= totalPage)
|
||
|
|
.map((p) => (
|
||
|
|
<button key={p} type="button" className={p === page ? 'on' : ''} onClick={() => handlePageChange(p)}>{p}</button>
|
||
|
|
))}
|
||
|
|
{page < totalPage - 3 && <span className="ellipsis">...</span>}
|
||
|
|
{page < totalPage - 2 && <button type="button" onClick={() => handlePageChange(totalPage)}>{totalPage}</button>}
|
||
|
|
<button type="button" className={page >= totalPage ? 'disabled' : ''} disabled={page >= totalPage} onClick={() => handlePageChange(page + 1)}>></button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|