Files
Gnuboard7/app/Http/Controllers/Api/Admin/LanguagePackController.php
T
2026-05-11 11:29:41 +09:00

417 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin;
use App\Exceptions\LanguagePackSlotConflictException;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Http\Requests\LanguagePack\InstallFromBundledRequest;
use App\Http\Requests\LanguagePack\IndexLanguagePackRequest;
use App\Http\Requests\LanguagePack\InstallFromFileRequest;
use App\Http\Requests\LanguagePack\InstallFromGithubRequest;
use App\Http\Requests\LanguagePack\InstallFromUrlRequest;
use App\Http\Requests\LanguagePack\ManifestPreviewRequest;
use App\Http\Requests\LanguagePack\UninstallLanguagePackRequest;
use App\Extension\Helpers\ChangelogParser;
use App\Http\Resources\LanguagePackCollection;
use App\Http\Resources\LanguagePackResource;
use App\Services\LanguagePackService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
/**
* 관리자용 언어팩 관리 컨트롤러.
*
* 컨트롤러는 얇게 유지하고 모든 비즈니스 로직은 LanguagePackService 에 위임합니다.
*/
class LanguagePackController extends AdminBaseController
{
/**
* @param LanguagePackService $service 언어팩 Service
*/
public function __construct(
private readonly LanguagePackService $service,
) {
parent::__construct();
}
/**
* 언어팩 목록을 페이지네이션으로 조회합니다.
*
* @param IndexLanguagePackRequest $request 목록 조회 요청
* @return JsonResponse 페이지네이션된 언어팩 목록
*/
public function index(IndexLanguagePackRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$perPage = (int) ($validated['per_page'] ?? 10);
$paginator = $this->service->list($validated, $perPage);
$collection = new LanguagePackCollection(collect($paginator->items()));
$payload = [
'data' => $collection->toArray($request)['data'],
'meta' => array_merge(
$collection->with($request)['meta'] ?? [],
[
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
]
),
'abilities' => $collection->resolveCollectionAbilities($request),
];
return $this->success('language_packs.fetch_success', $payload);
} catch (\Throwable $e) {
return $this->error('language_packs.fetch_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 언어팩 상세 정보를 조회합니다.
*
* `{id}` 가 정수면 DB 레코드를, 문자열(번들 식별자)이면 `lang-packs/_bundled/{id}` 의
* manifest 로부터 합성된 가상 행을 반환합니다 (미설치 번들의 모달 열람용).
*
* @param Request $request HTTP 요청
* @param string $id 언어팩 ID 또는 번들 식별자
* @return JsonResponse 언어팩 상세 정보
*/
// audit:allow controller-base-request-injection reason: GET 상세 조회. 입력 검증 없음 — Resource::toDetailArray($request) 의 Request 인자 전달 목적
public function show(Request $request, string $id): JsonResponse
{
$pack = $this->service->findOrBundled($id);
if (! $pack) {
return $this->notFound('language_packs.not_found');
}
$resource = new LanguagePackResource($pack);
return $this->success('language_packs.fetch_success', $resource->toDetailArray($request));
}
/**
* 업로드된 ZIP 파일에서 언어팩을 설치합니다.
*
* @param InstallFromFileRequest $request 설치 요청
* @return JsonResponse 설치 결과
*/
public function installFromFile(InstallFromFileRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$pack = $this->service->installFromFile(
$request->file('file'),
(bool) ($validated['auto_activate'] ?? false),
$this->getCurrentUser()?->id
);
return $this->success(
'language_packs.install_success',
(new LanguagePackResource($pack))->toArray($request),
201
);
} catch (ValidationException $e) {
return $this->error('language_packs.manifest_invalid', 422, $e->errors());
} catch (\Throwable $e) {
return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* GitHub URL 에서 언어팩을 설치합니다.
*
* @param InstallFromGithubRequest $request 설치 요청
* @return JsonResponse 설치 결과
*/
public function installFromGithub(InstallFromGithubRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$pack = $this->service->installFromGithub(
$validated['github_url'],
(bool) ($validated['auto_activate'] ?? false),
$this->getCurrentUser()?->id
);
return $this->success(
'language_packs.install_success',
(new LanguagePackResource($pack))->toArray($request),
201
);
} catch (ValidationException $e) {
return $this->error('language_packs.manifest_invalid', 422, $e->errors());
} catch (\Throwable $e) {
return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* `lang-packs/_bundled/{identifier}` 디렉토리의 번들 소스에서 언어팩을 설치(또는 재설치)합니다.
*
* @param InstallFromBundledRequest $request 설치 요청
* @return JsonResponse 설치 결과
*/
public function installFromBundled(InstallFromBundledRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$pack = $this->service->installFromBundled(
$validated['identifier'],
(bool) ($validated['auto_activate'] ?? false),
$this->getCurrentUser()?->id
);
return $this->success(
'language_packs.install_success',
(new LanguagePackResource($pack))->toArray($request),
201
);
} catch (ValidationException $e) {
return $this->error('language_packs.manifest_invalid', 422, $e->errors());
} catch (\Throwable $e) {
return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 임의 URL 에서 언어팩을 설치합니다.
*
* @param InstallFromUrlRequest $request 설치 요청
* @return JsonResponse 설치 결과
*/
public function installFromUrl(InstallFromUrlRequest $request): JsonResponse
{
try {
$validated = $request->validated();
$pack = $this->service->installFromUrl(
$validated['url'],
$validated['checksum'] ?? null,
(bool) ($validated['auto_activate'] ?? false),
$this->getCurrentUser()?->id
);
return $this->success(
'language_packs.install_success',
(new LanguagePackResource($pack))->toArray($request),
201
);
} catch (ValidationException $e) {
return $this->error('language_packs.manifest_invalid', 422, $e->errors());
} catch (\Throwable $e) {
return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 언어팩을 활성화합니다 (슬롯 스위칭).
*
* @param Request $request HTTP 요청
* @param int $id 언어팩 ID
* @return JsonResponse 활성화 결과
*/
// audit:allow controller-base-request-injection reason: force 플래그(boolean) 단순 토글. FormRequest 분리 시 빈 rules() 만 제공 — 검증 가치 없음
public function activate(Request $request, int $id): JsonResponse
{
$pack = $this->service->find($id);
if (! $pack) {
return $this->notFound('language_packs.not_found');
}
$force = (bool) $request->boolean('force', false);
try {
$pack = $this->service->activate($pack, $force);
return $this->success(
'language_packs.activate_success',
(new LanguagePackResource($pack))->toArray($request)
);
} catch (LanguagePackSlotConflictException $e) {
// 동일 슬롯에 다른 활성 팩 존재 — 프론트가 모달로 사용자 확인 후 force=true 로 재호출
return $this->error('language_packs.errors.slot_conflict', 409, [
'current' => (new LanguagePackResource($e->current))->toArray($request),
'target' => (new LanguagePackResource($e->target))->toArray($request),
]);
} catch (\Throwable $e) {
return $this->error('language_packs.activate_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 여러 언어팩을 일괄 활성화합니다 (PO #7 reactivate 모달 → "활성화" 버튼).
*
* @param \App\Http\Requests\LanguagePack\BulkActivateRequest $request 요청
* @return JsonResponse 결과 (succeeded/failed 분리)
*/
public function bulkActivate(\App\Http\Requests\LanguagePack\BulkActivateRequest $request): JsonResponse
{
$ids = $request->validated('ids');
$result = $this->service->bulkActivate($ids);
return $this->success('language_packs.bulk_activate_success', $result);
}
/**
* 언어팩을 비활성화합니다.
*
* @param Request $request HTTP 요청
* @param int $id 언어팩 ID
* @return JsonResponse 비활성화 결과
*/
// audit:allow controller-base-request-injection reason: 라우트 파라미터만 사용. Request 는 Resource::toArray($request) 전달용
public function deactivate(Request $request, int $id): JsonResponse
{
$pack = $this->service->find($id);
if (! $pack) {
return $this->notFound('language_packs.not_found');
}
try {
$pack = $this->service->deactivate($pack);
return $this->success(
'language_packs.deactivate_success',
(new LanguagePackResource($pack))->toArray($request)
);
} catch (\Throwable $e) {
return $this->error('language_packs.deactivate_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 언어팩을 제거합니다.
*
* @param UninstallLanguagePackRequest $request 제거 요청
* @param int $id 언어팩 ID
* @return JsonResponse 제거 결과
*/
public function uninstall(UninstallLanguagePackRequest $request, int $id): JsonResponse
{
$pack = $this->service->find($id);
if (! $pack) {
return $this->notFound('language_packs.not_found');
}
try {
$cascade = (bool) ($request->validated()['cascade'] ?? false);
$this->service->uninstall($pack, $cascade);
return $this->success('language_packs.uninstall_success');
} catch (\Throwable $e) {
return $this->error('language_packs.uninstall_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* GitHub 소스 언어팩의 업데이트 가능 여부를 확인합니다.
*
* @return JsonResponse 검사 결과 (checked, updates, details)
*/
public function checkUpdates(): JsonResponse
{
try {
$result = $this->service->checkUpdates();
return $this->success('language_packs.check_updates_success', $result);
} catch (\Throwable $e) {
return $this->error('language_packs.check_updates_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 언어팩을 신버전으로 업데이트합니다 (GitHub 재다운로드 + 적용).
*
* @param Request $request HTTP 요청
* @param int $id 언어팩 ID
* @return JsonResponse 업데이트된 언어팩
*/
// audit:allow controller-base-request-injection reason: 입력 검증 없음. Request 는 Resource::toArray($request) 전달용
public function performUpdate(Request $request, int $id): JsonResponse
{
$pack = $this->service->find($id);
if (! $pack) {
return $this->notFound('language_packs.not_found');
}
try {
$updated = $this->service->performUpdate($pack);
return $this->success(
'language_packs.update_success',
(new LanguagePackResource($updated))->toArray($request)
);
} catch (\Throwable $e) {
return $this->error('language_packs.update_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 번역/레지스트리/템플릿 언어 캐시를 무효화합니다.
*
* @return JsonResponse 캐시 갱신 결과
*/
public function refreshCache(): JsonResponse
{
try {
$result = $this->service->refreshCache();
return $this->success('language_packs.refresh_cache_success', $result);
} catch (\Throwable $e) {
return $this->error('language_packs.refresh_cache_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 설치 전 ZIP 의 manifest 와 검증 결과만 미리 조회합니다 (실제 설치 X).
*
* @param ManifestPreviewRequest $request 미리보기 요청
* @return JsonResponse manifest + validation 결과
*/
public function manifestPreview(ManifestPreviewRequest $request): JsonResponse
{
try {
$result = $this->service->previewManifest($request->file('file'));
return $this->success('language_packs.preview_success', $result);
} catch (\Throwable $e) {
return $this->error('language_packs.preview_failed', 422, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
/**
* 언어팩의 CHANGELOG.md 파일 내용을 반환합니다.
*
* `{id}` 가 정수면 DB 레코드를, 문자열(번들 식별자)이면 가상 행을 사용합니다.
*
* @param string $id 언어팩 ID 또는 번들 식별자
* @return JsonResponse CHANGELOG 문자열
*/
public function changelog(string $id): JsonResponse
{
$pack = $this->service->findOrBundled($id);
if (! $pack) {
return $this->notFound('language_packs.not_found');
}
try {
$directory = $pack->resolveDirectory();
$changelogPath = $directory.DIRECTORY_SEPARATOR.'CHANGELOG.md';
$entries = is_file($changelogPath) ? ChangelogParser::parse($changelogPath) : [];
$rawContent = is_file($changelogPath) ? (string) file_get_contents($changelogPath) : '';
return $this->success('language_packs.fetch_success', [
'identifier' => $pack->identifier,
'entries' => $entries,
'changelog' => $rawContent,
'has_changelog' => $entries !== [] || $rawContent !== '',
]);
} catch (\Throwable $e) {
return $this->error('language_packs.fetch_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]);
}
}
}