Merge branch 'main' into feature/screen-management
This commit is contained in:
68
backend-node/src/controllers/MaterialController.ts
Normal file
68
backend-node/src/controllers/MaterialController.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Request, Response } from "express";
|
||||
import MaterialService from "../services/MaterialService";
|
||||
|
||||
export class MaterialController {
|
||||
// 임시 자재 마스터 목록 조회
|
||||
async getTempMaterials(req: Request, res: Response) {
|
||||
try {
|
||||
const { search, category, page, limit } = req.query;
|
||||
|
||||
const result = await MaterialService.getTempMaterials({
|
||||
search: search as string,
|
||||
category: category as string,
|
||||
page: page ? parseInt(page as string) : 1,
|
||||
limit: limit ? parseInt(limit as string) : 20,
|
||||
});
|
||||
|
||||
return res.json({ success: true, ...result });
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching temp materials:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "자재 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 자재 상세 조회
|
||||
async getTempMaterialByCode(req: Request, res: Response) {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
const material = await MaterialService.getTempMaterialByCode(code);
|
||||
|
||||
if (!material) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "자재를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ success: true, data: material });
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching temp material:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "자재 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 카테고리 목록 조회
|
||||
async getCategories(req: Request, res: Response) {
|
||||
try {
|
||||
const categories = await MaterialService.getCategories();
|
||||
return res.json({ success: true, data: categories });
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MaterialController();
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { WarehouseService } from "../services/WarehouseService";
|
||||
|
||||
export class WarehouseController {
|
||||
private warehouseService: WarehouseService;
|
||||
|
||||
constructor() {
|
||||
this.warehouseService = new WarehouseService();
|
||||
}
|
||||
|
||||
// 창고 및 자재 데이터 조회
|
||||
getWarehouseData = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await this.warehouseService.getWarehouseData();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
warehouses: data.warehouses,
|
||||
materials: data.materials,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("창고 데이터 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "창고 데이터를 불러오는데 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 특정 창고 정보 조회
|
||||
getWarehouseById = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const warehouse = await this.warehouseService.getWarehouseById(id);
|
||||
|
||||
if (!warehouse) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "창고를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: warehouse,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("창고 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "창고 정보를 불러오는데 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 창고별 자재 목록 조회
|
||||
getMaterialsByWarehouse = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { warehouseId } = req.params;
|
||||
const materials =
|
||||
await this.warehouseService.getMaterialsByWarehouse(warehouseId);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: materials,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("자재 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "자재 목록을 불러오는데 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 창고 통계 조회
|
||||
getWarehouseStats = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const stats = await this.warehouseService.getWarehouseStats();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: stats,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("창고 통계 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "창고 통계를 불러오는데 실패했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
299
backend-node/src/controllers/YardLayoutController.ts
Normal file
299
backend-node/src/controllers/YardLayoutController.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { Request, Response } from "express";
|
||||
import YardLayoutService from "../services/YardLayoutService";
|
||||
|
||||
export class YardLayoutController {
|
||||
// 모든 야드 레이아웃 목록 조회
|
||||
async getAllLayouts(req: Request, res: Response) {
|
||||
try {
|
||||
const layouts = await YardLayoutService.getAllLayouts();
|
||||
res.json({ success: true, data: layouts });
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching yard layouts:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 야드 레이아웃 상세 조회
|
||||
async getLayoutById(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const layout = await YardLayoutService.getLayoutById(parseInt(id));
|
||||
|
||||
if (!layout) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ success: true, data: layout });
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching yard layout:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 새 야드 레이아웃 생성
|
||||
async createLayout(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "야드 이름은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const created_by = (req as any).user?.userId || "system";
|
||||
const layout = await YardLayoutService.createLayout({
|
||||
name,
|
||||
description,
|
||||
created_by,
|
||||
});
|
||||
|
||||
return res.status(201).json({ success: true, data: layout });
|
||||
} catch (error: any) {
|
||||
console.error("Error creating yard layout:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃 생성 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 야드 레이아웃 수정
|
||||
async updateLayout(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description } = req.body;
|
||||
|
||||
const layout = await YardLayoutService.updateLayout(parseInt(id), {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
|
||||
if (!layout) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ success: true, data: layout });
|
||||
} catch (error: any) {
|
||||
console.error("Error updating yard layout:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃 수정 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 야드 레이아웃 삭제
|
||||
async deleteLayout(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const layout = await YardLayoutService.deleteLayout(parseInt(id));
|
||||
|
||||
if (!layout) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "야드 레이아웃이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error deleting yard layout:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃 삭제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 야드의 모든 배치 자재 조회
|
||||
async getPlacementsByLayoutId(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const placements = await YardLayoutService.getPlacementsByLayoutId(
|
||||
parseInt(id)
|
||||
);
|
||||
|
||||
res.json({ success: true, data: placements });
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching placements:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "배치 자재 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 야드에 자재 배치 추가
|
||||
async addMaterialPlacement(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const placementData = req.body;
|
||||
|
||||
if (!placementData.external_material_id || !placementData.material_code) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "자재 정보가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const placement = await YardLayoutService.addMaterialPlacement(
|
||||
parseInt(id),
|
||||
placementData
|
||||
);
|
||||
|
||||
return res.status(201).json({ success: true, data: placement });
|
||||
} catch (error: any) {
|
||||
console.error("Error adding material placement:", error);
|
||||
|
||||
if (error.code === "23505") {
|
||||
// 유니크 제약 조건 위반
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 배치된 자재입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "자재 배치 추가 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 배치 정보 수정
|
||||
async updatePlacement(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const placementData = req.body;
|
||||
|
||||
const placement = await YardLayoutService.updatePlacement(
|
||||
parseInt(id),
|
||||
placementData
|
||||
);
|
||||
|
||||
if (!placement) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "배치 정보를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ success: true, data: placement });
|
||||
} catch (error: any) {
|
||||
console.error("Error updating placement:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "배치 정보 수정 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 배치 해제
|
||||
async removePlacement(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const placement = await YardLayoutService.removePlacement(parseInt(id));
|
||||
|
||||
if (!placement) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "배치 정보를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ success: true, message: "배치가 해제되었습니다." });
|
||||
} catch (error: any) {
|
||||
console.error("Error removing placement:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "배치 해제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 배치 일괄 업데이트
|
||||
async batchUpdatePlacements(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { placements } = req.body;
|
||||
|
||||
if (!Array.isArray(placements) || placements.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "배치 목록이 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const updatedPlacements = await YardLayoutService.batchUpdatePlacements(
|
||||
parseInt(id),
|
||||
placements
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: updatedPlacements });
|
||||
} catch (error: any) {
|
||||
console.error("Error batch updating placements:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "배치 일괄 업데이트 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 야드 레이아웃 복제
|
||||
async duplicateLayout(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "새 야드 이름은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const layout = await YardLayoutService.duplicateLayout(
|
||||
parseInt(id),
|
||||
name
|
||||
);
|
||||
|
||||
return res.status(201).json({ success: true, data: layout });
|
||||
} catch (error: any) {
|
||||
console.error("Error duplicating yard layout:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "야드 레이아웃 복제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new YardLayoutController();
|
||||
@@ -17,19 +17,54 @@ export class OpenApiProxyController {
|
||||
|
||||
console.log(`🌤️ 날씨 조회 요청: ${city}`);
|
||||
|
||||
// 기상청 API Hub 키 확인
|
||||
// 1순위: OpenWeatherMap API (실시간에 가까움, 10분마다 업데이트)
|
||||
const openWeatherKey = process.env.OPENWEATHER_API_KEY;
|
||||
if (openWeatherKey) {
|
||||
try {
|
||||
console.log(`🌍 OpenWeatherMap API 호출: ${city}`);
|
||||
const response = await axios.get('https://api.openweathermap.org/data/2.5/weather', {
|
||||
params: {
|
||||
q: `${city},KR`,
|
||||
appid: openWeatherKey,
|
||||
units: 'metric',
|
||||
lang: 'kr',
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
const weatherData = {
|
||||
city: data.name,
|
||||
country: data.sys.country,
|
||||
temperature: Math.round(data.main.temp),
|
||||
feelsLike: Math.round(data.main.feels_like),
|
||||
humidity: data.main.humidity,
|
||||
pressure: data.main.pressure,
|
||||
weatherMain: data.weather[0].main,
|
||||
weatherDescription: data.weather[0].description,
|
||||
weatherIcon: data.weather[0].icon,
|
||||
windSpeed: Math.round(data.wind.speed * 10) / 10,
|
||||
clouds: data.clouds.all,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log(`✅ OpenWeatherMap 날씨 조회 성공: ${weatherData.city} ${weatherData.temperature}°C`);
|
||||
res.json({ success: true, data: weatherData });
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ OpenWeatherMap API 실패, 기상청 API로 폴백:', error instanceof Error ? error.message : error);
|
||||
}
|
||||
}
|
||||
|
||||
// 2순위: 기상청 API Hub (매시간 정시 데이터)
|
||||
const apiKey = process.env.KMA_API_KEY;
|
||||
|
||||
// API 키가 없으면 테스트 모드로 실시간 날씨 제공
|
||||
// API 키가 없으면 오류 반환
|
||||
if (!apiKey) {
|
||||
console.log('⚠️ 기상청 API 키가 없습니다. 테스트 데이터를 반환합니다.');
|
||||
|
||||
const regionCode = getKMARegionCode(city as string);
|
||||
const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: weatherData,
|
||||
console.log('⚠️ 기상청 API 키가 설정되지 않았습니다.');
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: '기상청 API 키가 설정되지 않았습니다. 관리자에게 문의하세요.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -48,32 +83,39 @@ export class OpenApiProxyController {
|
||||
// 기상청 API Hub 사용 (apihub.kma.go.kr)
|
||||
const now = new Date();
|
||||
|
||||
// 기상청 데이터는 매시간 정시(XX:00)에 발표되고 약 10분 후 조회 가능
|
||||
// 현재 시각이 XX:10 이전이면 이전 시간 데이터 조회
|
||||
const minute = now.getMinutes();
|
||||
let targetTime = new Date(now);
|
||||
// 한국 시간(KST = UTC+9)으로 변환
|
||||
const kstOffset = 9 * 60 * 60 * 1000; // 9시간을 밀리초로
|
||||
const kstNow = new Date(now.getTime() + kstOffset);
|
||||
|
||||
if (minute < 10) {
|
||||
// 아직 이번 시간 데이터가 업데이트되지 않음 → 이전 시간으로
|
||||
targetTime = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
}
|
||||
// 기상청 지상관측 데이터는 매시간 정시(XX:00)에 발표
|
||||
// 가장 최근의 정시 데이터를 가져오기 위해 현재 시간의 정시로 설정
|
||||
const targetTime = new Date(kstNow);
|
||||
|
||||
// tm 파라미터: YYYYMMDDHH00 형식 (정시만 조회)
|
||||
const year = targetTime.getFullYear();
|
||||
const month = String(targetTime.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(targetTime.getDate()).padStart(2, '0');
|
||||
const hour = String(targetTime.getHours()).padStart(2, '0');
|
||||
const year = targetTime.getUTCFullYear();
|
||||
const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(targetTime.getUTCDate()).padStart(2, '0');
|
||||
const hour = String(targetTime.getUTCHours()).padStart(2, '0');
|
||||
const tm = `${year}${month}${day}${hour}00`;
|
||||
|
||||
console.log(`🕐 현재 시각(KST): ${kstNow.toISOString().slice(0, 16).replace('T', ' ')}, 조회 시각: ${tm}`);
|
||||
|
||||
// 기상청 API Hub - 지상관측시간자료
|
||||
const url = 'https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php';
|
||||
// 기상청 API Hub - 지상관측시간자료 (시간 범위 조회로 최신 데이터 확보)
|
||||
// sfctm3: 시간 범위 조회 가능 (tm1~tm2)
|
||||
const url = 'https://apihub.kma.go.kr/api/typ01/url/kma_sfctm3.php';
|
||||
|
||||
// 최근 1시간 범위 조회 (현재 시간 - 1시간 ~ 현재 시간) - KST 기준
|
||||
const tm1Time = new Date(kstNow.getTime() - 60 * 60 * 1000); // 1시간 전
|
||||
const tm1 = `${tm1Time.getUTCFullYear()}${String(tm1Time.getUTCMonth() + 1).padStart(2, '0')}${String(tm1Time.getUTCDate()).padStart(2, '0')}${String(tm1Time.getUTCHours()).padStart(2, '0')}00`;
|
||||
const tm2 = tm; // 현재 시간
|
||||
|
||||
console.log(`📡 기상청 API Hub 호출: ${regionCode.name} (관측소: ${regionCode.stnId}, 시간: ${tm})`);
|
||||
console.log(`📡 기상청 API Hub 호출: ${regionCode.name} (관측소: ${regionCode.stnId}, 기간: ${tm1}~${tm2})`);
|
||||
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
tm: tm,
|
||||
stn: 0, // 0 = 전체 관측소 데이터 조회
|
||||
tm1: tm1,
|
||||
tm2: tm2,
|
||||
stn: regionCode.stnId, // 특정 관측소만 조회
|
||||
authKey: apiKey,
|
||||
help: 0,
|
||||
disp: 1,
|
||||
@@ -95,30 +137,36 @@ export class OpenApiProxyController {
|
||||
} catch (error: unknown) {
|
||||
console.error('❌ 날씨 조회 실패:', error);
|
||||
|
||||
// API 호출 실패 시 자동으로 테스트 모드로 전환
|
||||
// API 호출 실패 시 명확한 오류 메시지 반환
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
|
||||
// 모든 오류 → 테스트 데이터 반환
|
||||
console.log('⚠️ API 오류 발생. 테스트 데이터를 반환합니다.');
|
||||
const { city = '서울' } = req.query;
|
||||
const regionCode = getKMARegionCode(city as string);
|
||||
const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: weatherData,
|
||||
});
|
||||
if (status === 401 || status === 403) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '기상청 API 인증에 실패했습니다. API 키를 확인하세요.',
|
||||
});
|
||||
} else if (status === 404) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '기상청 API에서 데이터를 찾을 수 없습니다.',
|
||||
});
|
||||
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||
res.status(504).json({
|
||||
success: false,
|
||||
message: '기상청 API 연결 시간이 초과되었습니다. 잠시 후 다시 시도하세요.',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '기상청 API 호출 중 오류가 발생했습니다.',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 예상치 못한 오류 → 테스트 데이터 반환
|
||||
console.log('⚠️ 예상치 못한 오류. 테스트 데이터를 반환합니다.');
|
||||
const { city = '서울' } = req.query;
|
||||
const regionCode = getKMARegionCode(city as string);
|
||||
const weatherData = generateRealisticWeatherData(regionCode?.name || (city as string));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: weatherData,
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '날씨 정보를 가져오는 중 예상치 못한 오류가 발생했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -169,15 +217,19 @@ export class OpenApiProxyController {
|
||||
} catch (error: unknown) {
|
||||
console.error('❌ 환율 조회 실패:', error);
|
||||
|
||||
// API 호출 실패 시 실제 근사값 반환
|
||||
console.log('⚠️ API 오류 발생. 근사값을 반환합니다.');
|
||||
const { base = 'KRW', target = 'USD' } = req.query;
|
||||
const approximateRate = generateRealisticExchangeRate(base as string, target as string);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: approximateRate,
|
||||
});
|
||||
// API 호출 실패 시 명확한 오류 메시지 반환
|
||||
if (axios.isAxiosError(error)) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '환율 정보를 가져오는 중 오류가 발생했습니다.',
|
||||
error: error.message,
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '환율 정보를 가져오는 중 예상치 못한 오류가 발생했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,19 +657,26 @@ function parseKMAHubWeatherData(data: any, regionCode: { name: string; stnId: st
|
||||
throw new Error('날씨 데이터를 파싱할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 요청한 관측소(stnId)의 데이터 찾기
|
||||
const targetLine = lines.find((line: string) => {
|
||||
// 요청한 관측소(stnId)의 모든 데이터 찾기 (시간 범위 조회 시 여러 줄 반환됨)
|
||||
const targetLines = lines.filter((line: string) => {
|
||||
const cols = line.trim().split(/\s+/);
|
||||
return cols[1] === regionCode.stnId; // STN 컬럼 (인덱스 1)
|
||||
});
|
||||
|
||||
if (!targetLine) {
|
||||
if (targetLines.length === 0) {
|
||||
throw new Error(`${regionCode.name} 관측소 데이터를 찾을 수 없습니다.`);
|
||||
}
|
||||
|
||||
// 가장 최근 데이터 선택 (마지막 줄)
|
||||
const targetLine = targetLines[targetLines.length - 1];
|
||||
|
||||
// 데이터 라인 파싱 (공백으로 구분)
|
||||
const values = targetLine.trim().split(/\s+/);
|
||||
|
||||
// 관측 시각 로깅
|
||||
const obsTime = values[0]; // YYMMDDHHMI
|
||||
console.log(`🕐 관측 시각: ${obsTime} (${regionCode.name})`);
|
||||
|
||||
// 기상청 API Hub 데이터 형식 (실제 응답 기준):
|
||||
// [0]YYMMDDHHMI [1]STN [2]WD [3]WS [4]GST_WD [5]GST_WS [6]GST_TM [7]PA [8]PS [9]PT [10]PR [11]TA [12]TD [13]HM [14]PV [15]RN ...
|
||||
const temperature = parseFloat(values[11]) || 0; // TA: 기온 (인덱스 11)
|
||||
|
||||
Reference in New Issue
Block a user