diff --git a/README.md b/README.md index 5baf287..0eec919 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ ## Demo Screenshots -![](docs/imgs/readme-1.png) -![](docs/imgs/readme-2.png) -![](docs/imgs/readme-3.png) +![](docs/imgs/screenshots/0.3.0/1.png) +![](docs/imgs/screenshots/0.3.0/2.png) +![](docs/imgs/screenshots/0.3.0/3.png) * * * @@ -52,6 +52,10 @@ DB_PORT= DB_USER= DB_PSWD= DB_SCHEMA= +DATAGO_SERVICE_KEY=<공공데이터포털 서비스키> +REDIS_URL= +REDIS_HEALTH_CHECK_INTERVAL=<초단위> + ``` 2. web 디렉토리에 들어가서 `.local.env`라는 환경변수 파일에 클라이언트 빌드에 필요한 환경변수를 입력합니다. diff --git a/docs/imgs/readme-1.png b/docs/imgs/screenshots/0.2.0/readme-1.png similarity index 100% rename from docs/imgs/readme-1.png rename to docs/imgs/screenshots/0.2.0/readme-1.png diff --git a/docs/imgs/readme-2.png b/docs/imgs/screenshots/0.2.0/readme-2.png similarity index 100% rename from docs/imgs/readme-2.png rename to docs/imgs/screenshots/0.2.0/readme-2.png diff --git a/docs/imgs/readme-3.png b/docs/imgs/screenshots/0.2.0/readme-3.png similarity index 100% rename from docs/imgs/readme-3.png rename to docs/imgs/screenshots/0.2.0/readme-3.png diff --git a/docs/imgs/screenshots/0.3.0/1.png b/docs/imgs/screenshots/0.3.0/1.png new file mode 100644 index 0000000..52e8c18 Binary files /dev/null and b/docs/imgs/screenshots/0.3.0/1.png differ diff --git a/docs/imgs/screenshots/0.3.0/2.png b/docs/imgs/screenshots/0.3.0/2.png new file mode 100644 index 0000000..8aa4f19 Binary files /dev/null and b/docs/imgs/screenshots/0.3.0/2.png differ diff --git a/docs/imgs/screenshots/0.3.0/3.png b/docs/imgs/screenshots/0.3.0/3.png new file mode 100644 index 0000000..94ec896 Binary files /dev/null and b/docs/imgs/screenshots/0.3.0/3.png differ diff --git a/server/api/README.md b/server/api/README.md index db19814..979f119 100644 --- a/server/api/README.md +++ b/server/api/README.md @@ -14,6 +14,9 @@ DB_PORT=5432 DB_USER=<아이디> DB_PSWD=<패스워드> DB_SCHEMA=<스키마이름> +DATAGO_SERVICE_KEY=<공공데이터포털 서비스키> +REDIS_URL= +REDIS_HEALTH_CHECK_INTERVAL=<초단위> ``` ```shell diff --git a/server/api/src/trailine_api/externals/datago.py b/server/api/src/trailine_api/externals/datago.py index a41cc0c..17c9346 100644 --- a/server/api/src/trailine_api/externals/datago.py +++ b/server/api/src/trailine_api/externals/datago.py @@ -193,17 +193,6 @@ def call(self, nx: int, ny: int, days: int) -> List[ShortForecastItem]: ): break - print({ - "serviceKey": self.service_key, - "dataType": "JSON", - "numOfRows": 500, - "pageNo": page, - "base_date": base_date, - "base_time": base_time, - "nx": nx, - "ny": ny, - }) - # API 호출 response = httpx.get(self.url, params={ "serviceKey": self.service_key, diff --git a/server/api/src/trailine_api/main.py b/server/api/src/trailine_api/main.py index a3131f1..41a6f21 100644 --- a/server/api/src/trailine_api/main.py +++ b/server/api/src/trailine_api/main.py @@ -11,7 +11,7 @@ def create_app() -> FastAPI: container = Container() - # setup_logging(level="INFO") + setup_logging(level="INFO") app = FastAPI( title="trailine_api", diff --git a/server/api/src/trailine_api/schemas/weather.py b/server/api/src/trailine_api/schemas/weather.py index 78cb4c4..169a4b4 100644 --- a/server/api/src/trailine_api/schemas/weather.py +++ b/server/api/src/trailine_api/schemas/weather.py @@ -11,8 +11,14 @@ ) +DAY_OF_WEEK_LIST = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"] +DAY_OF_WEEK_KO_LIST = ["월", "화", "수", "목", "금", "토", "일"] + + class WeatherForecastItemSchema(BaseModel): date: str = Field(..., description="예보 날짜 (YYYY-MM-DD)") + day_of_week: str = Field(..., alias="dayOfWeek", description="요일 (영문)") + day_of_week_ko: str = Field(..., alias="dayOfWeekKo", description="요일 (한국어)") min_temperature: float = Field(..., alias="minTemperature", description="최저 기온 (°C)") max_temperature: float = Field(..., alias="maxTemperature", description="최고 기온 (°C)") precipitation_probability: int = Field(..., alias="precipitationProbability", description="강수 확률 (%)") diff --git a/server/api/src/trailine_api/services/weather_services.py b/server/api/src/trailine_api/services/weather_services.py index 4b5faaa..e4ec785 100644 --- a/server/api/src/trailine_api/services/weather_services.py +++ b/server/api/src/trailine_api/services/weather_services.py @@ -13,7 +13,7 @@ from trailine_api.externals.datago import IKmaMidLandForecastAPI, IKmaMidLandTemperatureAPI, IKmaShortForecastAPI from trailine_api.repositories.course_repositories import ICourseRepository from trailine_api.repositories.weather_repositories import IWeatherRepository -from trailine_api.schemas.weather import ShortForecastItem, WeatherForecastItemSchema +from trailine_api.schemas.weather import DAY_OF_WEEK_KO_LIST, DAY_OF_WEEK_LIST, ShortForecastItem, WeatherForecastItemSchema MID_FORECAST_MIN_DAY = 5 # 기상청 중기예보 시작일 (5일 후부터) @@ -132,10 +132,13 @@ def _build_short_forecasts(self, nx: int, ny: int, days: int) -> List[WeatherFor results: List[WeatherForecastItemSchema] = [] for date_key in sorted(daily): items = daily[date_key] + weekday = datetime.strptime(date_key, DATE_FORMAT).weekday() results.append( WeatherForecastItemSchema( date=date_key, + dayOfWeek=DAY_OF_WEEK_LIST[weekday], + dayOfWeekKo=DAY_OF_WEEK_KO_LIST[weekday], minTemperature=min(item.temperature for item in items), maxTemperature=max(item.temperature for item in items), precipitationProbability=max(item.rain_probability for item in items), @@ -173,9 +176,12 @@ def _build_mid_forecasts( raw_i = i - MID_FORECAST_MIN_DAY forecast_date = today + timedelta(days=i) + weekday = forecast_date.weekday() results.append( WeatherForecastItemSchema( date=forecast_date.strftime(DATE_FORMAT), + dayOfWeek=DAY_OF_WEEK_LIST[weekday], + dayOfWeekKo=DAY_OF_WEEK_KO_LIST[weekday], minTemperature=mid_temperatures[raw_i].min_temperature, maxTemperature=mid_temperatures[raw_i].max_temperature, precipitationProbability=max( diff --git a/server/scripts/src/trailine_scripts/common/database.py b/server/scripts/src/trailine_scripts/common/database.py index 16f2d9d..d839e0a 100644 --- a/server/scripts/src/trailine_scripts/common/database.py +++ b/server/scripts/src/trailine_scripts/common/database.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from trailine_model.base import SessionLocal, DATABASE_URL +from trailine_model.base import SessionLocal @contextmanager def get_db(): @@ -12,6 +12,3 @@ def get_db(): raise finally: db.close() - - -print(DATABASE_URL) \ No newline at end of file diff --git a/web/src/components/client/WeatherForecast.tsx b/web/src/components/client/WeatherForecast.tsx new file mode 100644 index 0000000..737dc25 --- /dev/null +++ b/web/src/components/client/WeatherForecast.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from "react"; +import type { Forecast, SkyCondition, WeatherForecastResponse } from "@/types/responses/weather-forecast"; +import { FORECAST_DAYS } from "@/vars/weather"; + +interface Props { + courseId: number | undefined; +} + +const SkyConditionIcon: React.FC<{ condition: SkyCondition }> = ({ condition }) => { + switch (condition) { + case "clear": + return ( + + + + + ); + case "cloudy": + return ( + + + + ); + case "rain": + return ( + + + + + ); + case "snow": + return ( + + + + + ); + } +}; + +const SKY_CONDITION_LABELS: Record = { // 한글 레이블, Record는 타입스크립트의 유틸리티 타입으로, 키와 값의 타입을 지정할 때 사용 + clear: "맑음", + cloudy: "흐림", + rain: "비", + snow: "눈", +}; + +const isWeekend = (dayOfWeekKo: string): boolean => { + return dayOfWeekKo === "토" || dayOfWeekKo === "일"; +}; + +const formatDate = (dateStr: string): string => { + const [, month, day] = dateStr.split("-"); + return `${parseInt(month)}/${parseInt(day)}`; +}; + +const WeatherForecast: React.FC = ({ courseId }: Props) => { + const [forecasts, setForecasts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + if (courseId === undefined) return; + + setLoading(true); + fetch(`${import.meta.env.PUBLIC_API_ENDPOINT}/v1/courses/${courseId}/weather/forecast?days=${FORECAST_DAYS}`) + .then((res) => { + if (!res.ok) throw new Error("Failed to fetch forecast"); + return res.json(); + }) + .then((data: WeatherForecastResponse) => { + setForecasts(data.forecasts); + }) + .catch(() => { + setError(true); + }) + .finally(() => setLoading(false)); + }, [courseId]); + + if (loading) { + return ( +
+

주간 날씨 예보

+
+ {Array.from({ length: FORECAST_DAYS }).map((_, i) => ( +
+ ))} +
+
+ ); + } + + if (error || forecasts.length === 0) return null; + + return ( +
+

주간 날씨 예보

+
+ {forecasts.map((forecast) => { + const weekend = isWeekend(forecast.dayOfWeekKo); + return ( +
+ {/* 요일 */} + + {forecast.dayOfWeekKo} + + + {/* 날짜 */} + + {formatDate(forecast.date)} + + + {/* 날씨 아이콘 */} +
+ +
+ + {/* 최저/최고 온도 */} +
+ {forecast.minTemperature}° + {" / "} + {forecast.maxTemperature}° +
+ + {/* 강수확률 */} + {forecast.precipitationProbability > 0 ? ( + = 40 + ? "text-blue-500 font-bold" + : "text-base-content/40" + }`}> + 강수확률 {forecast.precipitationProbability}% + + ) : ( + - + )} +
+ ); + })} +
+
+ ); +}; + +export default WeatherForecast; diff --git a/web/src/pages/courses/[id].astro b/web/src/pages/courses/[id].astro index 7c5e3e0..9fa1a47 100644 --- a/web/src/pages/courses/[id].astro +++ b/web/src/pages/courses/[id].astro @@ -8,6 +8,7 @@ import type { CourseDetailSchemaResponse } from "@/types/responses/course-detail import CourseIntervalDetail from "@/components/client/CourseIntervalDetail"; import { COURSE_DIFFICULTY_COLORS } from "@/vars/colors"; import { minutesToKoreanDuration } from "@/lib/string-mapper"; +import WeatherForecast from "@/components/client/WeatherForecast"; const { id } = Astro.params; @@ -96,6 +97,9 @@ const difficultyBackgroundColor = course ? COURSE_DIFFICULTY_COLORS[course.diffi
+ + + {course?.images && course.images.length > 0 && ( diff --git a/web/src/types/responses/weather-forecast.ts b/web/src/types/responses/weather-forecast.ts new file mode 100644 index 0000000..3fb5df9 --- /dev/null +++ b/web/src/types/responses/weather-forecast.ts @@ -0,0 +1,16 @@ +export type SkyCondition = "clear" | "cloudy" | "rain" | "snow"; + +export interface Forecast { + date: string; + dayOfWeek: string; + dayOfWeekKo: string; + minTemperature: number; + maxTemperature: number; + precipitationProbability: number; + skyCondition: SkyCondition; +} + +export interface WeatherForecastResponse { + courseId: number; + forecasts: Forecast[]; +} diff --git a/web/src/vars/weather.ts b/web/src/vars/weather.ts new file mode 100644 index 0000000..07aa3e4 --- /dev/null +++ b/web/src/vars/weather.ts @@ -0,0 +1 @@ +export const FORECAST_DAYS = 7;