71. Google Map API Settings
๊ตฌ๊ธ๋งต ์ค์
์์ฑ์ผ
5/3/2025์์ฑ
๊น๋ฒ์ค์์ ์ผ
5/3/2025๊ตฌ๊ธ๋งต
์ฃผ์๊ธฐ๋ฅ
- ๊ตฌ๊ธ๋งต ๊ธฐ๋ณธ ์ปดํฌ๋ํธ
- ๊ณต์ธ ์์ดํผ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์น ์ ๋ณด ๋ฐ ๊ธฐํ ์ฃผ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
- ์ฃผ์์ ๋ ฅ์ ๋ฐํ์ผ๋ก ์ง๋ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
- ๋ํ๋ฏผ๊ตญ ์ฃผ์ ์๊ตฐ๊ตฌ๋ฅผ ํตํ์ฌ ์ง๋ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
- ์ง๋ ๋ง์ปค (์๋ก์ด ๋ฒ์ )
์ฝ๋
// -- google-map -- //
'use client'
import VivGoogleMap from "@/components/VivGoogleMap";
import VivTitle from "@/components/VivTitle";
import GpsFixedOutlinedIcon from '@mui/icons-material/GpsFixedOutlined';
import LocationDisabledOutlinedIcon from '@mui/icons-material/LocationDisabledOutlined';
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
import { getIpInfomations } from '@/lib/fetchIpInfo';
import { IIpInfo } from '@/interfaces/i-ip-info';
import { IPosData } from '@/interfaces/i-pos-data';
import FormSection from '../ip-address/FormSection'; // FormSection ๊ฒฝ๋ก๋ ์ค์ ํ๋ก์ ํธ์ ๋ง๊ฒ ํ์ธ ํ์
import { ILocationInfo } from '@/interfaces/i-location-info';
import { useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { ButtonGroup, FilledInput, FormControl, FormHelperText, IconButton, InputLabel, MenuItem, Select, SelectChangeEvent, Tooltip } from "@mui/material";
interface AddressInfo {
lat: number;
lng: number;
jibunAddress?: string;
roadAddress?: string;
}
const GoogleMapPage = (
) => {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const [initialIpInfo, setInitialIpInfo] = useState<IIpInfo | undefined>();
const [posData, setPosData] = useState<IPosData | undefined>(); // ์ง๋์ ์ค์ฌ ์ขํ ์ํ (undefined ๊ฐ๋ฅ)
const [locationInfos, setLocationInfos] = useState<ILocationInfo[]>([]); // ์ด๊ธฐ๊ฐ์ ๋น ๋ฐฐ์ด๋ก ์ค์
const [locationId, setLocationId] = useState<string>(''); // Select ์ปดํฌ๋ํธ value๋ string์ ๊ธฐ๋ํ๋ฏ๋ก string์ผ๋ก ๊ด๋ฆฌ
const [isLoadingIp, setIsLoadingIp] = useState(true); // ์ด๊ธฐ IP ๋ก๋ฉ ์ํ
const [isLoadingLocations, setIsLoadingLocations] = useState(true);
const {
control,
handleSubmit,
formState: { errors },
reset,
setValue,
} = useForm<IPosData>({
defaultValues: {
latitude: '',
longitude: ''
},
mode: 'onTouched'
});
// ? IP ์ ๋ณด(IIpInfo)๋ฅผ ๋ฐ์์ posData ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ ํจ์
// ? ์ด ํจ์๋ ์ด์ setPosData๋ง ํธ์ถํจ. ํผ ์
๋ฐ์ดํธ๋ ๋ณ๋ useEffect์์ ์ฒ๋ฆฌ.
const updatePosDataFromIpInfo = useCallback((ipInfo: IIpInfo | null) => {
let newPosData: IPosData | undefined = undefined;
if (ipInfo?.location) {
const locationParts = ipInfo.location.split(',');
if (locationParts.length === 2) {
const lat = locationParts[0].trim();
const lng = locationParts[1].trim();
if (lat && lng) {
newPosData = { latitude: lat, longitude: lng };
} else {
console.warn('โ ๏ธ Parsed latitude or longitude is empty after splitting.');
}
} else {
console.warn(`โ ๏ธ Invalid location format received: ${ipInfo.location}`);
}
} else {
console.log('โน๏ธ No location data found in the received IP Info.');
}
setPosData(newPosData); // posData ์ํ ์
๋ฐ์ดํธ (undefined์ผ ์๋ ์์)
}, []); // setPosData๋ ์์ ์ ์ด๋ฏ๋ก ์์กด์ฑ ๋ฐฐ์ด์ ๋ฃ์ง ์์๋ ๋จ (๋๋ [setPosData] ์ถ๊ฐ)
// ? ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์ด๊ธฐ IP ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
useEffect(() => {
const getInitialIp = async () => {
setIsLoadingIp(true);
try {
const ip = await getIpInfomations();
setInitialIpInfo(ip);
updatePosDataFromIpInfo(ip); // ๊ฐ์ ธ์จ IP ์ ๋ณด๋ก posData ์ค์
} catch (error) {
console.error("Error fetching initial IP info:", error);
// ์ด๊ธฐ IP ๋ก๋ฉ ์คํจ ์ posData๋ฅผ ์ค์ ํ์ง ์๊ฑฐ๋ ๊ธฐ๋ณธ๊ฐ ์ค์ ๊ฐ๋ฅ
setPosData(undefined);
} finally {
setIsLoadingIp(false);
}
}
getInitialIp();
}, [updatePosDataFromIpInfo]); // updatePosDataFromIpInfo๋ useCallback์ผ๋ก ๊ฐ์ธ์ ธ ์์ ์
// ? ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์ฅ์ ๋ชฉ๋ก(LocationInfo) ๊ฐ์ ธ์ค๊ธฐ
useEffect(() => {
const fetchData = async () => {
setIsLoadingLocations(true);
const url = `${apiUrl}/api/locationinfo`;
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error(`๋ฐ์ดํฐ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ ์คํจ: ${response.status}`);
}
const result: ILocationInfo[] = await response.json();
setLocationInfos(result || []); // ๊ฒฐ๊ณผ๊ฐ null/undefined์ผ ๊ฒฝ์ฐ ๋น ๋ฐฐ์ด
} catch (e: any) {
console.error("Error fetching location infos:", e);
setLocationInfos([]); // ์๋ฌ ๋ฐ์ ์ ๋น ๋ฐฐ์ด๋ก ์ค์
} finally {
setIsLoadingLocations(false);
}
}
fetchData();
}, [apiUrl]); // apiUrl์ด ๋ณ๊ฒฝ๋ ๊ฐ๋ฅ์ฑ์ด ๋ฎ๋ค๋ฉด []๋ก ํด๋ ๋ฌด๋ฐฉ
// ? posData ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ํผ ํ๋ ์
๋ฐ์ดํธ
useEffect(() => {
if (posData && posData.latitude && posData.longitude) {
setValue('latitude', posData.latitude, { shouldValidate: true, shouldDirty: true });
setValue('longitude', posData.longitude, { shouldValidate: true, shouldDirty: true });
// posData์ ํด๋นํ๋ locationId๋ฅผ ์ฐพ์์ Select ๊ฐ๋ ์
๋ฐ์ดํธ (์ ํ ์ฌํญ)
const foundLocation = locationInfos.find(loc =>
loc.latitude.toString() === posData.latitude?.toString() &&
loc.longitude.toString() === posData.longitude?.toString()
);
setLocationId(foundLocation ? foundLocation.id.toString() : '');
} else {
setValue('latitude', '', { shouldDirty: true });
setValue('longitude', '', { shouldDirty: true });
setLocationId(''); // Select ๊ฐ๋ ์ด๊ธฐํ
}
}, [posData, setValue, locationInfos]); // posData๋ locationInfos๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์คํ
// ์ฌ๋ฌด์ค ์์น ์ค์ ํธ๋ค๋ฌ
const onSetOffice = () => {
const office: IPosData = {
latitude: 37.7110454443427,
longitude: 127.116683580175
};
setPosData(office); // posData๋ง ์
๋ฐ์ดํธ -> useEffect๊ฐ ํผ ํ๋ ์
๋ฐ์ดํธ
};
// ํผ ์ ์ถ ํธ๋ค๋ฌ
const onSubmit = (data: IPosData) => {
// ์ ํจํ ์ซ์ ํํ์ธ์ง ์ถ๊ฐ ๊ฒ์ฆ ๊ฐ๋ฅ
if (data.latitude && data.longitude) {
setPosData(data); // posData ์
๋ฐ์ดํธ -> useEffect๊ฐ ํผ ํ๋ ์
๋ฐ์ดํธ (ํ์์)
} else {
console.warn("onSubmit: Invalid latitude or longitude in form data", data);
}
};
// ์ฅ์ ์ ํ ๋ณ๊ฒฝ ํธ๋ค๋ฌ
const handleChange = (event: SelectChangeEvent<string>) => { // Select value ํ์
string์ผ๋ก ๋ณ๊ฒฝ
const id = event.target.value;
setLocationId(id); // ์ ํ๋ ID ์
๋ฐ์ดํธ (string)
if (id) {
const location = locationInfos?.find((x) => x.id === Number(id));
if (location) {
const pos: IPosData = {
latitude: location.latitude,
longitude: location.longitude
};
setPosData(pos); // posData ์
๋ฐ์ดํธ -> useEffect๊ฐ ํผ ํ๋ ์
๋ฐ์ดํธ
}
} else {
// '์ ํ ์ํจ' ๋๋ ๋น ๊ฐ ์ ํ ์ ์ฒ๋ฆฌ
setPosData(undefined); // ์ง๋ ์์น ์ด๊ธฐํ
}
};
// ํผ ๋ฐ ์ง๋ ์ด๊ธฐํ ํธ๋ค๋ฌ
const handleReset = () => {
reset({ latitude: '', longitude: '' }); // ํผ ํ๋ ์ด๊ธฐํ
setPosData(undefined); // ์ง๋ ์์น ์ํ ์ด๊ธฐํ
setLocationId(''); // Select ๊ฐ ์ด๊ธฐํ
};
return (
<div className="w-full min-h-screen flex flex-col items-center">
<VivTitle title="๋ด ์ง๋" />
<div className="w-full">
<VivGoogleMap params={posData!} locations={locationInfos} />
</div>
{/* ์์ดํผ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ ๋ฐ ์
๋ ฅ ์น์
*/}
{/* ๋ก๋ฉ ์ํ ๋๋ ์ด๊ธฐ IP ์ ๋ณด์ ๋ฐ๋ผ ํ์ */}
{isLoadingIp && <div>Loading initial IP information...</div>}
{!isLoadingIp && initialIpInfo && (
<FormSection
initialIpInfo={initialIpInfo}
onIpInfoFetched={updatePosDataFromIpInfo} // IP ๊ฒ์ ์ ํธ์ถ๋ ์ฝ๋ฐฑ (posData ์
๋ฐ์ดํธ์ฉ)
/>
)}
{!isLoadingIp && !initialIpInfo && <div>Could not load initial IP information.</div>}
{/* ์๋/๊ฒฝ๋ ์
๋ ฅ ํผ */}
<form autoComplete='off'
className='flex flex-col gap-8 items-center p-4 w-[400px]' // ๋ฐ์ํ ๋๋น ์กฐ์
onSubmit={handleSubmit(onSubmit)} >
<div className='flex flex-col sm:flex-row gap-4 w-full justify-center'> {/* ๋ฐ์ํ ๋ ์ด์์ */}
{/* ์๋, Latitude */}
<FormControl className='w-full sm:w-[250px]'> {/* ๋ฐ์ํ ๋๋น */}
<InputLabel htmlFor="latitude">์๋ (Latitude)</InputLabel>
<Controller
name='latitude'
control={control}
rules={{ required: '์๋๋ฅผ ์
๋ ฅํ์ฌ ์ฃผ์ธ์' }}
render={({ field }) => (
<FilledInput
{...field}
error={!!errors.latitude}
autoComplete='off'
id='latitude'
name='latitude'
type="number" // ์ซ์ ์
๋ ฅ์ ๋ ์ ํฉํ ์ ์์
inputProps={{ step: "any" }} // ์์์ ํ์ฉ
/>
)}
/>
{errors.latitude &&
<FormHelperText className='!text-red-400'>
{errors.latitude?.message}
</FormHelperText>}
</FormControl>
{/* ๊ฒฝ๋, Longitude */}
<FormControl className='w-full sm:w-[250px]'> {/* ๋ฐ์ํ ๋๋น */}
<InputLabel htmlFor="longitude">๊ฒฝ๋ (Longitude)</InputLabel> {/* ์คํ ์์ : logitude -> longitude */}
<Controller
name='longitude'
control={control}
rules={{ required: '๊ฒฝ๋๋ฅผ ์
๋ ฅํ์ฌ ์ฃผ์ธ์' }} // ๋ฉ์์ง ์์
render={({ field }) => (
<FilledInput
{...field}
error={!!errors.longitude}
autoComplete='off'
id='longitude'
name='longitude'
type="number" // ์ซ์ ์
๋ ฅ์ ๋ ์ ํฉํ ์ ์์
inputProps={{ step: "any" }} // ์์์ ํ์ฉ
/>
)}
/>
{errors.longitude &&
<FormHelperText className='!text-red-400'>
{errors.longitude?.message}
</FormHelperText>}
</FormControl>
</div>
<ButtonGroup variant='outlined'>
<Tooltip title='Clear Location' placement='bottom'>
<IconButton type='button' onClick={handleReset}> {/* ์ด๊ธฐํ ํธ๋ค๋ฌ ์ฌ์ฉ */}
<LocationDisabledOutlinedIcon />
</IconButton>
</Tooltip>
<Tooltip title='Set Office Location' placement='bottom'>
<IconButton type='button' onClick={onSetOffice}>
<PlaceOutlinedIcon />
</IconButton>
</Tooltip>
<Tooltip title='Update Map from Input' placement='bottom'>
<IconButton type='submit'> {/* ํผ ์ ์ถ ๋ฒํผ */}
<GpsFixedOutlinedIcon />
</IconButton>
</Tooltip>
</ButtonGroup>
</form>
{/* ์ฅ์ ์ ํ ๋๋กญ๋ค์ด */}
<FormControl sx={{ m: 1, minWidth: 200, maxWidth: '80%' }} size="small"> {/* ๋๋น ์กฐ์ */}
<InputLabel id="location-select-label">์ฅ์ ์ ํ</InputLabel>
<Select
labelId="location-select-label"
id="location-select"
value={locationId} // ์ํ๊ฐ ๋ฐ์ธ๋ฉ (string)
label="์ฅ์ ์ ํ"
onChange={handleChange}
disabled={isLoadingLocations} // ๋ก๋ฉ ์ค ๋นํ์ฑํ
>
<MenuItem value="">
<em>์ ํ ์ํจ</em>
</MenuItem>
{locationInfos?.map((info) => (
<MenuItem key={info.id} value={info.id.toString()}> {/* value๋ string์ผ๋ก */}
{info?.name || `Location ${info.id}`} {/* ์ด๋ฆ ์์ผ๋ฉด ๊ธฐ๋ณธ ํ
์คํธ */}
</MenuItem>
))}
</Select>
{isLoadingLocations && <FormHelperText>Loading locations...</FormHelperText>}
</FormControl>
</div>
);
}
export default GoogleMapPage;
์ฐ๊ด์ฝ๋
// -- VivGoogleMap.tsx --//
'use client';
import { ILocationInfo } from '@/interfaces/i-location-info';
import { IPosData } from '@/interfaces/i-pos-data';
import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';
import { useEffect, useRef, useState } from 'react';
interface MapComponentProps {
params: IPosData;
locations: ILocationInfo[]
}
const GOOGLE_MAP_LIBRARIES: ("marker")[] = ["marker"]; // ์ปดํฌ๋ํธ ๋ฐ!
export default function VivGoogleMap({ params, locations }: MapComponentProps) {
const [address, setAddress] = useState<string | null>();
const mapRef = useRef<google.maps.Map | null>(null);
const markerRefs = useRef<google.maps.marker.AdvancedMarkerElement[]>([]);
const { latitude, longitude }: IPosData = params || { latitude: 37.5665, longitude: 126.9780 };
const mapOptions = {
center: { lat: Number(latitude), lng: Number(longitude) },
zoom: 18,
mapId: process.env.NEXT_PUBLIC_GOOGLE_MAP_ID
};
const { isLoaded } = useJsApiLoader({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
language: 'ko', // ์ง๋ UI ํ
์คํธ, ๋ฒ๋ก ๋ฑ์ด ํ๊ธ๋ก ํํ๋จ
region: 'KR', // ์ฅ์ ์ด๋ฆ์ ์ง์ญ์ ํํ์ ํ๊ตญ์์ผ๋ก ์ ๋ ํจ.
libraries: GOOGLE_MAP_LIBRARIES // โ
์ด์ ๋ ์ด์ ๋งค๋ฒ ์ ๋ฐฐ์ด ์๋!
});
useEffect(() => {
if (!mapRef.current || !isLoaded) return;
(async () => {
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary;
// 1. ์ค์ ๋ง์ปค ์ถ๊ฐ
const centerMarker = new AdvancedMarkerElement({
map: mapRef.current,
position: { lat: Number(latitude), lng: Number(longitude) },
title: '+'
})
markerRefs.current.push(centerMarker);
// 2. ์์น ๋ชฉ๋ก ๋ง์ปค๋ค ์ถ๊ฐ
locations.forEach((l) => {
const div = document.createElement('div');
div.innerHTML = `<div style="background: #007bff;
color: white; padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
transform: translateY(-180%); /* ๐ ์๊ฒ ํต์ฌ! */
position: relative;
z-index: 1000;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
">${l.name}</div>`;
const advancedMarker = new AdvancedMarkerElement({
map: mapRef.current!,
position: { lat: l.latitude, lng: l.longitude },
content: div,
});
markerRefs.current.push(advancedMarker);
});
})();
}, [isLoaded, latitude, locations, longitude]);
const handleAddressSearch = async () => {
if (!address) return;
const geocoder = new google.maps.Geocoder();
geocoder.geocode({ address }, (results, status) => {
if (status === 'OK' && results && results[0]) {
const location = results[0].geometry.location;
const lat = location.lat();
const lng = location.lng();
// ์ง๋ ์ค์ฌ ์ด๋
mapRef.current?.panTo({ lat, lng });
// ๋ง์ปค ์ถ๊ฐ or ์ด๋
const centerMarker = new google.maps.marker.AdvancedMarkerElement({
map: mapRef.current,
position: { lat, lng },
title: '๊ฒ์๋ ์์น',
});
markerRefs.current.push(centerMarker);
} else {
alert('์ฃผ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค!');
}
});
};
if (!isLoaded) return <div>์ง๋๋ฅผ ๋ถ๋ฌ์ค๋ ์ค์
๋๋ค...</div>;
return (
<>
<GoogleMap
mapContainerStyle={{
width: 'calc(100%-2rem)',
height: '400px',
border: 'solid 5px #A6BBCF',
borderRadius: '1rem',
margin: '0 1rem'
}}
id='VIV-MAP-ID'
center={{ lat: Number(latitude), lng: Number(longitude) }}
onLoad={(map) => { mapRef.current = map }}
options={mapOptions} />
<div className='flex gap-4 justify-center items-center p-4 text-slate-400'>
<input
type="text"
placeholder="์ฃผ์๋ฅผ ์
๋ ฅํ์ธ์"
className='px-4 py-3 border-2 rounded-2xl'
onChange={(e) => setAddress(e.target.value)}
value={address ?? ''}
/>
<button
onClick={handleAddressSearch}
className='bg-sky-400
px-4 py-3 rounded-xl
cursor-pointer hover:bg-sky-700 text-white'
>
์ง๋ ์ด๋
</button>
</div>
</>
);
}
CONCLUSION
Google MAP API with next.js settings
// src/app/etc/ip-address/FormSection.tsx
'use client';
import { IIpInfo } from '@/interfaces/i-ip-info';
import { Box, IconButton, Stack, TextField } from '@mui/material';
import React, { useState } from 'react';
import DeleteIcon from '@mui/icons-material/Delete';
import TouchAppOutlinedIcon from '@mui/icons-material/TouchAppOutlined';
const api = process.env.NEXT_PUBLIC_IPINFO_URL2;
interface FormSectionProps {
initialIpInfo: IIpInfo;
onIpInfoFetched?: (ipInfo: IIpInfo) => void; // ๋ถ๋ชจ์๊ฒ IP ์ ๋ณด๋ฅผ ์ ๋ฌํ ํจ์ ํ์
์ ์
}
async function getInfoAsync(ip: string): Promise<IIpInfo> {
const response = await fetch(`${api}/api/ip/${ip}`);
if (!response.ok) {
throw new Error(`Failed to fetch IP info: ${response.statusText}`);
}
const data: IIpInfo = await response.json();
return data;
}
const FormSection: React.FC<FormSectionProps> = ({ initialIpInfo, onIpInfoFetched }) => {
const [ipAddress, setIpAddress] = useState<string>('');
const [info, setInfo] = useState<IIpInfo>(initialIpInfo);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
// ๋ถ๋ชจ๋ก๋ถํฐ ๋ฐ์ initialIpInfo๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ๋ด๋ถ info ์ํ ์
๋ฐ์ดํธ (์ ํ์ )
// ๋ง์ฝ ๋ถ๋ชจ๊ฐ ์ด๊ธฐ IP ์ธ์ ๋ค๋ฅธ ์ด์ ๋ก initialIpInfo๋ฅผ ๋ณ๊ฒฝํ ๊ฐ๋ฅ์ฑ์ด ์๋ค๋ฉด ํ์
const handleDelete = () => {
setIpAddress('');
setError(null);
};
const handleGetIpInfo = async () => {
if (ipAddress && !isLoading) {
setIsLoading(true);
setError(null); // ์ด์ ์๋ฌ ์ด๊ธฐํ
try {
const fetchedIpInfo = await getInfoAsync(ipAddress);
setInfo(fetchedIpInfo); // ์์ ์ปดํฌ๋ํธ ๋ด๋ถ ์ํ ์
๋ฐ์ดํธ (ํ์์ฉ)
// ? ํต์ฌ,์ฑ๊ณต์ ์ผ๋ก IP ์ ๋ณด๋ฅผ ๊ฐ์ก์์ผ๋ฉด ๋ถ๋ชจ๋ก ๋ถํฐ ๋ฐ์ ์ฝ๋ฐฑ ํจ์ ํธ์ถ ํ์ฌ ๋ฐ์ดํฐ ์ ๋ฌ
if (onIpInfoFetched)
onIpInfoFetched(fetchedIpInfo);
// ? ------------------------
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch IP info. Please try again.';
setError(errorMessage);
console.error('Error fetching IP info: ', error);
// ์๋ฌ ๋ฐ์ ์ ๋ถ๋ชจ์๊ฒ ์๋ฆด ํ์๊ฐ ์๋ค๋ฉด, onIpInfoFetched(null) ๋ฑ์ ํธ์ถํ ์๋ ์์
const nullIp: IIpInfo = {}
if (onIpInfoFetched)
onIpInfoFetched(nullIp); // ์์: ์๋ฌ ์ null ์ ๋ฌ
} finally {
setIsLoading(false);
}
}
}
const handleBlur = () => {
if (ipAddress && !/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ipAddress)) {
setError('Invalid IP format');
} else if (error === 'Invalid IP format') { // ์ ํจํ ํ์์ผ๋ก ๋ฐ๋๋ฉด ์๋ฌ ์ ๊ฑฐ
setError(null);
}
}
const handleIpAddress = (e: React.ChangeEvent<HTMLInputElement>) => {
setIpAddress(e.target.value);
if (error) { // ์
๋ ฅ ์ ์๋ฌ ๋ฉ์์ง ์ด๊ธฐํ (์ ํ์ )
setError(null);
}
};
const infoItems = [
{ label: 'IP', value: info.ip },
{ label: 'City', value: info.city },
{ label: 'Region', value: info.region },
{ label: 'Country', value: info.country },
{ label: 'Location', value: info.location },
{ label: 'ISP', value: info.isp },
];
return (
<div className='h-auto'>
<Box
component="form"
sx={{ '& > :not(style)': { m: 1, width: '25ch' } }}
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}
noValidate
autoComplete="off"
>
<h1 className="text-slate-400 text-center w-96 mx-auto mb-4">{info.ip}</h1>
<TextField id="ipaddress" name='ipaddress'
label="Enter IP Address"
variant="standard"
value={ipAddress}
onBlur={handleBlur}
onChange={handleIpAddress}
helperText={error}
disabled={isLoading}
/>
<span className='text-xs'>
{error && <p className="text-red-400 text-center">{error}</p>}
</span>
<Stack direction="row" spacing={1} style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<IconButton
color="warning"
disabled={isLoading}
onClick={handleDelete}>
<DeleteIcon />
</IconButton>
<div className='flex-1'></div>
<IconButton color="primary"
disabled={!ipAddress || isLoading || !!error}
onClick={handleGetIpInfo}>
<TouchAppOutlinedIcon />
</IconButton>
</Stack>
</Box>
<div className="border-2 rounded-2xl border-slate-400 p-4 flex flex-col mx-auto w-[500px]">
<dl style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '0.5rem' }}>
{infoItems.map((item) => (
<React.Fragment key={item.label}>
<dt style={{ fontWeight: 'bold', marginRight: '2em' }}>{item.label}</dt>
<dd>{item.value}</dd>
</React.Fragment>
))}
</dl>
<span>
{isLoading && <p className="text-blue-500 text-xs text-center">Loading...</p>}
{error && <p className="text-red-500 text-center">{error}</p>}
</span>
</div>
</div>
);
};
export default FormSection;
์ญ์ ๊ถํ ํ์ธ ์ค...
์์ ๊ถํ ํ์ธ ์ค...