Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


* * *
Expand Down Expand Up @@ -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`๋ผ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํŒŒ์ผ์— ํด๋ผ์ด์–ธํŠธ ๋นŒ๋“œ์— ํ•„์š”ํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
Expand Down
File renamed without changes
File renamed without changes
File renamed without changes
Binary file added docs/imgs/screenshots/0.3.0/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/imgs/screenshots/0.3.0/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/imgs/screenshots/0.3.0/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions server/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ DB_PORT=5432
DB_USER=<์•„์ด๋””>
DB_PSWD=<ํŒจ์Šค์›Œ๋“œ>
DB_SCHEMA=<์Šคํ‚ค๋งˆ์ด๋ฆ„>
DATAGO_SERVICE_KEY=<๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ ์„œ๋น„์Šคํ‚ค>
REDIS_URL=
REDIS_HEALTH_CHECK_INTERVAL=<์ดˆ๋‹จ์œ„>
```

```shell
Expand Down
11 changes: 0 additions & 11 deletions server/api/src/trailine_api/externals/datago.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion server/api/src/trailine_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
def create_app() -> FastAPI:
container = Container()

# setup_logging(level="INFO")
setup_logging(level="INFO")

app = FastAPI(
title="trailine_api",
Expand Down
6 changes: 6 additions & 0 deletions server/api/src/trailine_api/schemas/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="๊ฐ•์ˆ˜ ํ™•๋ฅ  (%)")
Expand Down
8 changes: 7 additions & 1 deletion server/api/src/trailine_api/services/weather_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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์ผ ํ›„๋ถ€ํ„ฐ)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 1 addition & 4 deletions server/scripts/src/trailine_scripts/common/database.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -12,6 +12,3 @@ def get_db():
raise
finally:
db.close()


print(DATABASE_URL)
152 changes: 152 additions & 0 deletions web/src/components/client/WeatherForecast.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<circle cx="12" cy="12" r="4" fill="currentColor" />
<path strokeLinecap="round" d="M12 2v4M12 18v4M2 12h4M18 12h4M17.66 6.34l1.41-1.41M6.34 6.34l-1.41-1.41M6.34 17.66l-1.41 1.41M17.66 17.66l1.41 1.41" />
</svg>
);
case "cloudy":
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
</svg>
);
case "rain":
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-blue-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
<path d="M8 19v3m4-3v3m4-3v3" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
);
case "snow":
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-sky-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
<circle cx="8" cy="21" r="1" /><circle cx="12" cy="21" r="1" /><circle cx="16" cy="21" r="1" />
</svg>
);
}
};

const SKY_CONDITION_LABELS: Record<SkyCondition, string> = { // ํ•œ๊ธ€ ๋ ˆ์ด๋ธ”, 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<Props> = ({ courseId }: Props) => {
const [forecasts, setForecasts] = useState<Forecast[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<boolean>(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 (
<div className="my-6">
<p className="text-sm font-bold text-base-content/60 mb-3">์ฃผ๊ฐ„ ๋‚ ์”จ ์˜ˆ๋ณด</p>
<div className="flex gap-2 overflow-x-auto">
{Array.from({ length: FORECAST_DAYS }).map((_, i) => (
<div key={i} className="skeleton h-36 w-20 shrink-0 rounded-lg" />
))}
</div>
</div>
);
}

if (error || forecasts.length === 0) return null;

return (
<div className="my-6">
<p className="text-sm font-bold text-base-content/60 mb-3">์ฃผ๊ฐ„ ๋‚ ์”จ ์˜ˆ๋ณด</p>
<div className="flex gap-2 overflow-x-auto pb-2">
{forecasts.map((forecast) => {
const weekend = isWeekend(forecast.dayOfWeekKo);
return (
<div
key={forecast.date}
className={`flex flex-col items-center gap-1.5 px-4 py-3 rounded-lg shrink-0 flex-1 min-w-[80px] border ${
weekend
? "bg-red-50 border-red-200"
: "bg-base-100 border-base-content/10"
}`}
>
{/* ์š”์ผ */}
<span className={`text-sm font-bold ${weekend ? "text-red-500" : ""}`}>
{forecast.dayOfWeekKo}
</span>

{/* ๋‚ ์งœ */}
<span className="text-xs text-base-content/50">
{formatDate(forecast.date)}
</span>

{/* ๋‚ ์”จ ์•„์ด์ฝ˜ */}
<div className="my-1" title={SKY_CONDITION_LABELS[forecast.skyCondition]}>
<SkyConditionIcon condition={forecast.skyCondition} />
</div>

{/* ์ตœ์ €/์ตœ๊ณ  ์˜จ๋„ */}
<div className="text-xs font-semibold">
<span className="text-blue-500">{forecast.minTemperature}ยฐ</span>
{" / "}
<span className="text-red-500">{forecast.maxTemperature}ยฐ</span>
</div>

{/* ๊ฐ•์ˆ˜ํ™•๋ฅ  */}
{forecast.precipitationProbability > 0 ? (
<span className={`text-xs ${
forecast.precipitationProbability >= 40
? "text-blue-500 font-bold"
: "text-base-content/40"
}`}>
๊ฐ•์ˆ˜ํ™•๋ฅ  {forecast.precipitationProbability}%
</span>
) : (
<span className="text-xs text-base-content/20">-</span>
)}
</div>
);
})}
</div>
</div>
);
};

export default WeatherForecast;
4 changes: 4 additions & 0 deletions web/src/pages/courses/[id].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,6 +97,9 @@ const difficultyBackgroundColor = course ? COURSE_DIFFICULTY_COLORS[course.diffi
</div>
</div>

<!-- ์ฃผ๊ฐ„ ๋‚ ์”จ ์˜ˆ๋ณด -->
<WeatherForecast client:load courseId={course?.id} />

<!-- ์ฝ”์Šค ์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋” -->
{course?.images && course.images.length > 0 && (
<ImageSlider client:load images={course.images} className="w-full mx-auto" />
Expand Down
16 changes: 16 additions & 0 deletions web/src/types/responses/weather-forecast.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
1 change: 1 addition & 0 deletions web/src/vars/weather.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const FORECAST_DAYS = 7;
Loading