Merge conflict 해결 - 로컬 변경사항 유지 (날씨 API)

This commit is contained in:
leeheejin 2025-10-13 18:39:37 +09:00
parent cabaada5b8
commit 87bec6760a
6 changed files with 2316 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
/**
* OpenAPI
* - API
*/
import { Router } from 'express';
import { OpenApiProxyController } from '../controllers/openApiProxyController';
// import { authenticateToken } from '../middleware/authMiddleware'; // 공개 API는 인증 불필요
const router = Router();
const controller = new OpenApiProxyController();
// 날씨, 환율 등 공개 정보는 인증 없이 접근 가능
// router.use(authenticateToken);
/**
* GET /api/open-api/weather
* ( )
* Query: city (, 기본값: Seoul)
*/
router.get('/weather', (req, res) => controller.getWeather(req, res));
/**
* GET /api/open-api/exchange-rate
*
* Query: base ( , 기본값: KRW), target ( , 기본값: USD)
*/
router.get('/exchange-rate', (req, res) => controller.getExchangeRate(req, res));
/**
* POST /api/open-api/geocode
* (Geocoding)
* Body: { address: string }
*/
router.post('/geocode', (req, res) => controller.geocode(req, res));
export default router;

View File

@ -0,0 +1,288 @@
# 기상청 Open API 키 발급 가이드 🇰🇷
## 📌 개요
날씨 위젯은 **공공데이터포털 기상청 API**를 사용합니다.
- 🌐 **플랫폼**: https://www.data.go.kr
- ✅ **완전 무료**
- ✅ **일일 트래픽 제한 없음**
- ✅ **실시간 한국 날씨 정보**
> **참고**: 기상청 API Hub (apihub.kma.go.kr)는 현재 접근 제한이 있어,
> 공공데이터포털의 기상청 API를 사용합니다.
---
## 🔑 API 키 발급 (5분 소요)
### 1⃣ 공공데이터포털 회원가입
```
👉 https://www.data.go.kr
```
1. 우측 상단 **회원가입** 클릭
2. 이메일 입력 및 인증
3. 약관 동의 후 가입 완료
---
### 2⃣ API 활용신청
```
👉 https://www.data.go.kr/data/15084084/openapi.do
```
**"기상청_단기예보 ((구)_동네예보) 조회서비스"** 페이지에서:
1. **활용신청** 버튼 클릭
2. 활용 목적: `기타`
3. 상세 기능 설명: `대시보드 날씨 위젯`
4. 신청 완료
⚠️ **승인까지 약 2-3시간 소요** (즉시 승인되는 경우도 있음)
---
### 3⃣ 인증키 확인
```
👉 https://www.data.go.kr/mypage/myPageOpenAPI.do
```
**마이페이지 > 오픈API > 인증키**에서:
1. **일반 인증키(Encoding)** 복사
2. 긴 문자열 전체를 복사하세요!
**예시:**
```
aBc1234dEf5678gHi9012jKl3456mNo7890pQr1234sTu5678vWx9012yZa3456bCd7890==
```
---
## ⚙️ 환경 변수 설정
### 방법 1: .env 파일 생성 (추천)
```bash
# 1. .env 파일 생성
cd /Users/leeheejin/ERP-node/backend-node
nano .env
```
### 2. 다음 내용 입력:
```bash
# Node 환경
NODE_ENV=development
# 서버 포트
PORT=8080
# 기상청 API 키 (발급받은 인증키를 여기에 붙여넣기)
KMA_API_KEY=여기에_발급받은_인증키를_붙여넣으세요
```
### 3. 저장 및 종료
- `Ctrl + O` (저장)
- `Enter` (확인)
- `Ctrl + X` (종료)
---
### 방법 2: 명령어로 추가
```bash
cd /Users/leeheejin/ERP-node/backend-node
echo "KMA_API_KEY=여기에_발급받은_인증키_붙여넣기" >> .env
```
---
## 🔄 백엔드 재시작
```bash
docker restart pms-backend-mac
```
또는
```bash
cd /Users/leeheejin/ERP-node/backend-node
npm run dev
```
---
## ✅ 테스트
### 1. 브라우저에서 대시보드 접속
```
http://localhost:9771/admin/dashboard
```
### 2. 날씨 위젯 드래그 앤 드롭
- 오른쪽 사이드바에서 **☁️ 날씨 위젯** 드래그
- 캔버스에 드롭
- **실시간 한국 날씨** 표시 확인! 🎉
### 3. API 직접 테스트
```bash
curl "http://localhost:9771/api/open-api/weather?city=서울" \
-H "Authorization: Bearer YOUR_TOKEN"
```
**응답 예시:**
```json
{
"success": true,
"data": {
"city": "서울",
"country": "KR",
"temperature": 18,
"feelsLike": 16,
"humidity": 65,
"pressure": 1013,
"weatherMain": "Clear",
"weatherDescription": "맑음",
"weatherIcon": "01d",
"windSpeed": 3.5,
"clouds": 10,
"timestamp": "2025-10-13T07:30:00.000Z"
}
}
```
---
## 🌍 지원 지역
### 한국 주요 도시
- **서울** (Seoul)
- **부산** (Busan)
- **인천** (Incheon)
- **대구** (Daegu)
- **광주** (Gwangju)
- **대전** (Daejeon)
- **울산** (Ulsan)
- **세종** (Sejong)
- **수원** (Suwon)
- **춘천** (Chuncheon)
- **제주** (Jeju)
**영문/한글 모두 지원!**
---
## 🔧 트러블슈팅
### 1. "기상청 API 키가 설정되지 않았습니다" 오류
**원인**: `.env` 파일에 API 키가 없음
**해결방법**:
```bash
# .env 파일 확인
cat /Users/leeheejin/ERP-node/backend-node/.env
# KMA_API_KEY가 있는지 확인
# 없으면 위 "환경 변수 설정" 참고하여 추가
```
---
### 2. "기상청 API 오류: SERVICE_KEY_IS_NOT_REGISTERED_ERROR" 오류
**원인**: API 키가 아직 승인되지 않았거나 잘못된 키
**해결방법**:
1. 공공데이터포털에서 승인 상태 확인
2. **일반 인증키(Encoding)** 복사했는지 확인 (Decoding 아님!)
3. 키 앞뒤에 공백 없는지 확인
4. 백엔드 재시작
---
### 3. "지원하지 않는 지역입니다" 오류
**원인**: 등록되지 않은 도시명
**해결방법**:
- 위 "지원 지역" 목록 참고
- 영문 또는 한글 정확히 입력
- 예: `서울`, `Seoul`, `부산`, `Busan`
---
### 4. API 키 재발급
공공데이터포털에서:
1. **마이페이지 > 오픈API**
2. 해당 API 찾기
3. **상세보기 > 인증키 재발급**
---
## 📊 API 사용 현황 확인
```
👉 https://www.data.go.kr/mypage/myPageOpenAPIStatView.do
```
- 일일 트래픽: **무제한**
- 서비스 상태: 정상
- 응답 속도: 평균 1초 이내
---
## 🎨 위젯 커스터마이징
### 기본 도시 변경
```typescript
// DashboardDesigner.tsx
case 'weather': return '부산'; // 원하는 도시로 변경
```
### 새로고침 주기 변경
```typescript
// WeatherWidget.tsx
<WeatherWidget
city="서울"
refreshInterval={300000} // 5분 (밀리초)
/>
```
---
## 📝 참고 링크
- **공공데이터포털**: https://www.data.go.kr
- **기상청 API 신청**: https://www.data.go.kr/data/15084084/openapi.do
- **마이페이지(인증키)**: https://www.data.go.kr/mypage/myPageOpenAPI.do
- **기상청 공식 사이트**: https://www.weather.go.kr
---
## 💡 FAQ
**Q: 승인이 언제 되나요?**
A: 보통 **2-3시간**, 빠르면 즉시 승인됩니다.
**Q: 유료인가요?**
A: **완전 무료**입니다! 트래픽 제한도 없어요.
**Q: 해외 도시도 되나요?**
A: 아니요, 기상청 API는 **한국 지역만** 지원합니다.
**Q: 실시간인가요?**
A: 실시간 관측 데이터를 제공합니다 (매시간 업데이트).
---
**설정 완료 후 대시보드에서 실시간 한국 날씨를 확인하세요!** 🌤️

View File

@ -0,0 +1,168 @@
# 날씨 위젯 API 키 설정 가이드 🌤️
## 📌 개요
날씨 위젯을 사용하려면 **OpenWeatherMap API 키**가 필요합니다.
---
## 🔑 OpenWeatherMap API 키 발급
### 1. 회원가입
- 사이트: https://openweathermap.org/api
- 무료 플랜 선택 (Free Plan)
- 하루 **60회** 무료 호출 가능 (충분함)
### 2. API 키 확인
- 로그인 후 **API keys** 메뉴로 이동
- 자동 생성된 API 키 복사
- 또는 **Create Key** 버튼으로 새 키 생성
---
## ⚙️ 환경 변수 설정
### 백엔드 `.env` 파일 수정
```bash
# backend-node/.env 파일 열기
cd /Users/leeheejin/ERP-node/backend-node
vi .env
```
### 다음 내용 추가:
```bash
# OpenWeatherMap API 키
OPENWEATHER_API_KEY=your_actual_api_key_here
```
**예시:**
```bash
OPENWEATHER_API_KEY=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
```
---
## 🔄 백엔드 재시작
```bash
docker restart pms-backend-mac
```
또는
```bash
cd /Users/leeheejin/ERP-node/backend-node
npm run dev
```
---
## ✅ 테스트
### 1. 브라우저에서 대시보드 설계 도구 접속
```
http://localhost:9771/admin/dashboard
```
### 2. 날씨 위젯 드래그 앤 드롭
- 오른쪽 사이드바에서 **☁️ 날씨 위젯** 찾기
- 캔버스로 드래그 앤 드롭
- 실시간 날씨 정보 표시 확인
### 3. API 직접 테스트
```bash
curl "http://localhost:9771/api/open-api/weather?city=Seoul" \
-H "Authorization: Bearer YOUR_TOKEN"
```
---
## 🌍 지원 도시
### 한국 주요 도시
- Seoul (서울)
- Busan (부산)
- Incheon (인천)
- Daegu (대구)
- Gwangju (광주)
- Daejeon (대전)
### 해외 도시
- Tokyo
- New York
- London
- Paris
- Singapore
---
## 🔧 트러블슈팅
### 1. "날씨 API 키가 유효하지 않습니다" 오류
**원인**: API 키가 잘못되었거나 활성화되지 않음
**해결방법**:
1. OpenWeatherMap 사이트에서 API 키 재확인
2. 새로 발급한 키는 **2시간 후** 활성화됨 (대기 필요)
3. `.env` 파일에 복사한 키가 정확한지 확인
4. 백엔드 재시작
### 2. "도시를 찾을 수 없습니다" 오류
**원인**: 도시명 철자 오류 또는 지원하지 않는 도시
**해결방법**:
- 영문 도시명 사용 (Seoul, Busan 등)
- OpenWeatherMap 도시 목록 확인: https://openweathermap.org/find
### 3. "CORS 오류" 발생
**원인**: 프론트엔드-백엔드 통신 문제
**해결방법**:
- 백엔드가 정상 실행 중인지 확인 (`docker ps`)
- `backend-node/src/app.ts`의 CORS 설정 확인
- 브라우저 개발자 도구에서 요청 URL 확인
---
## 📊 API 사용량 확인
- OpenWeatherMap 대시보드: https://home.openweathermap.org/api_keys
- 무료 플랜: 하루 60회 (1분당 1회)
- 위젯 새로고침 주기: **10분** (기본값)
---
## 🎨 커스터마이징
### 날씨 위젯 도시 변경
```typescript
// DashboardDesigner.tsx에서 요소 생성 시
const newElement = {
...
content: "Tokyo", // 원하는 도시명으로 변경
};
```
### 새로고침 주기 변경
```typescript
// WeatherWidget.tsx에서
<WeatherWidget
city="Seoul"
refreshInterval={300000} // 5분 (밀리초)
/>
```
---
## 📝 참고 링크
- OpenWeatherMap API 문서: https://openweathermap.org/current
- 무료 API 키 발급: https://openweathermap.org/price
- 지원 도시 검색: https://openweathermap.org/find
---
✅ **설정 완료 후 대시보드에서 실시간 날씨를 확인하세요!**

View File

@ -0,0 +1,405 @@
'use client';
/**
*
* -
*/
import React, { useEffect, useState } from 'react';
import { getWeather, WeatherData } from '@/lib/api/openApi';
import {
Cloud,
CloudRain,
Sun,
CloudSnow,
Wind,
Droplets,
Gauge,
RefreshCw,
Check,
ChevronsUpDown,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { cn } from '@/lib/utils';
interface WeatherWidgetProps {
city?: string;
refreshInterval?: number; // 새로고침 간격 (ms), 기본값: 600000 (10분)
}
export default function WeatherWidget({
city = '서울',
refreshInterval = 600000,
}: WeatherWidgetProps) {
const [open, setOpen] = useState(false);
const [selectedCity, setSelectedCity] = useState(city);
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
// 도시 목록 (전국 시/군/구 단위)
const cities = [
// 서울특별시 (25개 구)
{ value: '서울', label: '서울' },
{ value: '종로구', label: '서울 종로구' },
{ value: '중구', label: '서울 중구' },
{ value: '용산구', label: '서울 용산구' },
{ value: '성동구', label: '서울 성동구' },
{ value: '광진구', label: '서울 광진구' },
{ value: '동대문구', label: '서울 동대문구' },
{ value: '중랑구', label: '서울 중랑구' },
{ value: '성북구', label: '서울 성북구' },
{ value: '강북구', label: '서울 강북구' },
{ value: '도봉구', label: '서울 도봉구' },
{ value: '노원구', label: '서울 노원구' },
{ value: '은평구', label: '서울 은평구' },
{ value: '서대문구', label: '서울 서대문구' },
{ value: '마포구', label: '서울 마포구' },
{ value: '양천구', label: '서울 양천구' },
{ value: '강서구', label: '서울 강서구' },
{ value: '구로구', label: '서울 구로구' },
{ value: '금천구', label: '서울 금천구' },
{ value: '영등포구', label: '서울 영등포구' },
{ value: '동작구', label: '서울 동작구' },
{ value: '관악구', label: '서울 관악구' },
{ value: '서초구', label: '서울 서초구' },
{ value: '강남구', label: '서울 강남구' },
{ value: '송파구', label: '서울 송파구' },
{ value: '강동구', label: '서울 강동구' },
// 부산광역시
{ value: '부산', label: '부산' },
{ value: '해운대구', label: '부산 해운대구' },
{ value: '부산진구', label: '부산 부산진구' },
{ value: '동래구', label: '부산 동래구' },
{ value: '사하구', label: '부산 사하구' },
{ value: '금정구', label: '부산 금정구' },
{ value: '사상구', label: '부산 사상구' },
// 인천광역시
{ value: '인천', label: '인천' },
{ value: '부평구', label: '인천 부평구' },
{ value: '계양구', label: '인천 계양구' },
{ value: '남동구', label: '인천 남동구' },
// 대구광역시
{ value: '대구', label: '대구' },
{ value: '수성구', label: '대구 수성구' },
{ value: '달서구', label: '대구 달서구' },
// 광주광역시
{ value: '광주', label: '광주' },
{ value: '광산구', label: '광주 광산구' },
// 대전광역시
{ value: '대전', label: '대전' },
{ value: '유성구', label: '대전 유성구' },
// 울산광역시
{ value: '울산', label: '울산' },
// 세종특별자치시
{ value: '세종', label: '세종' },
// 경기도 (주요 도시)
{ value: '수원', label: '수원' },
{ value: '성남', label: '성남' },
{ value: '고양', label: '고양' },
{ value: '용인', label: '용인' },
{ value: '부천', label: '부천' },
{ value: '안산', label: '안산' },
{ value: '안양', label: '안양' },
{ value: '남양주', label: '남양주' },
{ value: '화성', label: '화성' },
{ value: '평택', label: '평택' },
{ value: '의정부', label: '의정부' },
{ value: '시흥', label: '시흥' },
{ value: '파주', label: '파주' },
{ value: '김포', label: '김포' },
{ value: '광명', label: '광명' },
// 강원도
{ value: '춘천', label: '춘천' },
{ value: '원주', label: '원주' },
{ value: '강릉', label: '강릉' },
{ value: '속초', label: '속초' },
{ value: '동해', label: '동해' },
{ value: '태백', label: '태백' },
{ value: '삼척', label: '삼척' },
// 충청북도
{ value: '청주', label: '청주' },
{ value: '충주', label: '충주' },
{ value: '제천', label: '제천' },
// 충청남도
{ value: '천안', label: '천안' },
{ value: '공주', label: '공주' },
{ value: '보령', label: '보령' },
{ value: '아산', label: '아산' },
{ value: '서산', label: '서산' },
{ value: '논산', label: '논산' },
{ value: '당진', label: '당진' },
// 전라북도
{ value: '전주', label: '전주' },
{ value: '군산', label: '군산' },
{ value: '익산', label: '익산' },
{ value: '정읍', label: '정읍' },
{ value: '남원', label: '남원' },
{ value: '김제', label: '김제' },
// 전라남도
{ value: '목포', label: '목포' },
{ value: '여수', label: '여수' },
{ value: '순천', label: '순천' },
{ value: '나주', label: '나주' },
{ value: '광양', label: '광양' },
// 경상북도
{ value: '포항', label: '포항' },
{ value: '경주', label: '경주' },
{ value: '김천', label: '김천' },
{ value: '안동', label: '안동' },
{ value: '구미', label: '구미' },
{ value: '영주', label: '영주' },
{ value: '영천', label: '영천' },
{ value: '상주', label: '상주' },
{ value: '문경', label: '문경' },
{ value: '경산', label: '경산' },
{ value: '울릉도', label: '울릉도' },
// 경상남도
{ value: '창원', label: '창원' },
{ value: '진주', label: '진주' },
{ value: '통영', label: '통영' },
{ value: '사천', label: '사천' },
{ value: '김해', label: '김해' },
{ value: '밀양', label: '밀양' },
{ value: '거제', label: '거제' },
{ value: '양산', label: '양산' },
// 제주특별자치도
{ value: '제주', label: '제주' },
{ value: '서귀포', label: '서귀포' },
];
// 날씨 정보 가져오기
const fetchWeather = async () => {
try {
setLoading(true);
setError(null);
const data = await getWeather(selectedCity, 'metric', 'kr');
setWeather(data);
setLastUpdated(new Date());
} catch (err: any) {
console.error('날씨 조회 실패:', err);
// 에러 메시지 추출
let errorMessage = '날씨 정보를 가져오는 중 오류가 발생했습니다.';
if (err.response?.status === 503) {
errorMessage = 'API 키가 설정되지 않았습니다. 관리자에게 문의하세요.';
} else if (err.response?.status === 401) {
errorMessage = 'API 키가 유효하지 않습니다.';
} else if (err.response?.status === 404) {
errorMessage = `도시를 찾을 수 없습니다: ${city}`;
} else if (err.response?.data?.message) {
errorMessage = err.response.data.message;
}
setError(errorMessage);
} finally {
setLoading(false);
}
};
// 초기 로딩 및 자동 새로고침
useEffect(() => {
fetchWeather();
const interval = setInterval(fetchWeather, refreshInterval);
return () => clearInterval(interval);
}, [selectedCity, refreshInterval]);
// 도시 변경 핸들러
const handleCityChange = (newCity: string) => {
setSelectedCity(newCity);
};
// 날씨 아이콘 선택
const getWeatherIcon = (weatherMain: string) => {
switch (weatherMain.toLowerCase()) {
case 'clear':
return <Sun className="h-12 w-12 text-yellow-500" />;
case 'clouds':
return <Cloud className="h-12 w-12 text-gray-400" />;
case 'rain':
case 'drizzle':
return <CloudRain className="h-12 w-12 text-blue-500" />;
case 'snow':
return <CloudSnow className="h-12 w-12 text-blue-300" />;
default:
return <Cloud className="h-12 w-12 text-gray-400" />;
}
};
// 로딩 상태
if (loading && !weather) {
return (
<div className="flex h-full items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-6">
<div className="flex flex-col items-center gap-2">
<RefreshCw className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-sm text-gray-600"> ...</p>
</div>
</div>
);
}
// 에러 상태
if (error || !weather) {
return (
<div className="flex h-full flex-col items-center justify-center bg-gradient-to-br from-red-50 to-orange-50 rounded-lg border p-6">
<Cloud className="h-12 w-12 text-gray-400 mb-2" />
<p className="text-sm text-gray-600 text-center mb-3">{error || '날씨 정보를 불러올 수 없습니다.'}</p>
<Button
variant="outline"
size="sm"
onClick={fetchWeather}
className="gap-1"
>
<RefreshCw className="h-3 w-3" />
</Button>
</div>
);
}
return (
<div className="h-full bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-6">
{/* 헤더 */}
<div className="flex items-center justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
role="combobox"
aria-expanded={open}
className="justify-between text-lg font-semibold text-gray-900 hover:bg-white/50 h-auto py-1 px-2"
>
{cities.find((city) => city.value === selectedCity)?.label || '도시 선택'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder="도시 검색..." />
<CommandList>
<CommandEmpty> .</CommandEmpty>
<CommandGroup>
{cities.map((city) => (
<CommandItem
key={city.value}
value={city.value}
onSelect={(currentValue) => {
handleCityChange(currentValue === selectedCity ? selectedCity : currentValue);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedCity === city.value ? 'opacity-100' : 'opacity-0'
)}
/>
{city.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<p className="text-xs text-gray-500 pl-2">
{lastUpdated
? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit',
})}`
: ''}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={fetchWeather}
disabled={loading}
className="h-8 w-8 p-0"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* 날씨 아이콘 및 온도 */}
<div className="flex items-center justify-center mb-6">
<div className="flex items-center gap-4">
{getWeatherIcon(weather.weatherMain)}
<div>
<div className="text-5xl font-bold text-gray-900">
{weather.temperature}°C
</div>
<p className="text-sm text-gray-600 capitalize">
{weather.weatherDescription}
</p>
</div>
</div>
</div>
{/* 상세 정보 */}
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
<Wind className="h-5 w-5 text-blue-500" />
<div>
<p className="text-xs text-gray-500"> </p>
<p className="text-sm font-semibold text-gray-900">
{weather.feelsLike}°C
</p>
</div>
</div>
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
<Droplets className="h-5 w-5 text-blue-500" />
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-semibold text-gray-900">
{weather.humidity}%
</p>
</div>
</div>
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
<Wind className="h-5 w-5 text-blue-500" />
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-semibold text-gray-900">
{weather.windSpeed} m/s
</p>
</div>
</div>
<div className="flex items-center gap-2 bg-white/50 rounded-lg p-3">
<Gauge className="h-5 w-5 text-blue-500" />
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-semibold text-gray-900">
{weather.pressure} hPa
</p>
</div>
</div>
</div>
</div>
);
}

111
frontend/lib/api/openApi.ts Normal file
View File

@ -0,0 +1,111 @@
/**
* OpenAPI
* - API(, )
*/
import { apiClient } from './client';
// ============================================================
// 타입 정의
// ============================================================
/**
*
*/
export interface WeatherData {
city: string;
country: string;
temperature: number;
feelsLike: number;
humidity: number;
pressure: number;
weatherMain: string;
weatherDescription: string;
weatherIcon: string;
windSpeed: number;
clouds: number;
timestamp: string;
}
/**
*
*/
export interface ExchangeRateData {
base: string;
target: string;
rate: number;
timestamp: string;
}
/**
* Geocoding
*/
export interface GeocodeData {
address: string;
lat: number;
lng: number;
}
/**
* API
*/
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
// ============================================================
// API 함수
// ============================================================
/**
*
* @param city (기본값: Seoul)
* @param units (metric: 섭씨, imperial: 화씨)
* @param lang (kr: 한국어, en: 영어)
*/
export async function getWeather(
city: string = '서울',
units: string = 'metric',
lang: string = 'kr'
): Promise<WeatherData> {
const response = await apiClient.get<ApiResponse<WeatherData>>(
`/open-api/weather`,
{
params: { city, units, lang },
}
);
return response.data.data;
}
/**
*
* @param base (기본값: KRW)
* @param target (기본값: USD)
*/
export async function getExchangeRate(
base: string = 'KRW',
target: string = 'USD'
): Promise<ExchangeRateData> {
const response = await apiClient.get<ApiResponse<ExchangeRateData>>(
`/open-api/exchange-rate`,
{
params: { base, target },
}
);
return response.data.data;
}
/**
* (Geocoding)
* @param address
*/
export async function geocode(address: string): Promise<GeocodeData> {
const response = await apiClient.post<ApiResponse<GeocodeData>>(
`/open-api/geocode`,
{ address }
);
return response.data.data;
}