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
// =========================================================
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
{
@ -203,7 +219,8 @@ abstract class AbstractWebController extends Controller
'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()) {
@ -214,6 +231,7 @@ abstract class AbstractWebController extends Controller
}
$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 TITLE = Model::TITLE;
protected array $nullableFields = [
'user_uid',
'worker_uid',
];
protected $attributes = [
'user_uid' => null,
'worker_uid' => null,

View File

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

View File

@ -158,130 +158,6 @@ abstract class CommonForm
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
* --------------------------------------------------------------------- */
@ -294,9 +170,7 @@ abstract class CommonForm
*/
final public function validate(array &$formDatas): void
{
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class);
try {
$formDatas = $this->sanitizeFormDatas($formDatas);
$formFields = $this->getFormFields();
$formRules = $this->getFormRules();
@ -304,11 +178,6 @@ abstract class CommonForm
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
}
$this->ensureParentArrayForWildcardRules($formDatas, $formRules);
$numericFields = $this->collectNumericFieldsFromRules($formRules);
$formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields);
$dynamicRules = [];
foreach ($formRules as $field => $rule) {
[$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
@ -321,15 +190,12 @@ abstract class CommonForm
} else {
$label = $fieldName;
}
$dynamicRules[$fieldName] = [
'label' => $label,
'rules' => $ruleStr,
];
}
$this->validation->setRules($dynamicRules);
if (!$this->validation->run($formDatas)) {
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}]" : "");
break;
case "role":
// $formRules[$field] = 'required|trim|string';
$formRules[$field] = 'required|is_array|at_least_one';
$formRules['role.*'] = 'permit_empty|trim|in_list[manager,cloudflare,firewall,security,director,master]';
break;

View File

@ -18,19 +18,36 @@ class UserHelper extends CommonHelper
$form = form_password($field, "", $extras);
break;
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)
? array_map('strtolower', array_map('trim', $value))
: [];
$form = '';
//Form페이지에서는 맨앞에것 제외하기 위함
array_shift($viewDatas['formOptions'][$field]['options']);
foreach ($viewDatas['formOptions'][$field]['options'] as $key => $label) {
$checked = in_array(strtolower(trim($key)), $currentRoles);
// Form페이지에서는 맨앞에것 제외하기 위함
if (isset($viewDatas['formOptions'][$field]['options']) && is_array($viewDatas['formOptions'][$field]['options'])) {
$options = $viewDatas['formOptions'][$field]['options'];
// ✅ 원본을 건드리지 말고 복사본에서 shift (중요)
array_shift($options);
foreach ($options as $key => $label) {
$checked = in_array(strtolower(trim((string) $key)), $currentRoles, true);
$form .= '<label class="me-3">';
$form .= form_checkbox('role[]', $key, $checked, ['id' => "role_{$key}", ...$extras]);
$form .= form_checkbox('role[]', (string) $key, $checked, ['id' => "role_{$key}", ...$extras]);
$form .= " {$label}";
$form .= '</label>';
}
}
break;
default:
$form = parent::getFieldForm($field, $value, $viewDatas, $extras);

View File

@ -250,7 +250,7 @@ abstract class CommonService
}
//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;
}
@ -262,11 +262,14 @@ abstract class CommonService
$actionForm = $this->getActionForm();
if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('create', $formDatas);
// log_message('debug', 'BEFORE hook CREATE FORMDATA:' . print_r($formDatas ?? null, true));
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); // ✅ 여기서 검증
}
$entityClass = $this->getEntityClass();
$entity = new $entityClass($formDatas);
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
{
try {
$actionForm = $this->getActionForm();
if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('modify', $formDatas);
// log_message('debug', 'BEFORE hook MODIFY FORMDATA:' . print_r($formDatas ?? null, true));
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); // ✅ 여기서 검증
}
// 검증 통과 후 엔티티 반영
$formDatas = $this->save_before_fill($formDatas);
// log_message('debug', 'BEFORE MODIFY fill Entity:' . print_r($formDatas ?? null, true));
$entity->fill($formDatas);
// log_message('debug', 'AFTER MODIFY fill Entity:' . print_r($entity ?? null, true));
if (!$entity->hasChanged()) {
return $entity;
}

View File

@ -2,12 +2,11 @@
namespace App\Services;
use RuntimeException;
use App\Models\UserModel;
use App\Helpers\UserHelper;
use App\Forms\UserForm;
use App\Entities\UserEntity;
use App\Entities\CommonEntity;
class UserService extends CommonService
{
@ -28,6 +27,44 @@ class UserService extends CommonService
{
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 검색용
//FormFilter 조건절 처리
public function setFilter(string $field, mixed $filter_value): void