daemon-idc init

This commit is contained in:
최준흠 2026-02-13 17:39:29 +09:00
parent fcebaa0ae0
commit a0fa225f3e
9 changed files with 105 additions and 178 deletions

View File

@ -171,6 +171,22 @@ abstract class AbstractWebController extends Controller
// ========================================================= // =========================================================
// 공통화: runAction / okResponse / failResponse // 공통화: runAction / okResponse / failResponse
// ========================================================= // =========================================================
protected function stringifyError(mixed $x): string
{
if (is_string($x))
return $x;
if ($x instanceof \Throwable) {
return $x->getMessage();
}
if (is_array($x) || is_object($x)) {
$json = json_encode($x, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return $json !== false ? $json : print_r($x, true);
}
return (string) $x;
}
protected function runAction(string $action, callable $core): mixed protected function runAction(string $action, callable $core): mixed
{ {
@ -203,7 +219,8 @@ abstract class AbstractWebController extends Controller
'errors' => $e->errors, 'errors' => $e->errors,
]); ]);
} }
return $this->action_redirect_process('error', dev_exception($e->getMessage())); // ✅ redirect에는 string만 넣는다
return $this->action_redirect_process('error', $e->getMessage());
} }
if ($this->request->isAJAX()) { if ($this->request->isAJAX()) {
@ -214,6 +231,7 @@ abstract class AbstractWebController extends Controller
} }
$msg = $humanPrefix ? ($humanPrefix . $e->getMessage()) : $e->getMessage(); $msg = $humanPrefix ? ($humanPrefix . $e->getMessage()) : $e->getMessage();
return $this->action_redirect_process('error', dev_exception($msg)); // ✅ redirect에는 string만 넣는다
return $this->action_redirect_process('error', $msg);
} }
} }

View File

@ -9,10 +9,6 @@ class BoardEntity extends CommonEntity
{ {
const PK = Model::PK; const PK = Model::PK;
const TITLE = Model::TITLE; const TITLE = Model::TITLE;
protected array $nullableFields = [
'user_uid',
'worker_uid',
];
protected $attributes = [ protected $attributes = [
'user_uid' => null, 'user_uid' => null,
'worker_uid' => null, 'worker_uid' => null,

View File

@ -13,8 +13,6 @@ abstract class CommonEntity extends Entity
* 엔티티에서 "빈문자/공백 입력은 NULL로 저장"해야 하는 필드 목록. * 엔티티에서 "빈문자/공백 입력은 NULL로 저장"해야 하는 필드 목록.
* 기본은 배열이고, Entity에서 필요한 것만 override해서 채우면 . * 기본은 배열이고, Entity에서 필요한 것만 override해서 채우면 .
*/ */
protected array $nullableFields = [];
public function __construct(array|null $data = null) public function __construct(array|null $data = null)
{ {
parent::__construct($data); parent::__construct($data);
@ -35,21 +33,8 @@ abstract class CommonEntity extends Entity
final public function __set(string $key, $value = null) final public function __set(string $key, $value = null)
{ {
if (array_key_exists($key, $this->attributes)) { if (array_key_exists($key, $this->attributes)) {
return $this->attributes[$key] = $value;
// 이 엔티티에서 NULL로 보정할 필드만 처리 (화이트리스트)
if (!empty($this->nullableFields) && in_array($key, $this->nullableFields, true)) {
if (is_string($value)) {
$value = trim($value);
}
$this->attributes[$key] = ($value === '' || $value === null) ? null : $value;
return;
}
// 기본: 그대로 저장
$this->attributes[$key] = $value;
return;
} }
parent::__set($key, $value); parent::__set($key, $value);
} }
@ -72,21 +57,21 @@ abstract class CommonEntity extends Entity
final public function getStatus(): string final public function getStatus(): string
{ {
return $this->status ?? ""; return $this->attributes['status'] ?? "";
} }
final public function getUpdatedAt(): string final public function getUpdatedAt(): string
{ {
return $this->updated_at ?? ""; return $this->attributes['updated_at'] ?? "";
} }
final public function getCreatedAt(): string final public function getCreatedAt(): string
{ {
return $this->created_at ?? ""; return $this->attributes['created_at'] ?? "";
} }
final public function getDeletedAt(): string final public function getDeletedAt(): string
{ {
return $this->deleted_at ?? ""; return $this->attributes['deleted_at'] ?? "";
} }
} }

View File

@ -10,11 +10,6 @@ class UserEntity extends CommonEntity
const PK = Model::PK; const PK = Model::PK;
const TITLE = Model::TITLE; const TITLE = Model::TITLE;
protected array $nullableFields = [
'mobile',
// uid 같은 숫자 PK가 nullable이면 여기에 추가
];
// ✅ role은 반드시 "문자열" 기본값 (DB 저장형) // ✅ role은 반드시 "문자열" 기본값 (DB 저장형)
protected $attributes = [ protected $attributes = [
'id' => '', 'id' => '',

View File

@ -158,130 +158,6 @@ abstract class CommonForm
return $this->_batchjobButtons; return $this->_batchjobButtons;
} }
/* ---------------------------------------------------------------------
* Normalize / Sanitize
* --------------------------------------------------------------------- */
/**
* 1) 깊은 배열 구조 정리(배열은 유지)
* - 여기서는 null -> '' 같은 변환을 절대 하지 않습니다.
*/
protected function sanitizeFormDatas($data, string $path = '')
{
if (!is_array($data)) {
return $data;
}
foreach ($data as $k => $v) {
if (is_array($v)) {
$data[$k] = $this->sanitizeFormDatas($v, ($path !== '' ? "{$path}.{$k}" : (string) $k));
}
}
return $data;
}
/**
* 2) 숫자/FK 필드 정규화
* - '' -> null
* - 숫자 문자열 -> int
*/
protected function normalizeNumericEmptyToNull(array $data, array $numericFields): array
{
foreach ($numericFields as $f) {
if (!array_key_exists($f, $data)) {
continue;
}
if ($data[$f] === '') {
$data[$f] = null;
continue;
}
if (is_string($data[$f]) && ctype_digit($data[$f])) {
$data[$f] = (int) $data[$f];
}
}
return $data;
}
/**
* 3) role.* 같은 배열 원소 규칙이 있을 , 부모 배열 존재/타입 보정
*/
protected function ensureParentArrayForWildcardRules(array &$formDatas, array $formRules): void
{
foreach ($formRules as $fieldKey => $ruleDef) {
$fieldName = (string) $fieldKey;
if (!str_contains($fieldName, '.*')) {
continue;
}
$parent = str_replace('.*', '', $fieldName);
// 1) 부모가 없거나 ''/null 이면 빈 배열
if (!array_key_exists($parent, $formDatas) || $formDatas[$parent] === '' || $formDatas[$parent] === null) {
$formDatas[$parent] = [];
continue;
}
// 2) 문자열이면 CSV로 분해 (혹시 JSON 문자열이면 JSON 우선)
if (is_string($formDatas[$parent])) {
$raw = trim($formDatas[$parent]);
if ($raw === '') {
$formDatas[$parent] = [];
} else {
$decoded = json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$formDatas[$parent] = $decoded;
} else {
$formDatas[$parent] = explode(DEFAULTS["DELIMITER_COMMA"], $raw);
}
}
}
// 3) 배열이 아니면 강제 빈 배열
if (!is_array($formDatas[$parent])) {
$formDatas[$parent] = [];
}
// 4) 배열 원소 정리
$clean = array_map(
fn($v) => is_scalar($v) ? trim((string) $v) : '',
$formDatas[$parent]
);
$clean = array_values(array_filter($clean, fn($v) => $v !== ''));
$formDatas[$parent] = $clean;
}
}
/**
* 4) 검증 rule에 따라 numeric(FK 포함) 필드 수집
*/
protected function collectNumericFieldsFromRules(array $formRules): array
{
$numericFields = [];
foreach ($formRules as $field => $rule) {
$fieldName = (string) $field;
if (str_contains($fieldName, '.*')) {
continue;
}
// hook 적용
[$fieldName, $ruleStr] = $this->getValidationRule($fieldName, (string) $rule);
if (is_string($ruleStr) && str_contains($ruleStr, 'numeric')) {
$numericFields[] = $fieldName;
}
}
return array_values(array_unique($numericFields));
}
/* --------------------------------------------------------------------- /* ---------------------------------------------------------------------
* Validation * Validation
* --------------------------------------------------------------------- */ * --------------------------------------------------------------------- */
@ -294,9 +170,7 @@ abstract class CommonForm
*/ */
final public function validate(array &$formDatas): void final public function validate(array &$formDatas): void
{ {
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class);
try { try {
$formDatas = $this->sanitizeFormDatas($formDatas);
$formFields = $this->getFormFields(); $formFields = $this->getFormFields();
$formRules = $this->getFormRules(); $formRules = $this->getFormRules();
@ -304,11 +178,6 @@ abstract class CommonForm
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다."); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
} }
$this->ensureParentArrayForWildcardRules($formDatas, $formRules);
$numericFields = $this->collectNumericFieldsFromRules($formRules);
$formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields);
$dynamicRules = []; $dynamicRules = [];
foreach ($formRules as $field => $rule) { foreach ($formRules as $field => $rule) {
[$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule); [$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
@ -321,15 +190,12 @@ abstract class CommonForm
} else { } else {
$label = $fieldName; $label = $fieldName;
} }
$dynamicRules[$fieldName] = [ $dynamicRules[$fieldName] = [
'label' => $label, 'label' => $label,
'rules' => $ruleStr, 'rules' => $ruleStr,
]; ];
} }
$this->validation->setRules($dynamicRules); $this->validation->setRules($dynamicRules);
if (!$this->validation->run($formDatas)) { if (!$this->validation->run($formDatas)) {
throw new FormValidationException($this->validation->getErrors()); throw new FormValidationException($this->validation->getErrors());
} }

View File

@ -57,6 +57,7 @@ class UserForm extends CommonForm
$formRules[$field] = sprintf("required|trim|valid_email%s", in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""); $formRules[$field] = sprintf("required|trim|valid_email%s", in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : "");
break; break;
case "role": case "role":
// $formRules[$field] = 'required|trim|string';
$formRules[$field] = 'required|is_array|at_least_one'; $formRules[$field] = 'required|is_array|at_least_one';
$formRules['role.*'] = 'permit_empty|trim|in_list[manager,cloudflare,firewall,security,director,master]'; $formRules['role.*'] = 'permit_empty|trim|in_list[manager,cloudflare,firewall,security,director,master]';
break; break;

View File

@ -18,18 +18,35 @@ class UserHelper extends CommonHelper
$form = form_password($field, "", $extras); $form = form_password($field, "", $extras);
break; break;
case 'role': case 'role':
// ✅ value가 string이면 CSV -> array로 변환
if (is_string($value)) {
$value = array_values(array_filter(array_map('trim', explode(',', $value))));
} elseif ($value === null) {
$value = [];
}
// ✅ 현재 role 목록(소문자/trim 정규화)
$currentRoles = is_array($value) $currentRoles = is_array($value)
? array_map('strtolower', array_map('trim', $value)) ? array_map('strtolower', array_map('trim', $value))
: []; : [];
$form = ''; $form = '';
//Form페이지에서는 맨앞에것 제외하기 위함
array_shift($viewDatas['formOptions'][$field]['options']); // Form페이지에서는 맨앞에것 제외하기 위함
foreach ($viewDatas['formOptions'][$field]['options'] as $key => $label) { if (isset($viewDatas['formOptions'][$field]['options']) && is_array($viewDatas['formOptions'][$field]['options'])) {
$checked = in_array(strtolower(trim($key)), $currentRoles); $options = $viewDatas['formOptions'][$field]['options'];
$form .= '<label class="me-3">';
$form .= form_checkbox('role[]', $key, $checked, ['id' => "role_{$key}", ...$extras]); // ✅ 원본을 건드리지 말고 복사본에서 shift (중요)
$form .= " {$label}"; array_shift($options);
$form .= '</label>';
foreach ($options as $key => $label) {
$checked = in_array(strtolower(trim((string) $key)), $currentRoles, true);
$form .= '<label class="me-3">';
$form .= form_checkbox('role[]', (string) $key, $checked, ['id' => "role_{$key}", ...$extras]);
$form .= " {$label}";
$form .= '</label>';
}
} }
break; break;
default: default:

View File

@ -250,7 +250,7 @@ abstract class CommonService
} }
//Action 작업시 field에따른 Hook처리(각 Service에서 override); //Action 작업시 field에따른 Hook처리(각 Service에서 override);
protected function action_process_fieldhook(string $field, $value, array $formDatas): array protected function actionForm_fieldhook_process(string $field, $value, array $formDatas): array
{ {
return $formDatas; return $formDatas;
} }
@ -262,11 +262,14 @@ abstract class CommonService
$actionForm = $this->getActionForm(); $actionForm = $this->getActionForm();
if ($actionForm instanceof CommonForm) { if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('create', $formDatas); $actionForm->action_init_process('create', $formDatas);
// log_message('debug', 'BEFORE hook CREATE FORMDATA:' . print_r($formDatas ?? null, true));
foreach ($formDatas as $field => $value) { foreach ($formDatas as $field => $value) {
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas); $formDatas = $this->actionForm_fieldhook_process($field, $value, $formDatas);
} }
// log_message('debug', 'AFTER hook CREATE FORMDATA:' . print_r($formDatas ?? null, true));
$actionForm->validate($formDatas); // ✅ 여기서 검증 $actionForm->validate($formDatas); // ✅ 여기서 검증
} }
$entityClass = $this->getEntityClass(); $entityClass = $this->getEntityClass();
$entity = new $entityClass($formDatas); $entity = new $entityClass($formDatas);
if (!$entity instanceof $entityClass) { if (!$entity instanceof $entityClass) {
@ -289,19 +292,28 @@ abstract class CommonService
} }
//수정용 //수정용
protected function save_before_fill(array $formDatas): array
{
return $formDatas;
}
protected function modify_process($entity, array $formDatas): CommonEntity protected function modify_process($entity, array $formDatas): CommonEntity
{ {
try { try {
$actionForm = $this->getActionForm(); $actionForm = $this->getActionForm();
if ($actionForm instanceof CommonForm) { if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('modify', $formDatas); $actionForm->action_init_process('modify', $formDatas);
// log_message('debug', 'BEFORE hook MODIFY FORMDATA:' . print_r($formDatas ?? null, true));
foreach ($formDatas as $field => $value) { foreach ($formDatas as $field => $value) {
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas); $formDatas = $this->actionForm_fieldhook_process($field, $value, $formDatas);
} }
// log_message('debug', 'AFTER hook MODIFY FORMDATA:' . print_r($formDatas ?? null, true));
$actionForm->validate($formDatas); // ✅ 여기서 검증 $actionForm->validate($formDatas); // ✅ 여기서 검증
} }
// 검증 통과 후 엔티티 반영 // 검증 통과 후 엔티티 반영
$formDatas = $this->save_before_fill($formDatas);
// log_message('debug', 'BEFORE MODIFY fill Entity:' . print_r($formDatas ?? null, true));
$entity->fill($formDatas); $entity->fill($formDatas);
// log_message('debug', 'AFTER MODIFY fill Entity:' . print_r($entity ?? null, true));
if (!$entity->hasChanged()) { if (!$entity->hasChanged()) {
return $entity; return $entity;
} }

View File

@ -2,12 +2,11 @@
namespace App\Services; namespace App\Services;
use RuntimeException;
use App\Models\UserModel; use App\Models\UserModel;
use App\Helpers\UserHelper; use App\Helpers\UserHelper;
use App\Forms\UserForm; use App\Forms\UserForm;
use App\Entities\UserEntity; use App\Entities\UserEntity;
use App\Entities\CommonEntity;
class UserService extends CommonService class UserService extends CommonService
{ {
@ -28,6 +27,44 @@ class UserService extends CommonService
{ {
return $entity; return $entity;
} }
protected function actionForm_fieldhook_process(string $field, $value, array $formDatas): array
{
switch ($field) {
case 'role':
if (is_string($value)) {
$formDatas['role'] = explode(',', $value) ?? [];
}
break;
default:
$formDatas = parent::actionForm_fieldhook_process($field, $value, $formDatas);
break;
}
return $formDatas;
}
protected function save_before_fill(array $formDatas): array
{
// 1) DB 컬럼 아닌 값 제거
unset($formDatas['confirmpassword']);
// 2) role은 무조건 문자열로
if (array_key_exists('role', $formDatas)) {
$arr = is_array($formDatas['role'])
? $formDatas['role']
: explode(',', (string) $formDatas['role']);
$arr = array_values(array_filter(array_map('trim', $arr)));
sort($arr);
$formDatas['role'] = implode(',', $arr);
}
// 3) passwd는 빈 값이면 업데이트 제외 (원하면)
if (array_key_exists('passwd', $formDatas) && $formDatas['passwd'] === '') {
unset($formDatas['passwd']);
}
return $formDatas;
}
//List 검색용 //List 검색용
//FormFilter 조건절 처리 //FormFilter 조건절 처리
public function setFilter(string $field, mixed $filter_value): void public function setFilter(string $field, mixed $filter_value): void