205 lines
5.9 KiB
PHP
205 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Contracts\Extension\CacheInterface;
|
|
use GeoIp2\Database\Reader;
|
|
use GeoIp2\Exception\AddressNotFoundException;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class GeoIpService
|
|
{
|
|
private ?Reader $reader = null;
|
|
|
|
public function __construct(private CacheInterface $cache) {}
|
|
|
|
/**
|
|
* IP 주소로부터 타임존을 조회합니다.
|
|
*
|
|
* @param string $ip IP 주소
|
|
* @param array $supportedTimezones 지원하는 타임존 목록
|
|
* @return string|null 타임존 문자열 또는 null
|
|
*/
|
|
public function getTimezoneByIp(string $ip, array $supportedTimezones): ?string
|
|
{
|
|
// GeoIP 비활성화 상태
|
|
if (! config('geoip.enabled', false)) {
|
|
return null;
|
|
}
|
|
|
|
// 캐시 확인 (g7_core_settings 우선, fallback config)
|
|
$cacheEnabled = (bool) g7_core_settings('cache.geoip_enabled', config('geoip.cache.enabled', true));
|
|
$cacheKey = 'geoip.timezone.'.$ip;
|
|
|
|
if ($cacheEnabled) {
|
|
$cached = $this->cache->get($cacheKey);
|
|
|
|
if ($cached !== null) {
|
|
// 빈 문자열은 이전 조회 실패를 의미
|
|
if ($cached === '') {
|
|
return null;
|
|
}
|
|
|
|
// 캐시된 값이 지원 타임존에 포함되면 반환
|
|
return in_array($cached, $supportedTimezones) ? $cached : null;
|
|
}
|
|
}
|
|
|
|
// GeoIP 조회
|
|
$timezone = $this->lookupTimezone($ip);
|
|
|
|
// 캐시 저장 (조회 실패 시에도 빈 문자열 저장하여 반복 조회 방지)
|
|
if ($cacheEnabled) {
|
|
$ttl = (int) g7_core_settings('cache.geoip_ttl', config('geoip.cache.ttl', 86400));
|
|
$this->cache->put($cacheKey, $timezone ?? '', $ttl);
|
|
}
|
|
|
|
// 지원 타임존 확인
|
|
if ($timezone && in_array($timezone, $supportedTimezones)) {
|
|
return $timezone;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* IP 주소로부터 국가 코드(ISO 3166-1 alpha-2)를 조회합니다.
|
|
*
|
|
* getTimezoneByIp 미러: 동일한 MaxMind GeoLite2-City Reader 를 재사용한다.
|
|
* 비로그인 사용자의 기본 배송국가 추정 등에 사용된다(커머스 ShippingCountryResolver).
|
|
*
|
|
* @param string $ip IP 주소
|
|
* @return string|null 국가 코드 (대문자 2자, 조회 실패 시 null)
|
|
*/
|
|
public function getCountryByIp(string $ip): ?string
|
|
{
|
|
// GeoIP 비활성화 상태
|
|
if (! config('geoip.enabled', false)) {
|
|
return null;
|
|
}
|
|
|
|
// 캐시 확인 (g7_core_settings 우선, fallback config)
|
|
$cacheEnabled = (bool) g7_core_settings('cache.geoip_enabled', config('geoip.cache.enabled', true));
|
|
$cacheKey = 'geoip.country.'.$ip;
|
|
|
|
if ($cacheEnabled) {
|
|
$cached = $this->cache->get($cacheKey);
|
|
|
|
if ($cached !== null) {
|
|
// 빈 문자열은 이전 조회 실패를 의미 (음성 캐시)
|
|
return $cached === '' ? null : $cached;
|
|
}
|
|
}
|
|
|
|
// GeoIP 조회
|
|
$country = $this->lookupCountry($ip);
|
|
|
|
// 캐시 저장 (조회 실패 시에도 빈 문자열 저장하여 반복 조회 방지)
|
|
if ($cacheEnabled) {
|
|
$ttl = (int) g7_core_settings('cache.geoip_ttl', config('geoip.cache.ttl', 86400));
|
|
$this->cache->put($cacheKey, $country ?? '', $ttl);
|
|
}
|
|
|
|
return $country;
|
|
}
|
|
|
|
/**
|
|
* MaxMind DB에서 국가 코드를 조회합니다.
|
|
*/
|
|
private function lookupCountry(string $ip): ?string
|
|
{
|
|
try {
|
|
$reader = $this->getReader();
|
|
|
|
if ($reader === null) {
|
|
return null;
|
|
}
|
|
|
|
$isoCode = $reader->city($ip)->country->isoCode;
|
|
|
|
return $isoCode !== null ? strtoupper($isoCode) : null;
|
|
} catch (AddressNotFoundException $e) {
|
|
// IP를 찾을 수 없음 (정상적인 케이스)
|
|
return null;
|
|
} catch (\Exception $e) {
|
|
Log::warning('GeoIP country lookup failed', [
|
|
'ip' => $ip,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MaxMind DB에서 타임존을 조회합니다.
|
|
*/
|
|
private function lookupTimezone(string $ip): ?string
|
|
{
|
|
try {
|
|
$reader = $this->getReader();
|
|
|
|
if ($reader === null) {
|
|
return null;
|
|
}
|
|
|
|
$record = $reader->city($ip);
|
|
|
|
return $record->location->timeZone;
|
|
} catch (AddressNotFoundException $e) {
|
|
// IP를 찾을 수 없음 (정상적인 케이스)
|
|
return null;
|
|
} catch (\Exception $e) {
|
|
Log::warning('GeoIP lookup failed', [
|
|
'ip' => $ip,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MaxMind Reader 인스턴스를 반환합니다.
|
|
*/
|
|
private function getReader(): ?Reader
|
|
{
|
|
if ($this->reader !== null) {
|
|
return $this->reader;
|
|
}
|
|
|
|
$dbPath = config('geoip.database_path');
|
|
|
|
if (! file_exists($dbPath)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$this->reader = new Reader($dbPath);
|
|
|
|
return $this->reader;
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to initialize GeoIP reader', [
|
|
'path' => $dbPath,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GeoIP 데이터베이스가 사용 가능한지 확인합니다.
|
|
*
|
|
* @return bool GeoIP 활성화 및 DB 파일 존재 여부
|
|
*/
|
|
public function isAvailable(): bool
|
|
{
|
|
if (! config('geoip.enabled', false)) {
|
|
return false;
|
|
}
|
|
|
|
return file_exists(config('geoip.database_path'));
|
|
}
|
|
}
|