Files
Gnuboard7/config/settings/defaults.json
T
HeuJung 2ae1972a3b perf(core): 대용량 목록 상한 총 건수·커서 계약 신설 및 요청당 반복 실행 비용 정리
공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다.
응답·행의 무게를 다룬 · 과 겹치지 않는다.

공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이
기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은
per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지
번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로
정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은
계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다.

계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면
그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다.
응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새
필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate
응답은 필드 단위로 이전과 동일하다.

요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일
재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어
화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한
변경이 종전처럼 다음 요청에 반영된다.

검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한
엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어
활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩
대신 선언형 config 로 옮겼다.

총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을
돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
2026-08-06 11:13:51 +09:00

352 lines
18 KiB
JSON

{
"_meta": {
"version": "1.0.0",
"description": "그누보드7 환경설정 기본값 및 프론트엔드 스키마",
"categories": ["general", "security", "mail", "upload", "seo", "cache", "debug", "drivers", "core_update", "geoip", "notifications", "identity", "pagination"]
},
"defaults": {
"general": {
"site_name": "그누보드7",
"site_url": "",
"site_description": "",
"admin_email": "",
"timezone": "Asia/Seoul",
"language": "ko",
"currency": "KRW",
"maintenance_mode": false,
"site_logo": [],
"asset_url_mode": "extension"
},
"security": {
"force_https": false,
"login_attempt_enabled": true,
"auth_token_lifetime": 30,
"max_login_attempts": 5,
"login_lockout_time": 5,
"two_factor_auth": false,
"password_min_length": 8,
"require_password_special_char": false,
"allow_internal_outbound_urls": false
},
"mail": {
"mailer": "smtp",
"host": "",
"port": 587,
"username": "",
"password": "",
"encryption": "tls",
"mailgun_domain": "",
"mailgun_secret": "",
"mailgun_endpoint": "api.mailgun.net",
"ses_key": "",
"ses_secret": "",
"ses_region": "ap-northeast-2",
"from_address": "noreply@example.com",
"from_name": "그누보드7"
},
"upload": {
"max_file_size": 10,
"allowed_extensions": ["jpg", "jpeg", "png", "gif", "webp", "pdf", "doc", "docx", "xls", "xlsx", "zip"],
"image_max_width": 2000,
"image_max_height": 2000,
"image_quality": 85
},
"seo": {
"meta_title_suffix": "",
"meta_description": "",
"meta_keywords": "",
"google_analytics_id": "",
"google_site_verification": "",
"naver_site_verification": "",
"bot_user_agents": ["kakaotalk-scrap", "Meta-ExternalAgent", "ChatGPT-User"],
"bot_detection_enabled": true,
"bot_detection_library_enabled": true,
"og_default_site_name": "",
"og_image_default_width": 1200,
"og_image_default_height": 630,
"twitter_default_card": "summary_large_image",
"twitter_default_site": "",
"cache_enabled": null,
"cache_ttl": null,
"sitemap_enabled": true,
"sitemap_cache_ttl": null,
"sitemap_urls_per_file": 50000,
"sitemap_gzip": false,
"sitemap_serve_stale_on_miss": true,
"sitemap_max_urls_per_contributor": 0,
"sitemap_hreflang_enabled": true,
"sitemap_schedule": "daily",
"sitemap_schedule_time": "02:00",
"sitemap_last_updated_at": "",
"generator_enabled": true,
"generator_content": ""
},
"cache": {
"enabled": true,
"default_ttl": 86400,
"layout_enabled": true,
"layout_ttl": 3600,
"seo_enabled": true,
"seo_ttl": 7200,
"seo_sitemap_ttl": 86400,
"stats_enabled": true,
"stats_ttl": 1800,
"notification_ttl": 3600,
"extension_status_ttl": 86400,
"geoip_enabled": true,
"geoip_ttl": 86400,
"version_check_ttl": 3600
},
"debug": {
"mode": false,
"sql_query_log": false,
"log_level": "error"
},
"core_update": {
"github_url": "",
"github_token": ""
},
"geoip": {
"feature_enabled": false,
"license_key": "",
"auto_update_enabled": true,
"last_updated_at": ""
},
"pagination": {
"result_cap": 10000,
"max_page": 1000
},
"drivers": {
"storage_driver": "local",
"s3_bucket": "",
"s3_region": "ap-northeast-2",
"s3_access_key": "",
"s3_secret_key": "",
"s3_url": "",
"cache_driver": "file",
"redis_host": "127.0.0.1",
"redis_port": 6379,
"redis_password": "",
"redis_database": 0,
"memcached_host": "127.0.0.1",
"memcached_port": 11211,
"session_driver": "file",
"session_lifetime": 120,
"queue_driver": "sync",
"log_driver": "daily",
"log_level": "error",
"log_days": 14,
"websocket_enabled": false,
"websocket_app_id": "",
"websocket_app_key": "",
"websocket_app_secret": "",
"websocket_host": "localhost",
"websocket_port": 8080,
"websocket_scheme": "https",
"websocket_verify_ssl": true,
"websocket_server_host": "127.0.0.1",
"websocket_server_port": 8080,
"websocket_server_scheme": "http",
"search_engine_driver": "mysql-fulltext"
},
"notifications": {
"channels": [
{ "id": "mail", "is_active": true, "sort_order": 1 },
{ "id": "database", "is_active": true, "sort_order": 2 }
]
},
"identity": {
"default_provider": "g7:core.mail",
"purpose_providers": {
"password_reset": null,
"self_update": null,
"sensitive_action": null
},
"challenge_ttl_minutes": 15,
"max_attempts": 5
}
},
"frontend_schema": {
"_description": "프론트엔드에 노출할 설정 필드 정의. 각 카테고리별로 노출할 필드와 타입 캐스팅, 민감 정보 여부를 정의합니다.",
"general": {
"expose": true,
"_comment": "site_url, admin_email, language, timezone는 프론트엔드에서 미참조하므로 expose: false",
"fields": {
"site_name": { "type": "string", "sensitive": false },
"site_url": { "type": "string", "sensitive": false, "expose": false },
"site_description": { "type": "string", "sensitive": false },
"admin_email": { "type": "string", "sensitive": false, "expose": false },
"language": { "type": "string", "sensitive": false, "expose": false },
"timezone": { "type": "string", "sensitive": false, "expose": false },
"site_logo": { "type": "array", "sensitive": false, "transform": "attachments" },
"site_logo_url": { "type": "string", "sensitive": false, "computed": true, "source": "site_logo", "transform": "first_image_url" },
"asset_url_mode": { "type": "string", "sensitive": false }
}
},
"security": {
"expose": false,
"_comment": "보안 정책은 프론트엔드에서 미참조 — 노출 시 공격자에게 정보 제공 위험"
},
"mail": {
"expose": false,
"_comment": "mail 설정은 password 포함으로 프론트엔드에 노출하지 않음"
},
"upload": {
"expose": true,
"fields": {
"max_file_size": { "type": "integer", "sensitive": false },
"allowed_extensions": { "type": "string", "sensitive": false, "transform": "join_comma", "expose": false },
"allowed_extensions_formatted": { "type": "string", "sensitive": false, "computed": true, "source": "allowed_extensions", "transform": "format_extensions" },
"image_max_width": { "type": "integer", "sensitive": false },
"image_max_height": { "type": "integer", "sensitive": false },
"image_quality": { "type": "integer", "sensitive": false, "expose": false }
}
},
"seo": {
"expose": false,
"_comment": "SEO 설정 전체 — 프론트엔드 미참조 (admin 환경설정 폼은 API 데이터 기반)",
"fields": {
"meta_title_suffix": { "type": "string", "sensitive": false },
"meta_description": { "type": "string", "sensitive": false },
"meta_keywords": { "type": "string", "sensitive": false },
"google_analytics_id": { "type": "string", "sensitive": false },
"google_site_verification": { "type": "string", "sensitive": false },
"naver_site_verification": { "type": "string", "sensitive": false },
"bot_user_agents": { "type": "array", "sensitive": false },
"bot_detection_enabled": { "type": "boolean", "sensitive": false },
"bot_detection_library_enabled": { "type": "boolean", "sensitive": false },
"og_default_site_name": { "type": "string", "sensitive": false },
"og_image_default_width": { "type": "integer", "sensitive": false },
"og_image_default_height": { "type": "integer", "sensitive": false },
"twitter_default_card": { "type": "string", "sensitive": false },
"twitter_default_site": { "type": "string", "sensitive": false },
"cache_enabled": { "type": "boolean", "sensitive": false },
"cache_ttl": { "type": "integer", "sensitive": false },
"sitemap_enabled": { "type": "boolean", "sensitive": false },
"sitemap_cache_ttl": { "type": "integer", "sensitive": false },
"sitemap_urls_per_file": { "type": "integer", "sensitive": false },
"sitemap_gzip": { "type": "boolean", "sensitive": false },
"sitemap_serve_stale_on_miss": { "type": "boolean", "sensitive": false },
"sitemap_max_urls_per_contributor": { "type": "integer", "sensitive": false },
"sitemap_hreflang_enabled": { "type": "boolean", "sensitive": false },
"sitemap_schedule": { "type": "string", "sensitive": false },
"sitemap_schedule_time": { "type": "string", "sensitive": false },
"sitemap_last_updated_at": { "type": "string", "sensitive": false },
"generator_enabled": { "type": "boolean", "sensitive": false },
"generator_content": { "type": "string", "sensitive": false }
}
},
"cache": {
"expose": true,
"frontend_name": "advanced",
"_comment": "cache와 debug를 합쳐서 advanced로 노출",
"fields": {
"enabled": { "type": "boolean", "sensitive": false, "frontend_key": "cache_enabled", "expose": false },
"default_ttl": { "type": "integer", "sensitive": false, "frontend_key": "cache_default_ttl", "expose": false },
"layout_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "layout_cache_enabled", "expose": false },
"layout_ttl": { "type": "integer", "sensitive": false, "frontend_key": "layout_cache_ttl", "expose": false },
"seo_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "seo_cache_enabled", "expose": false },
"seo_ttl": { "type": "integer", "sensitive": false, "frontend_key": "seo_cache_ttl", "expose": false },
"seo_sitemap_ttl": { "type": "integer", "sensitive": false, "frontend_key": "seo_sitemap_cache_ttl", "expose": false },
"stats_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "stats_cache_enabled", "expose": false },
"stats_ttl": { "type": "integer", "sensitive": false, "frontend_key": "stats_cache_ttl", "expose": false },
"notification_ttl": { "type": "integer", "sensitive": false, "frontend_key": "notification_cache_ttl", "expose": false },
"extension_status_ttl": { "type": "integer", "sensitive": false, "frontend_key": "extension_status_cache_ttl", "expose": false },
"geoip_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "geoip_cache_enabled", "expose": false },
"geoip_ttl": { "type": "integer", "sensitive": false, "frontend_key": "geoip_cache_ttl", "expose": false },
"version_check_ttl": { "type": "integer", "sensitive": false, "frontend_key": "version_check_cache_ttl", "expose": false }
}
},
"debug": {
"expose": true,
"merge_into": "advanced",
"_comment": "debug 설정은 advanced 카테고리에 병합",
"fields": {
"mode": { "type": "boolean", "sensitive": false, "frontend_key": "debug_mode" },
"sql_query_log": { "type": "boolean", "sensitive": false, "expose": false }
}
},
"core_update": {
"expose": true,
"merge_into": "advanced",
"_comment": "core_update 설정은 advanced 카테고리에 병합",
"fields": {
"github_url": { "type": "string", "sensitive": false, "frontend_key": "core_update_github_url", "expose": false },
"github_token": { "type": "string", "sensitive": true, "frontend_key": "core_update_github_token" }
}
},
"geoip": {
"expose": true,
"merge_into": "advanced",
"_comment": "GeoIP 기능 설정은 advanced 카테고리에 병합",
"fields": {
"feature_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "geoip_enabled" },
"license_key": { "type": "string", "sensitive": true, "frontend_key": "geoip_license_key" },
"auto_update_enabled": { "type": "boolean", "sensitive": false, "frontend_key": "geoip_auto_update_enabled" },
"last_updated_at": { "type": "string", "sensitive": false, "frontend_key": "geoip_last_updated_at" }
}
},
"pagination": {
"expose": true,
"merge_into": "advanced",
"_comment": "목록 한계값 — advanced 카테고리에 병합",
"fields": {
"result_cap": { "type": "integer", "sensitive": false, "frontend_key": "pagination_result_cap" },
"max_page": { "type": "integer", "sensitive": false, "frontend_key": "pagination_max_page" }
}
},
"drivers": {
"expose": false,
"_comment": "드라이버 설정 — 프론트엔드 미노출 (admin_settings에서 API 원본값 기반 비교로 변경)",
"fields": {
"storage_driver": { "type": "string", "sensitive": false },
"s3_bucket": { "type": "string", "sensitive": false },
"s3_region": { "type": "string", "sensitive": false },
"s3_access_key": { "type": "string", "sensitive": true },
"s3_secret_key": { "type": "string", "sensitive": true },
"s3_url": { "type": "string", "sensitive": false },
"cache_driver": { "type": "string", "sensitive": false },
"redis_host": { "type": "string", "sensitive": false },
"redis_port": { "type": "integer", "sensitive": false },
"redis_password": { "type": "string", "sensitive": true },
"redis_database": { "type": "integer", "sensitive": false },
"memcached_host": { "type": "string", "sensitive": false },
"memcached_port": { "type": "integer", "sensitive": false },
"session_driver": { "type": "string", "sensitive": false },
"session_lifetime": { "type": "integer", "sensitive": false },
"queue_driver": { "type": "string", "sensitive": false },
"log_driver": { "type": "string", "sensitive": false },
"log_level": { "type": "string", "sensitive": false },
"log_days": { "type": "integer", "sensitive": false },
"websocket_enabled": { "type": "boolean", "sensitive": false },
"websocket_app_id": { "type": "string", "sensitive": false },
"websocket_app_key": { "type": "string", "sensitive": false },
"websocket_app_secret": { "type": "string", "sensitive": true },
"websocket_host": { "type": "string", "sensitive": false },
"websocket_port": { "type": "integer", "sensitive": false },
"websocket_scheme": { "type": "string", "sensitive": false },
"websocket_verify_ssl": { "type": "boolean", "sensitive": false },
"websocket_server_host": { "type": "string", "sensitive": false },
"websocket_server_port": { "type": "integer", "sensitive": false },
"websocket_server_scheme": { "type": "string", "sensitive": false }
}
},
"notifications": {
"expose": true,
"fields": {
"channels": { "type": "array", "sensitive": false }
}
},
"identity": {
"expose": true,
"_comment": "본인인증 인프라 provider 기술 파라미터 — signup/enabled 등 정책 분기 키는 IdentityPolicy 로 흡수됨",
"fields": {
"default_provider": { "type": "string", "sensitive": false },
"purpose_providers": { "type": "array", "sensitive": false, "expose": false },
"challenge_ttl_minutes": { "type": "integer", "sensitive": false, "expose": false },
"max_attempts": { "type": "integer", "sensitive": false, "expose": false }
}
}
}
}