대용량 사이트에서 사이트맵 생성이 메모리 부족으로 반복 실패하던 문제(공개 )의 1단계로, 전체 URL을 메모리에 적재하던 구조를 스트리밍 분할 생성으로 교체한다. 사이트맵 스트리밍 코어: - SitemapWriter — 자식 파일 1개 분량만 버퍼링하고 임계 도달 시 flush. StorageInterface가 append를 제공하지 않아(put이 전체 덮어쓰기) 택한 구조이며, 최대 메모리가 "자식 파일 1개 크기"로 유계가 된다. URL 수(50,000)와 바이트(45MB) 두 임계로 분할해 sitemaps.org 프로토콜을 지킨다. - 커밋은 _tmp 전량 기록 → live 재배치 → manifest 마지막 기록 순. manifest 존재가 커밋 완료 신호다. 기존 manifest를 먼저 지우지 않아 스왑 중 봇에게 503이 나가지 않는다. - SitemapFileStore — 읽기측 + 파일 경로 SSoT. SitemapXmlRenderer — escape 단일 출처. 데이터 접근 계층 정리: - contributor 3종이 쿼리를 직접 조립하던 것을 Repository 위임으로 전환. MySQL 버퍼드 쿼리에서 cursor는 결과셋 전체를 적재하므로 lazyById를 쓴다. - SeoCacheStatsService → SeoCacheStatRepository 신설 (집계 4쿼리 → 1쿼리). - audit 룰 service-direct-data-access의 사각 2건 수정 — 확장 디렉토리 정규식이 상대경로를 매칭하지 못해 *Contributor처럼 접미사 없는 클래스가 전부 미검출이었다. SEO 캐시 설정 정합화: - SEO 설정의 캐시 3키가 cache 카테고리 형제 키에 항상 져서 운영자 입력이 조용히 무시되고 있었다(설정을 cache 카테고리로 이관하며 SEO 탭 옛 칸을 미제거). 고급 탭을 기준값으로 두되 SEO 탭에 지정이 있으면 오버라이드하도록 SeoCacheSettings 단일 출처로 정리. "지정 여부"는 null로 판정하므로 기본값을 비웠다. - 기존 설치는 Upgrade_7_1_0이 이행 — 옛 기본값과 같으면 미설정으로 비우고, 다르면 운영자 의도로 보아 보존한다. 코어 버전 bump와 루트 CHANGELOG는 계획서 결정(D20)에 따라 마감 단계에서 일괄 수행한다.
91 lines
6.7 KiB
PHP
91 lines
6.7 KiB
PHP
<?php
|
|
|
|
return [
|
|
// 사용자 관련 예외
|
|
'cannot_delete_super_admin' => '슈퍼 관리자는 삭제할 수 없습니다.',
|
|
|
|
'circular_reference' => '레이아웃 순환 참조 감지: :trace',
|
|
'max_depth_exceeded' => '레이아웃 중첩 깊이가 최대 허용 깊이(:max)를 초과했습니다.',
|
|
'template_file_copy_failed' => '템플릿 파일 복사 실패: :source → :destination',
|
|
'template_build_directory_creation_failed' => '템플릿 빌드 디렉토리 생성 실패: :path',
|
|
'template_dist_directory_not_found' => '템플릿 dist 디렉토리를 찾을 수 없습니다: :path',
|
|
'template_not_found' => '템플릿을 찾을 수 없습니다: :identifier',
|
|
'template_not_active' => '템플릿이 활성화되지 않았습니다: :identifier (상태: :status)',
|
|
|
|
// 낙관적 잠금 — 동시 저장 충돌
|
|
'concurrent_modification' => '다른 사용자가 먼저 저장했습니다. (리소스: :resource, 현재 버전: :current, 보낸 버전: :expected)',
|
|
|
|
// 레이아웃 관련 예외
|
|
'layout' => [
|
|
'duplicate_data_source_id' => 'data_sources ID 중복: :id',
|
|
'duplicate_data_source_id_in_file' => '레이아웃 파일 내 data_sources ID 중복: :ids (파일: :file)',
|
|
'duplicate_data_source_id_extends' => 'extends 상속 관계에서 data_sources ID 중복: :ids (자식: :child, 부모: :parent)',
|
|
'not_found' => '레이아웃을 찾을 수 없습니다: :name',
|
|
'parent_not_found' => '부모 레이아웃을 찾을 수 없습니다: :parent (요청 레이아웃: :child)',
|
|
|
|
// include 관련 예외
|
|
'include_file_not_found' => 'include 파일을 찾을 수 없습니다: :path (해석된 경로: :resolved)',
|
|
'invalid_include_json' => 'include 파일의 JSON 형식이 올바르지 않습니다: :path (오류: :error)',
|
|
'circular_include' => 'include 순환 참조가 감지되었습니다: :trace',
|
|
'max_include_depth_exceeded' => 'include 최대 깊이를 초과했습니다 (최대: :max단계)',
|
|
'include_outside_directory' => 'include 경로가 허용된 디렉토리 외부입니다: :path (허용: :allowed_dir)',
|
|
],
|
|
|
|
// 레이아웃 버전 관련 예외
|
|
'layout_version' => [
|
|
'save_failed_after_retries' => '레이아웃 버전 저장에 실패했습니다. :attempts회 재시도 후에도 실패했습니다.',
|
|
'save_failed_unexpected' => '레이아웃 버전 저장 중 예기치 않은 오류가 발생했습니다.',
|
|
],
|
|
|
|
// 설정 관련 예외
|
|
'settings' => [
|
|
'backup_creation_failed' => '설정 백업 파일 생성에 실패했습니다.',
|
|
'restore_failed' => '설정 복원에 실패했습니다.',
|
|
'category_not_found' => '설정 카테고리를 찾을 수 없습니다: :category',
|
|
'save_failed' => '설정 저장에 실패했습니다: :category',
|
|
'json_parse_failed' => 'JSON 파일 파싱에 실패했습니다: :path (:error)',
|
|
'add_category_module_only' => 'addCategory는 모듈에서만 사용할 수 있습니다.',
|
|
],
|
|
|
|
// 코어 업데이트 관련 예외
|
|
'core_update' => [
|
|
'handoff' => '업그레이드 핸드오프: :after_version 까지 완료 — :reason (재실행: :resume_command)',
|
|
],
|
|
|
|
// Vendor 번들/설치 관련 예외
|
|
'vendor' => [
|
|
'composer_not_available' => 'Composer를 실행할 수 없는 환경입니다. 번들 모드를 사용하세요.',
|
|
'composer_not_available_for_build' => 'vendor-bundle 빌드는 Composer 실행이 필요합니다. 개발 환경에서 Composer를 설치/설정한 후 다시 시도하세요.',
|
|
'bundle_build_composer_failed' => 'vendor-bundle 빌드 중 composer install 실행 실패 (exit :exit): :message',
|
|
'bundle_build_promote_failed' => 'vendor-bundle 빌드 결과를 최종 경로로 반영하지 못했습니다 (:from → :to)',
|
|
'composer_execution_failed' => 'Composer 실행에 실패했습니다: :message',
|
|
'bundle_zip_missing' => 'vendor-bundle.zip 파일을 찾을 수 없습니다: :path',
|
|
'bundle_manifest_missing' => 'vendor-bundle.json 파일을 찾을 수 없습니다: :path',
|
|
'bundle_manifest_invalid' => 'vendor-bundle.json 파일을 읽을 수 없습니다: :error',
|
|
'bundle_integrity_failed' => '번들 파일의 무결성 검증에 실패했습니다: :details',
|
|
'bundle_schema_unsupported' => '지원하지 않는 vendor-bundle.json 스키마 버전입니다: :version',
|
|
'zip_archive_not_available' => 'PHP ZipArchive 확장이 설치되어 있지 않습니다.',
|
|
'zip_hash_mismatch' => 'vendor-bundle.zip의 해시가 일치하지 않습니다 (기대: :expected, 실제: :actual).',
|
|
'composer_json_sha_mismatch' => 'composer.json이 번들 빌드 시점 이후 변경되었습니다. vendor-bundle:build 로 재빌드하세요.',
|
|
'composer_lock_sha_mismatch' => 'composer.lock이 번들 빌드 시점 이후 변경되었습니다. vendor-bundle:build 로 재빌드하세요.',
|
|
'bundle_contains_unsafe_path' => '번들 zip에 안전하지 않은 파일 경로가 포함되어 있습니다: :path',
|
|
'extraction_failed' => '번들 zip 추출에 실패했습니다: :message',
|
|
'no_vendor_strategy_available' => 'Vendor 설치 전략을 결정할 수 없습니다. Composer와 번들 zip 모두 사용 불가합니다.',
|
|
'source_dir_not_found' => '설치 소스 디렉토리를 찾을 수 없습니다: :path',
|
|
'target_not_writable' => '대상 디렉토리에 쓰기 권한이 없습니다: :path',
|
|
'target_not_writable_owner_hint' => "실행 사용자(:current_user)와 디렉토리 소유자(:owner)가 다릅니다. 'chown -R :current_user_name :path' 후 재시도하거나 소유자(:owner_name)로 SSH 로그인 후 실행하세요.",
|
|
'composer_json_not_found' => 'composer.json 파일을 찾을 수 없습니다: :path',
|
|
'no_composer_lock' => 'composer.lock 파일이 필요합니다. 먼저 composer install을 실행하세요: :path',
|
|
'vendor_dir_not_found' => 'vendor/ 디렉토리가 존재하지 않습니다. 먼저 composer install을 실행하세요: :path',
|
|
'composer_binary_not_found' => 'Composer 바이너리를 찾을 수 없습니다. COMPOSER_BINARY 환경변수를 설정하거나 composer를 PATH에 추가하세요.',
|
|
],
|
|
|
|
// SEO / Sitemap 관련 예외
|
|
'seo' => [
|
|
'sitemap_write_failed' => 'Sitemap 파일 기록에 실패했습니다: :path',
|
|
'sitemap_gzip_failed' => 'Sitemap 자식 파일 압축에 실패했습니다: :file',
|
|
'sitemap_temp_file_unreadable' => 'Sitemap 임시 파일을 읽을 수 없습니다: :file',
|
|
'sitemap_stat_group_column_unsupported' => '지원하지 않는 그룹 기준 컬럼입니다: :column',
|
|
],
|
|
];
|