- Google Translate API 연동으로 릴리즈 노트 한국어 자동 번역 - 코드 블록 보호 처리 (번역 대상에서 제외) - 긴 텍스트 단락 분할 번역 지원 - Alpine.js 한국어/English 토글 버튼 (localStorage 저장) - 기본값: 한국어
177 lines
5.1 KiB
PHP
177 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ClaudeCodeNewsService
|
|
{
|
|
private const CACHE_KEY = 'claude_code_releases';
|
|
|
|
private const CACHE_TTL = 3600; // 1시간
|
|
|
|
private const API_URL = 'https://api.github.com/repos/anthropics/claude-code/releases';
|
|
|
|
/**
|
|
* GitHub Releases 목록 조회 (캐싱, 한국어 번역 포함)
|
|
*/
|
|
public function getReleases(int $perPage = 20): array
|
|
{
|
|
return Cache::remember(self::CACHE_KEY, self::CACHE_TTL, function () use ($perPage) {
|
|
return $this->fetchFromGitHub($perPage);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 캐시 클리어
|
|
*/
|
|
public function clearCache(): void
|
|
{
|
|
Cache::forget(self::CACHE_KEY);
|
|
}
|
|
|
|
/**
|
|
* GitHub API에서 릴리즈 정보 가져오기 + 한국어 번역
|
|
*/
|
|
private function fetchFromGitHub(int $perPage): array
|
|
{
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'Accept' => 'application/vnd.github.v3+json',
|
|
'User-Agent' => 'SAM-MNG-App',
|
|
])->timeout(10)->get(self::API_URL, [
|
|
'per_page' => $perPage,
|
|
]);
|
|
|
|
if (! $response->successful()) {
|
|
return [];
|
|
}
|
|
|
|
return collect($response->json())
|
|
->map(function ($release) {
|
|
$body = $release['body'] ?? '';
|
|
$bodyKo = $this->translateToKorean($body);
|
|
|
|
return [
|
|
'id' => $release['id'],
|
|
'tag_name' => $release['tag_name'],
|
|
'name' => $release['name'] ?? $release['tag_name'],
|
|
'body_html' => Str::markdown($body),
|
|
'body_html_ko' => Str::markdown($bodyKo),
|
|
'published_at' => $release['published_at'],
|
|
'author' => $release['author']['login'] ?? 'unknown',
|
|
'author_avatar' => $release['author']['avatar_url'] ?? '',
|
|
'html_url' => $release['html_url'],
|
|
'prerelease' => $release['prerelease'] ?? false,
|
|
'draft' => $release['draft'] ?? false,
|
|
];
|
|
})
|
|
->toArray();
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 영문 마크다운을 한국어로 번역 (코드 블록 보호)
|
|
*/
|
|
private function translateToKorean(string $text): string
|
|
{
|
|
if (empty(trim($text))) {
|
|
return '';
|
|
}
|
|
|
|
// 코드 블록/인라인 코드를 플레이스홀더로 보호
|
|
$protected = [];
|
|
$safe = preg_replace_callback('/```[\s\S]*?```|`[^`\n]+`/', function ($match) use (&$protected) {
|
|
$key = 'ZXCBLK'.count($protected).'QWE';
|
|
$protected[$key] = $match[0];
|
|
|
|
return $key;
|
|
}, $text);
|
|
|
|
// 번역
|
|
if (mb_strlen($safe) <= 4000) {
|
|
$translated = $this->callGoogleTranslate($safe);
|
|
} else {
|
|
$translated = $this->translateLongText($safe);
|
|
}
|
|
|
|
// 코드 블록 복원
|
|
foreach ($protected as $key => $code) {
|
|
$translated = str_replace($key, $code, $translated);
|
|
}
|
|
|
|
return $translated;
|
|
}
|
|
|
|
/**
|
|
* 긴 텍스트를 단락 단위로 분할 번역
|
|
*/
|
|
private function translateLongText(string $text): string
|
|
{
|
|
$parts = preg_split('/(\n\n+)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
$chunks = [];
|
|
$current = '';
|
|
|
|
foreach ($parts as $part) {
|
|
if (mb_strlen($current.$part) > 4000 && $current !== '') {
|
|
$chunks[] = $current;
|
|
$current = $part;
|
|
} else {
|
|
$current .= $part;
|
|
}
|
|
}
|
|
if ($current !== '') {
|
|
$chunks[] = $current;
|
|
}
|
|
|
|
$result = '';
|
|
foreach ($chunks as $chunk) {
|
|
$result .= $this->callGoogleTranslate($chunk);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Google Translate API 호출
|
|
*/
|
|
private function callGoogleTranslate(string $text): string
|
|
{
|
|
try {
|
|
$response = Http::timeout(5)
|
|
->get('https://translate.googleapis.com/translate_a/single', [
|
|
'client' => 'gtx',
|
|
'sl' => 'en',
|
|
'tl' => 'ko',
|
|
'dt' => 't',
|
|
'q' => $text,
|
|
]);
|
|
|
|
if (! $response->successful()) {
|
|
return $text;
|
|
}
|
|
|
|
$result = $response->json();
|
|
$translated = '';
|
|
|
|
if (is_array($result) && isset($result[0]) && is_array($result[0])) {
|
|
foreach ($result[0] as $segment) {
|
|
if (is_array($segment) && isset($segment[0])) {
|
|
$translated .= $segment[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $translated !== '' ? $translated : $text;
|
|
} catch (\Exception $e) {
|
|
return $text;
|
|
}
|
|
}
|
|
}
|