diff --git a/app/Controllers/AbstractWebController.php b/app/Controllers/AbstractWebController.php
index 7b8953e..9aa8fd1 100644
--- a/app/Controllers/AbstractWebController.php
+++ b/app/Controllers/AbstractWebController.php
@@ -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);
}
}
diff --git a/app/Entities/BoardEntity.php b/app/Entities/BoardEntity.php
index 252f2a4..1938874 100644
--- a/app/Entities/BoardEntity.php
+++ b/app/Entities/BoardEntity.php
@@ -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,
diff --git a/app/Entities/CommonEntity.php b/app/Entities/CommonEntity.php
index 578b6f8..37d562a 100644
--- a/app/Entities/CommonEntity.php
+++ b/app/Entities/CommonEntity.php
@@ -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);
- }
- $this->attributes[$key] = ($value === '' || $value === null) ? null : $value;
- return;
- }
-
- // 기본: 그대로 저장
- $this->attributes[$key] = $value;
- return;
+ return $this->attributes[$key] = $value;
}
-
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'] ?? "";
}
}
diff --git a/app/Entities/UserEntity.php b/app/Entities/UserEntity.php
index 8f936b8..d87467b 100644
--- a/app/Entities/UserEntity.php
+++ b/app/Entities/UserEntity.php
@@ -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' => '',
diff --git a/app/Forms/CommonForm.php b/app/Forms/CommonForm.php
index 1f09d15..7ebf17a 100644
--- a/app/Forms/CommonForm.php
+++ b/app/Forms/CommonForm.php
@@ -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());
}
diff --git a/app/Forms/UserForm.php b/app/Forms/UserForm.php
index a8ab106..4fa0215 100644
--- a/app/Forms/UserForm.php
+++ b/app/Forms/UserForm.php
@@ -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;
diff --git a/app/Helpers/UserHelper.php b/app/Helpers/UserHelper.php
index 07495ef..e545519 100644
--- a/app/Helpers/UserHelper.php
+++ b/app/Helpers/UserHelper.php
@@ -18,18 +18,35 @@ 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 .= '';
+
+ // 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 .= '';
+ }
}
break;
default:
diff --git a/app/Services/CommonService.php b/app/Services/CommonService.php
index c9003c3..fb31963 100644
--- a/app/Services/CommonService.php
+++ b/app/Services/CommonService.php
@@ -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;
}
diff --git a/app/Services/UserService.php b/app/Services/UserService.php
index 7466116..6b81ee9 100644
--- a/app/Services/UserService.php
+++ b/app/Services/UserService.php
@@ -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