공개이슈 ··· 및 그 전수 해소분. - 설정 저장·복원·초기화가 상주 프로세스 미러를 갱신하도록 단일 소유자화 - 만료 데이터 자동 정리 예약 12종 신설, 파기는 기본키 배치로 분할 - 회원 탈퇴를 전 경로 원자화하고 관리자 상태변경 경로를 정식 탈퇴로 통일 - 일괄 처리 목록의 선택을 화면에 보이는 행으로 한정(selectionScope) - 비밀번호 정책 문구를 도메인 중립 라벨로 정정 - 정적 검사 2종·검증 스킬 설계 정합성 축 신설
428 lines
13 KiB
PHP
428 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Services;
|
|
|
|
use App\Contracts\Extension\ModuleSettingsInterface;
|
|
use App\Contracts\Repositories\NotificationDefinitionRepositoryInterface;
|
|
use App\Services\NotificationDefinitionService;
|
|
use App\Traits\NormalizesSettingsData;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* 게시판 모듈 환경설정 서비스
|
|
*
|
|
* ModuleSettingsInterface를 구현하여 모듈별 설정을 관리합니다.
|
|
*/
|
|
class BoardSettingsService implements ModuleSettingsInterface
|
|
{
|
|
use NormalizesSettingsData;
|
|
|
|
/**
|
|
* 모듈 식별자
|
|
*/
|
|
private const MODULE_IDENTIFIER = 'sirsoft-board';
|
|
|
|
/**
|
|
* 설정 기본값 (캐시)
|
|
*/
|
|
private ?array $defaults = null;
|
|
|
|
/**
|
|
* 현재 설정값 (캐시)
|
|
*/
|
|
private ?array $settings = null;
|
|
|
|
/**
|
|
* 생성자
|
|
*
|
|
* @param BoardPermissionService $permissionService 게시판 권한 서비스
|
|
* @param NotificationDefinitionRepositoryInterface $notificationDefinitionRepository 알림 정의 저장소
|
|
*/
|
|
public function __construct(
|
|
private readonly BoardPermissionService $permissionService,
|
|
private readonly NotificationDefinitionRepositoryInterface $notificationDefinitionRepository,
|
|
) {
|
|
//
|
|
}
|
|
|
|
/**
|
|
* 모듈 설정 기본값 파일 경로 반환
|
|
*
|
|
* @return string|null defaults.json 파일의 절대 경로, 없으면 null
|
|
*/
|
|
public function getSettingsDefaultsPath(): ?string
|
|
{
|
|
$path = $this->getModulePath().'/config/settings/defaults.json';
|
|
|
|
return file_exists($path) ? $path : null;
|
|
}
|
|
|
|
/**
|
|
* 설정값 조회
|
|
*
|
|
* @param string $key 설정 키 (예: 'basic_defaults.per_page')
|
|
* @param mixed $default 기본값
|
|
* @return mixed 설정값
|
|
*/
|
|
public function getSetting(string $key, mixed $default = null): mixed
|
|
{
|
|
$settings = $this->getAllSettings();
|
|
|
|
return Arr::get($settings, $key, $default);
|
|
}
|
|
|
|
/**
|
|
* 설정값 저장
|
|
*
|
|
* @param string $key 설정 키
|
|
* @param mixed $value 저장할 값
|
|
* @return bool 성공 여부
|
|
*/
|
|
public function setSetting(string $key, mixed $value): bool
|
|
{
|
|
$settings = $this->getAllSettings();
|
|
Arr::set($settings, $key, $value);
|
|
|
|
// 카테고리 추출
|
|
$parts = explode('.', $key);
|
|
$category = $parts[0];
|
|
|
|
return $this->saveCategorySettings($category, $settings[$category] ?? []);
|
|
}
|
|
|
|
/**
|
|
* 전체 설정 조회
|
|
*
|
|
* @return array 모든 카테고리의 설정값
|
|
*/
|
|
public function getAllSettings(): array
|
|
{
|
|
if ($this->settings !== null) {
|
|
return $this->settings;
|
|
}
|
|
|
|
$defaults = $this->getDefaults();
|
|
$categories = $defaults['_meta']['categories'] ?? [];
|
|
$defaultValues = $defaults['defaults'] ?? [];
|
|
|
|
$settings = [];
|
|
foreach ($categories as $category) {
|
|
$categoryDefaults = $defaultValues[$category] ?? [];
|
|
$savedSettings = $this->loadCategorySettings($category);
|
|
$settings[$category] = array_merge($categoryDefaults, $savedSettings);
|
|
}
|
|
|
|
// 저장된 데이터를 defaults 스키마에 맞게 정규화 (하위호환성)
|
|
$settings = $this->normalizeSettingsData($settings, $defaultValues);
|
|
|
|
$this->settings = $settings;
|
|
|
|
return $settings;
|
|
}
|
|
|
|
/**
|
|
* 카테고리별 설정 조회
|
|
*
|
|
* @param string $category 카테고리명
|
|
* @return array 카테고리의 설정값
|
|
*/
|
|
public function getSettings(string $category): array
|
|
{
|
|
$allSettings = $this->getAllSettings();
|
|
|
|
return $allSettings[$category] ?? [];
|
|
}
|
|
|
|
/**
|
|
* 설정 저장
|
|
*
|
|
* @param array $settings 저장할 설정 배열
|
|
* @return bool 성공 여부
|
|
*/
|
|
public function saveSettings(array $settings): bool
|
|
{
|
|
$success = true;
|
|
$defaults = $this->getDefaults();
|
|
$defaultValues = $defaults['defaults'] ?? [];
|
|
|
|
foreach ($settings as $category => $categorySettings) {
|
|
if (str_starts_with($category, '_')) {
|
|
continue; // _meta, _tab 등 메타 정보 무시
|
|
}
|
|
|
|
// 카테고리 값이 배열이 아닌 경우 무시 (최상위 레벨 오염 데이터 방어)
|
|
if (! is_array($categorySettings)) {
|
|
continue;
|
|
}
|
|
|
|
// defaults 스키마에 맞게 정규화
|
|
$categoryDefaults = $defaultValues[$category] ?? [];
|
|
|
|
// Toggle/체크박스 OFF 시 키 미전송 대응: boolean 기본값 필드가 누락되면 false로 채움
|
|
foreach ($categoryDefaults as $key => $defaultValue) {
|
|
if (is_bool($defaultValue) && ! array_key_exists($key, $categorySettings)) {
|
|
$categorySettings[$key] = false;
|
|
}
|
|
}
|
|
|
|
// 신고 알림 활성화 토글 제거(알림 정의 활성화는 알림 설정 메뉴에서 관리)에 따라
|
|
// notify_admin_on_report / notify_author_on_report_action 은 항상 활성(true)으로 강제 저장한다.
|
|
// 신고 알림 발송 게이트(BoardNotificationDataListener)가 이 값을 참조하므로,
|
|
// false 로 저장되면 알림이 발송되지 않는다. 채널별 발송 여부는 알림 정의 템플릿에서 제어한다.
|
|
// (향후 토글 부활 가능성을 고려해 syncNotificationDefinitionStatus 는 유지)
|
|
if ($category === 'report_policy') {
|
|
$categorySettings['notify_admin_on_report'] = true;
|
|
$categorySettings['notify_author_on_report_action'] = true;
|
|
}
|
|
|
|
$processedSettings = $this->normalizeCategoryData($categorySettings, $categoryDefaults);
|
|
|
|
if (! $this->saveCategorySettings($category, $processedSettings)) {
|
|
$success = false;
|
|
}
|
|
}
|
|
|
|
// 캐시 초기화
|
|
$this->settings = null;
|
|
|
|
// 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109)
|
|
g7_refresh_module_settings_config('sirsoft-board');
|
|
|
|
// report_policy 설정 변경 시 알림 정의 활성 상태 동기화
|
|
// 저장된 최종 설정을 사용 (boolean 보정 후 값)
|
|
if (isset($settings['report_policy'])) {
|
|
$savedReportPolicy = $this->loadCategorySettings('report_policy');
|
|
$this->syncNotificationDefinitionStatus($savedReportPolicy);
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* 신고 정책 알림 설정에 따라 notification_definitions 활성 상태를 동기화합니다.
|
|
*
|
|
* 설정 OFF 시 해당 알림 정의를 비활성화하여
|
|
* NotificationHookListener가 훅을 구독하지 않도록 합니다.
|
|
*
|
|
* @param array $reportPolicy 신고 정책 설정
|
|
*/
|
|
private function syncNotificationDefinitionStatus(array $reportPolicy): void
|
|
{
|
|
$syncMap = [
|
|
'notify_admin_on_report' => 'report_received_admin',
|
|
'notify_author_on_report_action' => 'report_action',
|
|
];
|
|
|
|
$changed = false;
|
|
|
|
// sirsoft-board 확장의 알림 정의를 한 번만 조회 (type 매칭은 메모리에서 수행)
|
|
$boardDefinitions = $this->notificationDefinitionRepository
|
|
->getByExtension('module', self::MODULE_IDENTIFIER)
|
|
->keyBy('type');
|
|
|
|
foreach ($syncMap as $settingKey => $definitionType) {
|
|
if (! array_key_exists($settingKey, $reportPolicy)) {
|
|
continue;
|
|
}
|
|
|
|
$definition = $boardDefinitions->get($definitionType);
|
|
|
|
if ($definition === null) {
|
|
continue;
|
|
}
|
|
|
|
$desiredActive = (bool) $reportPolicy[$settingKey];
|
|
|
|
if ((bool) $definition->is_active === $desiredActive) {
|
|
continue;
|
|
}
|
|
|
|
$this->notificationDefinitionRepository->update($definition, ['is_active' => $desiredActive]);
|
|
$changed = true;
|
|
}
|
|
|
|
// 알림 정의 캐시 무효화 (NotificationHookListener가 새 상태를 읽도록)
|
|
if ($changed) {
|
|
app(NotificationDefinitionService::class)->invalidateAllCache();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 프론트엔드용 설정 조회 (민감정보 제외)
|
|
*
|
|
* frontend_schema에 따라 민감하지 않은 설정만 반환합니다.
|
|
*
|
|
* @return array 프론트엔드에 노출 가능한 설정값
|
|
*/
|
|
public function getFrontendSettings(): array
|
|
{
|
|
$defaults = $this->getDefaults();
|
|
$frontendSchema = $defaults['frontend_schema'] ?? [];
|
|
$allSettings = $this->getAllSettings();
|
|
|
|
$frontendSettings = [];
|
|
|
|
foreach ($frontendSchema as $category => $schema) {
|
|
if (! ($schema['expose'] ?? false)) {
|
|
continue;
|
|
}
|
|
|
|
$categorySettings = $allSettings[$category] ?? [];
|
|
$fields = $schema['fields'] ?? [];
|
|
|
|
if (empty($fields)) {
|
|
// fields가 없으면 전체 카테고리 노출
|
|
$frontendSettings[$category] = $categorySettings;
|
|
|
|
continue;
|
|
}
|
|
|
|
$exposedFields = [];
|
|
foreach ($fields as $fieldName => $fieldSchema) {
|
|
if ($fieldSchema['expose'] ?? false) {
|
|
$exposedFields[$fieldName] = $categorySettings[$fieldName] ?? null;
|
|
}
|
|
}
|
|
|
|
if (! empty($exposedFields)) {
|
|
$frontendSettings[$category] = $exposedFields;
|
|
}
|
|
}
|
|
|
|
return $frontendSettings;
|
|
}
|
|
|
|
/**
|
|
* 신고 관리 권한에 역할을 재할당합니다.
|
|
*
|
|
* @param array $reportPermissions { view_roles: [...], manage_roles: [...] }
|
|
*/
|
|
public function syncReportPermissionRoles(array $reportPermissions): void
|
|
{
|
|
$this->permissionService->syncModulePermissionRoles([
|
|
'sirsoft-board.reports.view' => $reportPermissions['view_roles'] ?? [],
|
|
'sirsoft-board.reports.manage' => $reportPermissions['manage_roles'] ?? [],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 신고 관리 권한에 현재 할당된 역할 목록을 반환합니다.
|
|
*
|
|
* @return array { view_roles: [...], manage_roles: [...] }
|
|
*/
|
|
public function getReportPermissionRoles(): array
|
|
{
|
|
return $this->permissionService->getModulePermissionRoles([
|
|
'sirsoft-board.reports.view',
|
|
'sirsoft-board.reports.manage',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 캐시 초기화
|
|
*/
|
|
public function clearCache(): void
|
|
{
|
|
$this->defaults = null;
|
|
$this->settings = null;
|
|
|
|
// 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109)
|
|
g7_refresh_module_settings_config('sirsoft-board');
|
|
}
|
|
|
|
/**
|
|
* 기본값 조회
|
|
*
|
|
* @return array defaults.json 내용
|
|
*/
|
|
private function getDefaults(): array
|
|
{
|
|
if ($this->defaults !== null) {
|
|
return $this->defaults;
|
|
}
|
|
|
|
$path = $this->getSettingsDefaultsPath();
|
|
if ($path === null) {
|
|
return [];
|
|
}
|
|
|
|
$content = File::get($path);
|
|
$this->defaults = json_decode($content, true) ?? [];
|
|
|
|
return $this->defaults;
|
|
}
|
|
|
|
/**
|
|
* 카테고리 설정 파일 경로 반환
|
|
*
|
|
* @param string $category 카테고리명
|
|
* @return string 설정 파일 경로
|
|
*/
|
|
private function getCategoryFilePath(string $category): string
|
|
{
|
|
return $this->getStoragePath().'/'.$category.'.json';
|
|
}
|
|
|
|
/**
|
|
* 카테고리 설정 로드
|
|
*
|
|
* @param string $category 카테고리명
|
|
* @return array 설정값
|
|
*/
|
|
private function loadCategorySettings(string $category): array
|
|
{
|
|
$path = $this->getCategoryFilePath($category);
|
|
|
|
if (! File::exists($path)) {
|
|
return [];
|
|
}
|
|
|
|
$content = File::get($path);
|
|
|
|
return json_decode($content, true) ?? [];
|
|
}
|
|
|
|
/**
|
|
* 카테고리 설정 저장
|
|
*
|
|
* @param string $category 카테고리명
|
|
* @param array $settings 설정값
|
|
* @return bool 성공 여부
|
|
*/
|
|
private function saveCategorySettings(string $category, array $settings): bool
|
|
{
|
|
$storagePath = $this->getStoragePath();
|
|
|
|
// 디렉토리 생성
|
|
if (! File::isDirectory($storagePath)) {
|
|
File::makeDirectory($storagePath, 0755, true);
|
|
}
|
|
|
|
$path = $this->getCategoryFilePath($category);
|
|
$content = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
return File::put($path, $content) !== false;
|
|
}
|
|
|
|
/**
|
|
* 모듈 경로 반환
|
|
*
|
|
* @return string 모듈 디렉토리 경로
|
|
*/
|
|
private function getModulePath(): string
|
|
{
|
|
return base_path('modules/'.self::MODULE_IDENTIFIER);
|
|
}
|
|
|
|
/**
|
|
* 설정 저장 경로 반환
|
|
*
|
|
* @return string 설정 파일 저장 디렉토리 경로
|
|
*/
|
|
private function getStoragePath(): string
|
|
{
|
|
return storage_path('app/modules/'.self::MODULE_IDENTIFIER.'/settings');
|
|
}
|
|
}
|