-
Notifications
You must be signed in to change notification settings - Fork 2
Feat/weather poc #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Feat/weather poc #39
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import axios from "axios"; | ||
| import { LOCATION_OBJECT } from "create/WeatherBox/const"; | ||
| import { getCorrectBaseTime } from "create/WeatherBox/helper"; | ||
| import { WeatherItem, WeatherResponse } from "create/WeatherBox/interfaces"; | ||
| import { isEmpty, isUndefined } from "lodash"; | ||
| import { NextApiRequest, NextApiResponse } from "next"; | ||
|
|
||
| export default async function handler( | ||
| req: NextApiRequest, | ||
| res: NextApiResponse, | ||
| ) { | ||
| const { date, locationCode } = req.query; | ||
| if (isEmpty(date) || isEmpty(locationCode)) return res.status(400).end(); | ||
| const { x, y } = LOCATION_OBJECT[locationCode as string]; | ||
|
|
||
| const params = { | ||
| serviceKey: process.env.NEXT_PUBLIC_WEATHER_API_KEY ?? "", | ||
| numOfRows: "846", | ||
| pageNo: "1", | ||
| nx: x, | ||
| ny: y, | ||
| dataType: "JSON", | ||
| ...getCorrectBaseTime(), | ||
| }; | ||
|
|
||
| const result = await axios.get( | ||
| "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst", | ||
| { params }, | ||
| ); | ||
|
|
||
| const items = result.data.response.body.items.item as WeatherItem[]; | ||
| const newWeatherObject = items.reduce((acc, cur) => { | ||
| const date = cur.fcstDate; | ||
| if (isUndefined(acc[date])) { | ||
| acc[date] = { | ||
| TMP: Array(24), | ||
| SKY: Array(24), | ||
| PTY: Array(24), | ||
| }; | ||
| } | ||
|
|
||
| switch (cur.category) { | ||
| case "TMP": | ||
| case "SKY": | ||
| case "PTY": | ||
| const time = Number(cur.fcstTime.slice(0, 2)); | ||
| acc[date][cur.category][time] = Number(cur.fcstValue); | ||
| break; | ||
| case "TMN": | ||
| case "TMX": | ||
| acc[date][cur.category] = Number(cur.fcstValue); | ||
| break; | ||
| } | ||
| return acc; | ||
| }, {} as WeatherResponse); | ||
|
|
||
| res.json(newWeatherObject); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * 하늘상태(SKY) 코드 : 맑음(1), 구름많음(3), 흐림(4) | ||
| * 강수형태(PTY) 코드 : 없음(0), 비(1), 비/눈(2), 눈(3), 소나기(4) | ||
| * | ||
| * 단기예보 제공 시간대는 0200, 0500, 0800, 1100, 1400, 1700, 2000, 2300 | ||
| * API 제공 시간(~이후) : 02:10, 05:10, 08:10, 11:10, 14:10, 17:10, 20:10, 23:10 | ||
| * | ||
| * POP 강수확률 | ||
| * PTY 강수형태 | ||
| * PCP 1시간 강수량 | ||
| * REH 습도 | ||
| * SNO 1시간 신적설 | ||
| * SKY 하늘상태 | ||
| * TMP 1시간 기온 | ||
| * TMN 일 최저기온 | ||
| * TMX 일 최고기온 | ||
| * UUU 풍속(동서성분) | ||
| * VVV 풍속(남북성분) | ||
| * WAV 파고 | ||
| * VEC 풍향 | ||
| * WSD 풍속 | ||
| */ | ||
|
NacreousCloud marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import WeatherBox from "create/WeatherBox"; | ||
| import { | ||
| getLoc1Arrays, | ||
| getLoc2Arrays, | ||
| getLocationCode, | ||
| } from "create/WeatherBox/helper"; | ||
| import { ChangeEvent, useEffect, useState } from "react"; | ||
| import SelectBox from "@shared/components/SelectBox"; | ||
|
|
||
| const WeatherTest = () => { | ||
| const [loc1Arrays] = useState(getLoc1Arrays()); | ||
| const [loc2Arrays, setLoc2Arrays] = useState<string[]>([]); | ||
|
|
||
| const [selectedLoc1, setSelectedLoc1] = useState(""); | ||
| const [selectedLoc2, setSelectedLoc2] = useState(""); | ||
|
|
||
| const [date, setDate] = useState(""); | ||
|
|
||
| useEffect(() => { | ||
| setLoc2Arrays(getLoc2Arrays(selectedLoc1)); | ||
| setSelectedLoc2(""); | ||
| }, [selectedLoc1]); | ||
|
|
||
| const handleChangeSelectBoxLoc1 = (e: ChangeEvent<HTMLSelectElement>) => { | ||
| setSelectedLoc1(e.target.value); | ||
| }; | ||
|
|
||
| const handleChangeSelectBoxLoc2 = (e: ChangeEvent<HTMLSelectElement>) => { | ||
| setSelectedLoc2(e.target.value); | ||
| }; | ||
|
|
||
| const handleChangeDate = (e: ChangeEvent<HTMLInputElement>) => { | ||
| setDate(e.target.value); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex items-center justify-center w-screen h-screen"> | ||
| <input type="date" value={date} onChange={handleChangeDate} /> | ||
| <SelectBox | ||
| options={loc1Arrays} | ||
| onChange={handleChangeSelectBoxLoc1} | ||
| defaultLabel="시 선택" | ||
| /> | ||
| <SelectBox | ||
| options={loc2Arrays} | ||
| onChange={handleChangeSelectBoxLoc2} | ||
| defaultLabel="군구 선택" | ||
| /> | ||
| <WeatherBox | ||
| date={date} | ||
| locationCode={getLocationCode(selectedLoc1, selectedLoc2)} | ||
| /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default WeatherTest; |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { BASE_TIMES, LOCATION_OBJECT } from "./const"; | ||
|
|
||
| /** | ||
| * @description loc1 배열을 반환 | ||
| */ | ||
| export const getLoc1Arrays = () => { | ||
| return Object.values(LOCATION_OBJECT).reduce<string[]>((prev, curr) => { | ||
| if (!prev.includes(curr.loc1)) return [...prev, curr.loc1]; | ||
| return prev; | ||
| }, []); | ||
| }; | ||
|
|
||
| /** | ||
| * @description loc1에 해당하는 loc2 배열을 반환 | ||
| */ | ||
| export const getLoc2Arrays = (loc1: string) => { | ||
| return Object.values(LOCATION_OBJECT).reduce<string[]>((prev, curr) => { | ||
| if (curr.loc1 === loc1 && !prev.includes(curr.loc2)) | ||
| return [...prev, curr.loc2]; | ||
| return prev; | ||
| }, []); | ||
| }; | ||
|
|
||
| /** | ||
| * @description loc1, loc2에 해당하는 locationCode를 반환 | ||
| */ | ||
| export const getLocationCode = (loc1: string, loc2: string) => { | ||
| if (loc1 === "" || loc2 === "") return ""; | ||
| const selectedLoc = Object.entries(LOCATION_OBJECT).find( | ||
| ([_, value]) => value.loc1 === loc1 && value.loc2 === loc2, | ||
| ); | ||
| return selectedLoc?.[0] ?? ""; | ||
| }; | ||
|
|
||
| /** | ||
| * @description Date 데이터를 YYYYMMDD 형식으로 반환 | ||
| */ | ||
| export const formatDate = (dateString?: string | number) => { | ||
| const date = new Date(dateString ?? Date.now()); | ||
| const year = date.getFullYear(); | ||
| const month = (date.getMonth() + 1).toString().padStart(2, "0"); | ||
| const day = date.getDate().toString().padStart(2, "0"); | ||
| return `${year}${month}${day}`; | ||
| }; | ||
|
|
||
| /** | ||
| * @description 현재 시간에 맞는 base_date, base_time을 반환 | ||
| */ | ||
| export const getCorrectBaseTime = () => { | ||
| const today = new Date(); | ||
| const currentHour = today.getHours(); | ||
| const currentMinute = today.getMinutes(); | ||
|
|
||
| if (currentHour < 2 || (currentHour === 2 && currentMinute < 10)) | ||
| return { | ||
| base_date: formatDate(today.setDate(today.getDate() - 1)), | ||
| base_time: "2300", | ||
| }; | ||
|
|
||
| for (let i = 1; i < BASE_TIMES.length; i++) { | ||
| const baseTime = BASE_TIMES[i - 1]; | ||
| if ( | ||
| currentHour < i * 3 + 2 || | ||
| (currentHour === i * 3 + 2 && currentMinute < 10) | ||
| ) { | ||
| return { | ||
| base_date: formatDate(), | ||
| base_time: baseTime, | ||
| }; | ||
| } | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import axios from "axios"; | ||
| import Image from "next/image"; | ||
| import { useEffect, useState } from "react"; | ||
| import { LOCATION_OBJECT } from "./const"; | ||
| import { WeatherBoxProps, WeatherObject, WeatherResponse } from "./interfaces"; | ||
| import Sunny from "/public/assets/weather/sunny.png"; | ||
|
|
||
| const WeatherBox = (props: WeatherBoxProps) => { | ||
| const locObj = LOCATION_OBJECT[props.locationCode]; | ||
| const [weatherObject, setWeatherObject] = useState<WeatherObject>(); | ||
|
|
||
| useEffect(() => { | ||
| axios | ||
| .get<WeatherResponse>("/api/weather", { params: props }) | ||
| .then((res) => { | ||
| const rawDate = props.date.replace(/-/g, ""); | ||
| if (res.data[rawDate]) { | ||
| console.log(res.data[rawDate]); | ||
| setWeatherObject(res.data[rawDate]); | ||
| } | ||
| return; | ||
| }) | ||
| .catch((err) => {}); | ||
| }, [props.locationCode]); | ||
|
|
||
| const getWeatherText = () => { | ||
| return ""; | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="bg-gradient-to-b from-c-pink to-[#FFCED1] rounded-[10px] w-[338px] h-[110px]"> | ||
| <div className="flex items-center justify-around h-full"> | ||
| <div> | ||
| <div className="text-base text-white font-extralight"> | ||
| {locObj?.loc1} | ||
| </div> | ||
| <div className="text-[28px] text-white font-medium"> | ||
| {locObj?.loc2} | ||
| </div> | ||
| <div className="text-sm text-white font-extralight"> | ||
| {getWeatherText()} | ||
| {/* TODO: 하늘상태, 강수상태에 따른 텍스트 변경 기능 추가 */} | ||
| </div> | ||
| </div> | ||
| <div className="flex items-center "> | ||
| {weatherObject?.TMX && weatherObject?.TMN ? ( | ||
| <> | ||
| <div> | ||
| <span className="text-4xl text-white font-extralight "> | ||
| {weatherObject?.TMN} / {weatherObject?.TMX} | ||
| </span> | ||
| <span className="text-xl text-white font-extralight">°C</span> | ||
| </div> | ||
| <div> | ||
| <Image src={Sunny} height={100} width={100} /> | ||
| {/* TODO: 하늘상태, 강수상태에 따른 아이콘 변경 기능 추가 */} | ||
| </div> | ||
| </> | ||
| ) : ( | ||
| <div className="text-sm text-white">예상 날씨를 알 수 없어요</div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default WeatherBox; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 코드 패치는 Next.js 프로젝트에서 사용되는 WeatherBox 컴포넌트입니다. 약간의 개선과 버그 위험을 제안해드리겠습니다:
이 외에도 코드 리뷰에는 다양한 관점이 있을 수 있으며, 기준은 프로젝트 요구 사항과 코딩 스타일 가이드에 따릅니다. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| export interface WeatherBoxProps { | ||
| date: string; | ||
| locationCode: string; | ||
| } | ||
| export interface WeatherItem { | ||
| baseDate: string; | ||
| baseTime: string; | ||
| category: WCategory; | ||
| fcstDate: string; | ||
| fcstTime: string; | ||
| fcstValue: string; | ||
| nx: number; | ||
| ny: number; | ||
| } | ||
| export interface LocationProps { | ||
| loc1: string; | ||
| loc2: string; | ||
| x: number; | ||
| y: number; | ||
| } | ||
| export type WCategory = | ||
| | "POP" | ||
| | "PTY" | ||
| | "PCP" | ||
| | "REH" | ||
| | "SNO" | ||
| | "SKY" | ||
| | "TMP" | ||
| | "TMN" | ||
| | "TMX" | ||
| | "UUU" | ||
| | "VVV" | ||
| | "WAV" | ||
| | "VEC" | ||
| | "WSD"; | ||
|
|
||
| export interface WeatherObject { | ||
| TMP: number[]; | ||
| SKY: number[]; | ||
| PTY: number[]; | ||
| TMN?: number; | ||
| TMX?: number; | ||
| } | ||
| export interface WeatherResponse { | ||
| [key: string]: WeatherObject; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 코드 패치를 요약적으로 검토하였습니다. 아래는 피드백입니다:
이상입니다! 위 코드 패치는 초기 검토에서 주요한 문제가 보이지 않았으며, 인터페이스 및 타입 정의도 일관성이 있어 보입니다. 그러나 전체 시스템 구조와 함께 사용되는 코드인지, 추가적인 설정 또는 요구사항이 있는지 등을 고려하여 보다 완벽한 검토를 수행하는 것이 좋습니다. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| interface SelectBoxProps extends React.HTMLAttributes<HTMLSelectElement> { | ||
| options: string[]; | ||
| defaultLabel?: string; | ||
| } | ||
| /** | ||
| * @param options | ||
| * @returns SelectBox | ||
| * @description 셀렉트 박스 | ||
| */ | ||
| const SelectBox = (props: SelectBoxProps) => { | ||
| return ( | ||
| <select | ||
| className={`h-14 w-[300px] px-5 py-4 border border-[#E0E0E0] rounded-[5px] ${props.className}`} | ||
| {...props} | ||
| > | ||
| <option>{props.defaultLabel ?? "선택"}</option> | ||
| {props.options.map((option, index) => ( | ||
| <option key={index} value={option}> | ||
| {option} | ||
| </option> | ||
| ))} | ||
| </select> | ||
| ); | ||
| }; | ||
|
|
||
| export default SelectBox; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 코드는 셀렉트 박스 컴포넌트를 정의하는 코드입니다. 이 코드의 주요 특징과 개선 사항을 아래에 설명하겠습니다:
이 코드는 안전하게 작동할 것으로 보이며, 개선 사항은 주로 코드 가독성과 유지 관리 측면에서 제안된 내용입니다. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,19 @@ module.exports = { | |
| ], | ||
| theme: { | ||
| extend: { | ||
| colors: { | ||
| "c-accept": "#00D179", // green. 확인용 색상 | ||
| "c-accept-disabled": "#00D17975", // green-disabled | ||
| "c-warn": "#FACD49", // yellow. 경고용 색상 | ||
| "c-warn-disabled": "#FACD4975", // yellow-disabled | ||
| "c-danger": "#FF2330", // red. 에러용 색상 | ||
| "c-danger-disabled": "#FFA4A475", // red-disabled | ||
| "c-pink": "#F864A1", | ||
| "c-orange": "#F5A200", | ||
| "c-purple": "#A564F8", | ||
| "c-gray": "#BABABA", | ||
| "c-black": "#222222", | ||
| }, | ||
| backgroundImage: { | ||
| stayLoggedIn: "url(/images/samples/StayLoggedIn.svg)", | ||
| idLabel: "url(/images/samples/IdLabel.svg)", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드 패치는 색상과 배경 이미지 확장을 위한 수정입니다. 주요 변경사항은 다음과 같습니다:
버그 리스크에 대해서는 주어진 코드만으로는 파악하기 어렵습니다. 그러나 새로운 색상과 배경 이미지를 추가하는 것은 일반적으로 크게 작용하지 않을 것으로 예상됩니다. 개발 환경과 사용 목적에 따라서 해당 코드의 테스트와 검증이 필요할 수 있습니다. 개선 제안:
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.