dbmsv4 init...5

This commit is contained in:
최준흠 2026-02-09 11:15:16 +09:00
parent 509cd0e999
commit 40b809949c
9 changed files with 465 additions and 217 deletions

View File

@ -25,6 +25,7 @@ class Validation extends BaseConfig
FormatRules::class, FormatRules::class,
FileRules::class, FileRules::class,
CreditCardRules::class, CreditCardRules::class,
\App\Validation\CustomRules::class, // ✅ 추가
]; ];
/** /**
@ -34,7 +35,7 @@ class Validation extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public array $templates = [ public array $templates = [
'list' => 'CodeIgniter\Validation\Views\list', 'list' => 'CodeIgniter\Validation\Views\list',
'single' => 'CodeIgniter\Validation\Views\single', 'single' => 'CodeIgniter\Validation\Views\single',
]; ];

View File

@ -8,10 +8,13 @@ class ClientEntity extends CustomerEntity
{ {
const PK = ClientModel::PK; const PK = ClientModel::PK;
const TITLE = ClientModel::TITLE; const TITLE = ClientModel::TITLE;
protected array $nullableFields = [ protected array $nullableFields = [
'id', 'id',
'passwd', 'passwd',
]; ];
// ✅ role은 반드시 string 기본값
protected $attributes = [ protected $attributes = [
'id' => null, 'id' => null,
'passwd' => null, 'passwd' => null,
@ -19,13 +22,14 @@ class ClientEntity extends CustomerEntity
'name' => '', 'name' => '',
'phone' => '', 'phone' => '',
'email' => '', 'email' => '',
'role' => [], 'role' => '', // ✅ [] 금지
'account_balance' => 0, 'account_balance' => 0,
'coupon_balance' => 0, 'coupon_balance' => 0,
'point_balance' => 0, 'point_balance' => 0,
'status' => '', 'status' => '',
'history' => '' 'history' => '',
]; ];
public function __construct(array|null $data = null) public function __construct(array|null $data = null)
{ {
parent::__construct($data); parent::__construct($data);
@ -35,91 +39,104 @@ class ClientEntity extends CustomerEntity
{ {
return $this->user_uid ?? null; return $this->user_uid ?? null;
} }
//기본기능
public function getCustomTitle(mixed $title = null): string public function getCustomTitle(mixed $title = null): string
{ {
return sprintf("%s/%s", $this->getSite(), $title ? $title : $this->getTitle()); return sprintf("%s/%s", $this->getSite(), $title ? $title : $this->getTitle());
} }
public function getName(): string public function getName(): string
{ {
return $this->name; return (string) ($this->attributes['name'] ?? '');
} }
public function getSite(): string public function getSite(): string
{ {
return $this->site; return (string) ($this->attributes['site'] ?? '');
} }
public function getAccountBalance(): int public function getAccountBalance(): int
{ {
return $this->account_balance ?? 0; return (int) ($this->attributes['account_balance'] ?? 0);
} }
public function getCouponBalance(): int public function getCouponBalance(): int
{ {
return $this->coupon_balance ?? 0; return (int) ($this->attributes['coupon_balance'] ?? 0);
} }
public function getPointBalance(): int public function getPointBalance(): int
{ {
return $this->point_balance ?? 0; return (int) ($this->attributes['point_balance'] ?? 0);
} }
public function getHistory(): string|null public function getHistory(): string|null
{ {
return $this->history; return $this->attributes['history'] ?? null;
} }
/*
* 사용자의 역할을 배열 형태로 반환합니다. /**
* DB의 JSON 또는 CSV 형식 데이터를 모두 배열로 복구할 있는 로직을 포함합니다. * role을 배열로 반환
* @return array
*/ */
public function getRole(): array public function getRole(): array
{ {
$role = $this->role ?? null; $role = $this->attributes['role'] ?? null;
// 1. 이미 배열인 경우 (방어적 코딩)
if (is_array($role)) { if (is_array($role)) {
return array_filter($role); return array_values(array_filter($role, fn($v) => (string) $v !== ''));
} }
// 2. 문자열 데이터인 경우 처리
if (is_string($role) && !empty($role)) { if (is_string($role) && $role !== '') {
// 2-a. JSON 디코딩 시도 (기존 DB의 JSON 형식 처리) $decoded = json_decode($role, true);
$decodedRole = json_decode($role, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
if (json_last_error() === JSON_ERROR_NONE && is_array($decodedRole)) { $clean = array_map(
return $decodedRole; fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
$decoded
);
return array_values(array_filter($clean, fn($v) => $v !== ''));
} }
// 2-b. JSON이 아니면 CSV로 가정하고 변환
$parts = explode(DEFAULTS["DELIMITER_COMMA"], $role); $parts = explode(DEFAULTS["DELIMITER_COMMA"], $role);
// 각 요소의 불필요한 공백과 따옴표 제거. null 가능성에 대비해 string 형변환 추가 $clean = array_map(
$cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $parts); fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
return array_filter($cleanedRoles); $parts
);
return array_values(array_filter($clean, fn($v) => $v !== ''));
} }
// 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환
return []; return [];
} }
// --- Setter Methods ---
/** /**
* Role 데이터가 Entity에 설정될 호출되어, 입력된 CSV/JSON 문자열을 정리 * role은 DB 저장용 CSV 문자열로 반환
* DB에 적합한 CSV 문자열로 최종 저장합니다.
* @param mixed $role 입력 데이터 (문자열 또는 배열)
*/ */
public function setRole(mixed $role) public function setRole($role): string
{ {
$roleArray = []; $roleArray = [];
// 입력된 데이터가 문자열인 경우에만 trim 및 explode 처리
if (is_string($role)) { if (is_string($role)) {
// trim()은 여기서 안전하게 호출됩니다. $clean = trim($role, " \t\n\r\0\x0B\"");
$cleanRoleString = trim($role, " \t\n\r\0\x0B\""); if ($clean !== '') {
if (!empty($cleanRoleString)) { $decoded = json_decode($clean, true);
// 문자열을 구분자로 분리하여 배열로 만듭니다. if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $cleanRoleString); $roleArray = $decoded;
} else {
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $clean);
}
} }
} } elseif (is_array($role)) {
// 입력된 데이터가 이미 배열인 경우 (modify_process에서 $formDatas가 배열로 넘어옴)
elseif (is_array($role)) {
$roleArray = $role; $roleArray = $role;
} else {
$roleArray = [];
} }
// 배열의 각 요소를 정리. null이나 scalar 타입이 섞여있을 경우에 대비해 string으로 명시적 형변환 후 trim 수행
$cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $roleArray); $cleaned = array_map(
$roleArray = array_filter($cleanedRoles); fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
// 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다. $roleArray
// ✅ setter함수는 반드시 attributes에 저장 );
$this->attributes['role'] = implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
$roleArray = array_values(array_filter($cleaned, fn($v) => $v !== ''));
return implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
} }
} }

View File

@ -9,95 +9,119 @@ class UserEntity extends CommonEntity
{ {
const PK = Model::PK; const PK = Model::PK;
const TITLE = Model::TITLE; const TITLE = Model::TITLE;
protected array $nullableFields = [ protected array $nullableFields = [
'mobile', 'mobile',
// uid 같은 숫자 PK가 nullable이면 여기에 추가
]; ];
// ✅ role은 반드시 "문자열" 기본값 (DB 저장형)
protected $attributes = [ protected $attributes = [
'id' => '', 'id' => '',
'passwd' => '', 'passwd' => '',
'name' => "", 'name' => '',
'email' => "", 'email' => '',
'mobile' => null, 'mobile' => null,
'role' => "", 'role' => '', // ✅ array 금지
'status' => '', 'status' => '',
]; ];
public function __construct(array|null $data = null) public function __construct(array|null $data = null)
{ {
parent::__construct($data); parent::__construct($data);
} }
public function getID(): string public function getID(): string
{ {
return (string) $this->id; return (string) ($this->attributes['id'] ?? '');
} }
public function getPassword(): string public function getPassword(): string
{ {
return $this->passwd; return (string) ($this->attributes['passwd'] ?? '');
} }
/** /**
* 사용자의 역할을 배열 형태로 반환합니다. * role을 "배열" 반환 (DB에는 CSV/JSON/배열 무엇이든 복구)
* DB의 JSON 또는 CSV 형식 데이터를 모두 배열로 복구할 있는 로직을 포함합니다.
* @return array
*/ */
public function getRole(): array public function getRole(): array
{ {
$role = $this->role ?? null; $role = $this->attributes['role'] ?? null;
// 1. 이미 배열인 경우 (방어적 코딩)
if (is_array($role)) {
return array_filter($role);
}
// 2. 문자열 데이터인 경우 처리
if (is_string($role) && !empty($role)) {
// 2-a. JSON 디코딩 시도 (기존 DB의 JSON 형식 처리)
$decodedRole = json_decode($role, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decodedRole)) {
return $decodedRole;
}
// 2-b. JSON이 아니면 CSV로 가정하고 변환
$parts = explode(DEFAULTS["DELIMITER_COMMA"], $role);
// 각 요소의 불필요한 공백과 따옴표 제거. null 가능성에 대비해 string 형변환 추가
$cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $parts);
return array_filter($cleanedRoles);
}
// 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환
return [];
}
// --- Setter Methods ---
public function setPasswd(string|null $password = null) if (is_array($role)) {
{ return array_values(array_filter($role, fn($v) => (string) $v !== ''));
// 입력된 비밀번호가 null이 아니고 비어있지 않을 때만 해시 처리
if (!empty($password)) {
// ✅ setter함수는 반드시 attributes에 저장
$this->attributes['passwd'] = password_hash($password, PASSWORD_BCRYPT);
} }
if (is_string($role) && $role !== '') {
// JSON 시도
$decoded = json_decode($role, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$clean = array_map(
fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
$decoded
);
return array_values(array_filter($clean, fn($v) => $v !== ''));
}
// CSV fallback
$parts = explode(DEFAULTS["DELIMITER_COMMA"], $role);
$clean = array_map(
fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
$parts
);
return array_values(array_filter($clean, fn($v) => $v !== ''));
}
return [];
} }
/** /**
* Role 데이터가 Entity에 설정될 호출되어, 입력된 CSV/JSON 문자열을 정리 * CI4 뮤테이터: "return 값" attributes에 저장됨
* DB에 적합한 CSV 문자열로 최종 저장합니다. * - 빈값이면 기존값 유지 (create에서 required면 validate에서 걸러짐)
* @param mixed $role 입력 데이터 (문자열 또는 배열)
*/ */
public function setRole(mixed $role) public function setPasswd($password): string
{
// null/'' 이면 기존값 유지
if (!is_string($password) || $password === '') {
return (string) ($this->attributes['passwd'] ?? '');
}
return password_hash($password, PASSWORD_BCRYPT);
}
/**
* role은 최종적으로 "CSV 문자열" 저장 (DB 안전)
*/
public function setRole($role): string
{ {
$roleArray = []; $roleArray = [];
// 입력된 데이터가 문자열인 경우에만 trim 및 explode 처리
if (is_string($role)) { if (is_string($role)) {
// trim()은 여기서 안전하게 호출됩니다. $clean = trim($role, " \t\n\r\0\x0B\"");
$cleanRoleString = trim($role, " \t\n\r\0\x0B\""); if ($clean !== '') {
if (!empty($cleanRoleString)) { // JSON 문자열 가능성도 있어서 먼저 JSON 시도
// 문자열을 구분자로 분리하여 배열로 만듭니다. $decoded = json_decode($clean, true);
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $cleanRoleString); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$roleArray = $decoded;
} else {
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $clean);
}
} }
} } elseif (is_array($role)) {
// 입력된 데이터가 이미 배열인 경우 (modify_process에서 $formDatas가 배열로 넘어옴)
elseif (is_array($role)) {
$roleArray = $role; $roleArray = $role;
} else {
// 그 외 타입은 안전하게 빈값 처리
$roleArray = [];
} }
// 배열의 각 요소를 정리. null이나 scalar 타입이 섞여있을 경우에 대비해 string으로 명시적 형변환 후 trim 수행
$cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $roleArray); $cleaned = array_map(
$roleArray = array_filter($cleanedRoles); fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
// 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다. $roleArray
// ✅ setter함수는 반드시 attributes에 저장 );
$this->attributes['role'] = implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
$roleArray = array_values(array_filter($cleaned, fn($v) => $v !== ''));
// ✅ 무조건 문자열 반환 (빈 배열이면 '')
return implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
} }
} }

View File

@ -4,9 +4,20 @@ namespace App\Forms;
use RuntimeException; use RuntimeException;
/**
* CommonForm
* - 모든 Form의 공통 베이스
* - 핵심 개선점:
* 1) FK/숫자 필드 미입력('') NULL로 정규화 ('' -> null)
* 2) 전역 null -> '' 변환 제거 (FK/숫자/날짜 타입 깨짐 방지)
* 3) validate()에서 dynamicRules 누적 버그 수정 (마지막 규칙만 남는 문제 해결)
* 4) "필드 존재 보장"으로 임의 '' 삽입 제거 (미입력 필드가 FK/숫자 규칙을 깨는 문제 방지)
* 5) role.* 같은 배열 원소 규칙을 위해 부모 배열 보정 로직 유지/강화
*/
abstract class CommonForm abstract class CommonForm
{ {
private $_validation = null; private $_validation = null;
private array $_attributes = []; private array $_attributes = [];
private array $_formFields = []; private array $_formFields = [];
private array $_formRules = []; private array $_formRules = [];
@ -16,10 +27,12 @@ abstract class CommonForm
private array $_formOptions = []; private array $_formOptions = [];
private array $_actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']]; private array $_actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
private array $_batchjobButtons = ['batchjob' => '일괄처리', 'batchjob_delete' => '일괄삭제']; private array $_batchjobButtons = ['batchjob' => '일괄처리', 'batchjob_delete' => '일괄삭제'];
protected function __construct() protected function __construct()
{ {
$this->_validation = service('validation'); $this->_validation = service('validation');
} }
public function action_init_process(string $action, array &$formDatas = []): void public function action_init_process(string $action, array &$formDatas = []): void
{ {
$actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']]; $actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
@ -27,10 +40,12 @@ abstract class CommonForm
$this->setActionButtons($actionButtons); $this->setActionButtons($actionButtons);
$this->setBatchjobButtons($batchjobButtons); $this->setBatchjobButtons($batchjobButtons);
} }
final public function setAttributes(array $attributes): void final public function setAttributes(array $attributes): void
{ {
$this->_attributes = $attributes; $this->_attributes = $attributes;
} }
final public function getAttribute(string $key): string final public function getAttribute(string $key): string
{ {
if (!array_key_exists($key, $this->_attributes)) { if (!array_key_exists($key, $this->_attributes)) {
@ -38,21 +53,23 @@ abstract class CommonForm
} }
return $this->_attributes[$key]; return $this->_attributes[$key];
} }
final public function setFormFields(array $fields): void final public function setFormFields(array $fields): void
{ {
foreach ($fields as $field) { foreach ($fields as $field) {
$this->_formFields[$field] = $this->getFormFieldLabel($field); $this->_formFields[$field] = $this->getFormFieldLabel($field);
} }
} }
//$fields 매치된것만 반환, []->전체
// $fields 매치된것만 반환, []->전체
final public function getFormFields(array $fields = []): array final public function getFormFields(array $fields = []): array
{ {
if (empty($fields)) { if (empty($fields)) {
return $this->_formFields; return $this->_formFields;
} }
// _formFields와 키를 비교하여 교집합을 반환합니다. $fields에 지정된 필드 정의만 추출됩니다.
return array_intersect_key($this->_formFields, array_flip($fields)); return array_intersect_key($this->_formFields, array_flip($fields));
} }
public function setFormRules(string $action, array $fields, $formRules = []): void public function setFormRules(string $action, array $fields, $formRules = []): void
{ {
foreach ($fields as $field) { foreach ($fields as $field) {
@ -60,6 +77,7 @@ abstract class CommonForm
} }
$this->_formRules = $formRules; $this->_formRules = $formRules;
} }
final public function getFormRules(array $fields = []): array final public function getFormRules(array $fields = []): array
{ {
if (empty($fields)) { if (empty($fields)) {
@ -67,6 +85,7 @@ abstract class CommonForm
} }
return array_intersect_key($this->_formRules, array_flip($fields)); return array_intersect_key($this->_formRules, array_flip($fields));
} }
final public function setFormOptions(string $action, array $fields, array $formDatas = [], $formOptions = []): void final public function setFormOptions(string $action, array $fields, array $formDatas = [], $formOptions = []): void
{ {
foreach ($fields as $field) { foreach ($fields as $field) {
@ -74,7 +93,8 @@ abstract class CommonForm
} }
$this->_formOptions = $formOptions; $this->_formOptions = $formOptions;
} }
//$fields 매치된것만 반환, []->전체
// $fields 매치된것만 반환, []->전체
final public function getFormOptions(array $fields = []): array final public function getFormOptions(array $fields = []): array
{ {
if (empty($fields)) { if (empty($fields)) {
@ -82,163 +102,285 @@ abstract class CommonForm
} }
return array_intersect_key($this->_formOptions, array_flip($fields)); return array_intersect_key($this->_formOptions, array_flip($fields));
} }
final public function setFormFilters(array $fields): void final public function setFormFilters(array $fields): void
{ {
$this->_formFilters = $fields; $this->_formFilters = $fields;
} }
final public function getFormFilters(): array final public function getFormFilters(): array
{ {
return $this->_formFilters; return $this->_formFilters;
} }
final public function setIndexFilters(array $fields): void final public function setIndexFilters(array $fields): void
{ {
$this->_indexFilters = $fields; $this->_indexFilters = $fields;
;
} }
final public function getIndexFilters(): array final public function getIndexFilters(): array
{ {
return $this->_indexFilters; return $this->_indexFilters;
} }
final public function setBatchjobFilters(array $fields): void final public function setBatchjobFilters(array $fields): void
{ {
$this->_batchjobFilters = $fields; $this->_batchjobFilters = $fields;
;
} }
final public function getBatchjobFilters(): array final public function getBatchjobFilters(): array
{ {
return $this->_batchjobFilters; return $this->_batchjobFilters;
} }
final public function setActionButtons(array $buttons): array final public function setActionButtons(array $buttons): array
{ {
return $this->_actionButtons = $buttons; return $this->_actionButtons = $buttons;
} }
final public function getActionButtons(): array final public function getActionButtons(): array
{ {
return $this->_actionButtons; return $this->_actionButtons;
} }
final public function setBatchjobButtons(array $buttons): array final public function setBatchjobButtons(array $buttons): array
{ {
return $this->_batchjobButtons = $buttons; return $this->_batchjobButtons = $buttons;
} }
final public function getBatchjobButtons(): array final public function getBatchjobButtons(): array
{ {
return $this->_batchjobButtons; return $this->_batchjobButtons;
} }
//Validation용
/**
* 데이터를 검증하고 유효하지 않을 경우 예외를 발생시킵니다.
* 2025 CI4 표준: 규칙 배열 내에 label을 포함하여 한글 메시지 출력을 보장합니다.
*
* @param array $formDatas 검증할 데이터
* @throws RuntimeException
*/
/* ---------------------------------------------------------------------
* Normalize / Sanitize
* --------------------------------------------------------------------- */
/**
* 1) 깊은 배열 구조 정리(배열은 유지)
* - 여기서는 null -> '' 같은 변환을 절대 하지 않습니다.
* - 이유: FK/숫자/날짜 필드가 '' 변하면 validation/DB에서 문제가 발생함.
*/
protected function sanitizeFormDatas($data, string $path = '') protected function sanitizeFormDatas($data, string $path = '')
{ {
if (!is_array($data)) if (!is_array($data)) {
return $data; return $data;
}
foreach ($data as $k => $v) { foreach ($data as $k => $v) {
if (is_array($v)) { if (is_array($v)) {
$data[$k] = $this->sanitizeFormDatas($v, ($path !== '' ? "$path.$k" : (string) $k)); $data[$k] = $this->sanitizeFormDatas($v, ($path !== '' ? "{$path}.{$k}" : (string) $k));
} elseif ($v === null) {
$data[$k] = '';
} }
} }
return $data; return $data;
} }
/**
* 2) 숫자/FK 필드 정규화
* - 폼에서 미선택은 보통 '' 들어옴 -> NULL로 변환
* - 숫자 문자열은 int 캐스팅 (선택)
*
* 주의:
* - "빈값을 0으로 취급" 같은 정책이 있다면 여기에서 조정해야 .
*/
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) 핵심: 배열 원소의 null/'' 제거 + 문자열화(Trim이 null 받지 않도록)
$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)" 취급할 필드를 수집
* - getFormRule()에서 permit_empty|numeric 정의되는 필드를 공통 처리하기 위함
*
* 구현 전략:
* - formRules에서 rule 문자열에 'numeric' 포함된 필드를 모음
* - wildcard(role.*) 제외
*/
protected function collectNumericFieldsFromRules(array $formRules): array
{
$numericFields = [];
foreach ($formRules as $field => $rule) {
$fieldName = (string) $field;
if (str_contains($fieldName, '.*')) {
continue;
}
// getValidationRule 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
* --------------------------------------------------------------------- */
/**
* 데이터를 검증하고 유효하지 않을 경우 예외를 발생시킵니다.
* 2025 CI4 표준: 규칙 배열 내에 label을 포함하여 한글 메시지 출력을 보장합니다.
*/
final public function validate(array &$formDatas): void final public function validate(array &$formDatas): void
{ {
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class);
if ($this->_validation === null) { if ($this->_validation === null) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Validation 서비스가 초기화되지 않았습니다."); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Validation 서비스가 초기화되지 않았습니다.");
} }
try { try {
//목적은 검증 전에 데이터 형태를 안전하게 정리 // 0) 데이터 구조 정리 (null 변환 X)
$formDatas = $this->sanitizeFormDatas($formDatas); $formDatas = $this->sanitizeFormDatas($formDatas);
// 1. 필드 라벨/규칙
// 1) 필드 라벨/규칙
$formFields = $this->getFormFields(); $formFields = $this->getFormFields();
$formRules = $this->getFormRules(); $formRules = $this->getFormRules();
if (empty($formRules)) { if (empty($formRules)) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다."); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
} }
// ✅ 1.5 배열 원소 규칙(role.*)이 존재할 경우, 부모 필드(role)가 없으면 빈 배열로 보장 // 2) wildcard(role.*) 부모 배열 보정
// 이렇게 해두면 엔진 내부에서 role.* 처리 중 trim(null) 류가 재발할 확률이 확 줄어듦 $this->ensureParentArrayForWildcardRules($formDatas, $formRules);
foreach ($formRules as $fieldKey => $ruleDef) {
$fieldName = is_array($ruleDef) ? $fieldKey : $fieldKey;
// role.* 형태면 부모 role을 배열로 보장 // 3) numeric(FK 포함) 필드: '' -> null, 숫자 문자열 -> int
if (str_contains($fieldName, '.*')) { // (규칙 기반 자동 수집)
$parent = str_replace('.*', '', $fieldName); $numericFields = $this->collectNumericFieldsFromRules($formRules);
if (!array_key_exists($parent, $formDatas) || $formDatas[$parent] === '') { $formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields);
$formDatas[$parent] = [];
} elseif (is_string($formDatas[$parent])) {
// 혹시 문자열로 들어오면 CSV → 배열 복원 (공통 방어막)
$formDatas[$parent] = ($formDatas[$parent] === '') ? [] : explode(DEFAULTS["DELIMITER_COMMA"], $formDatas[$parent]);
} elseif (!is_array($formDatas[$parent])) {
$formDatas[$parent] = [];
}
}
}
// 4) dynamicRules 누적 구성 (버그 수정: 루프마다 초기화 금지)
$dynamicRules = [];
foreach ($formRules as $field => $rule) { foreach ($formRules as $field => $rule) {
try { try {
// 2. 필드명/규칙 추출 // 필드명/규칙 추출(확장 포인트)
list($field, $rule) = $this->getValidationRule($field, $rule); [$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
// 3. label 결정 // label 결정
if (isset($formFields[$field])) { if (isset($formFields[$fieldName])) {
$label = $formFields[$field]; $label = $formFields[$fieldName];
} elseif (str_contains($field, '.*')) { } elseif (str_contains($fieldName, '.*')) {
$parentField = str_replace('.*', '', $field); $parentField = str_replace('.*', '', $fieldName);
$label = ($formFields[$parentField] ?? $field) . " 항목"; $label = ($formFields[$parentField] ?? $fieldName) . " 항목";
} else { } else {
$label = $field; $label = $fieldName;
} }
// 4. rules 설정 $dynamicRules[$fieldName] = [
$dynamicRules = [];
$dynamicRules[$field] = [
'label' => $label, 'label' => $label,
'rules' => $rule 'rules' => $ruleStr,
]; ];
// ✅ 4.5 존재 보장 로직 수정 // ❌ 존재 보장으로 '' 삽입하지 않음
// - 일반 필드: 없으면 '' 세팅 // - required는 CI4가 "키 없음"도 실패 처리 가능(일반적으로)
// - 배열 원소 필드(role.*): 여기서 만들면 안 됨 (부모 role에서 처리해야 함) // - permit_empty는 키 없어도 통과 (강제로 '' 만들면 FK/숫자 문제 발생)
if (!array_key_exists($field, $formDatas) && !str_contains($field, '.*')) {
$formDatas[$field] = '';
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw new RuntimeException("유효성 검사 규칙 준비 중 오류 발생 (필드: {$field}): " . $e->getMessage()); throw new RuntimeException("유효성 검사 규칙 준비 중 오류 발생 (필드: {$field}): " . $e->getMessage());
} }
} }
$this->_validation->setRules($dynamicRules); $this->_validation->setRules($dynamicRules);
try { try {
if (!$this->_validation->run($formDatas)) { if (!$this->_validation->run($formDatas)) {
$errors = $this->_validation->getErrors(); $errors = $this->_validation->getErrors();
throw new RuntimeException(implode("\n", $errors)); throw new RuntimeException(implode("\n", $errors));
} }
} catch (\TypeError $e) { } catch (\TypeError $e) {
// 너의 상세 디버깅 로직은 그대로 둬도 됨 (생략 가능)
throw new RuntimeException("검증 도중 타입 오류 발생: " . $e->getMessage()); throw new RuntimeException("검증 도중 타입 오류 발생: " . $e->getMessage());
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($e instanceof RuntimeException) if ($e instanceof RuntimeException) {
throw $e; throw $e;
}
throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage()); throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
} }
} }
/* ---------------------------------------------------------------------
* Overridable hooks
* --------------------------------------------------------------------- */
//필수함수 // 사용자 정의 hook: 필드/룰 커스터마이즈
//사용자정의 함수
protected function getValidationRule(string $field, string $rule): array protected function getValidationRule(string $field, string $rule): array
{ {
return array($field, $rule); return [$field, $rule];
} }
public function getFormFieldLabel(string $field, ?string $label = null): string public function getFormFieldLabel(string $field, ?string $label = null): string
@ -251,22 +393,35 @@ abstract class CommonForm
return $label; return $label;
} }
/**
* Form rule 정의
* - permit_empty|numeric FK들이 여기서 정의되면,
* validate()에서 자동으로 ''->null 정규화 대상에 포함됩니다.
*/
public function getFormRule(string $action, string $field, array $formRules): array public function getFormRule(string $action, string $field, array $formRules): array
{ {
switch ($field) { switch ($field) {
case $this->getAttribute('pk_field'): case $this->getAttribute('pk_field'):
if (!$this->getAttribute('useAutoIncrement')) { if (!$this->getAttribute('useAutoIncrement')) {
$formRules[$field] = sprintf("required|regex_match[/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/]%s", in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""); $formRules[$field] = sprintf(
"required|regex_match[/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/]%s",
in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
);
} else { } else {
$formRules[$field] = "required|numeric"; $formRules[$field] = "required|numeric";
} }
break; break;
case $this->getAttribute('title_field'): case $this->getAttribute('title_field'):
$formRules[$field] = sprintf("required|trim|string%s", in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""); $formRules[$field] = sprintf(
"required|trim|string%s",
in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
);
break; break;
case "code": case "code":
// a-zA-Z → 영문 대소문자,0-9 → 숫자,가-힣 → 한글 완성형,\- → 하이픈 $formRules[$field] = sprintf(
$formRules[$field] = sprintf("required|regex_match[/^[a-zA-Z0-9가-힣\-\_]+$/]|min_length[4]%s", in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""); "required|regex_match[/^[a-zA-Z0-9가-힣\-\_]+$/]|min_length[4]%s",
in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
);
break; break;
case "user_uid": case "user_uid":
$formRules[$field] = "required|numeric"; $formRules[$field] = "required|numeric";
@ -288,11 +443,18 @@ abstract class CommonForm
$formRules[$field] = "permit_empty|trim|string"; $formRules[$field] = "permit_empty|trim|string";
break; break;
} }
return $formRules; return $formRules;
} }
/* ---------------------------------------------------------------------
* Options
* --------------------------------------------------------------------- */
protected function getFormOption_process($service, string $action, string $field, array $formDatas = []): array protected function getFormOption_process($service, string $action, string $field, array $formDatas = []): array
{ {
$entities = []; $entities = [];
switch ($field) { switch ($field) {
default: default:
if (in_array($action, ['create_form', 'modify_form', 'alternative_create_form'])) { if (in_array($action, ['create_form', 'modify_form', 'alternative_create_form'])) {
@ -307,12 +469,14 @@ abstract class CommonForm
} }
break; break;
} }
return $entities; return $entities;
} }
public function getFormOption(string $action, string $field, array $formDatas = [], array $options = ['options' => [], 'atttributes' => []]): array public function getFormOption(string $action, string $field, array $formDatas = [], array $options = ['options' => [], 'atttributes' => []]): array
{ {
$tempOptions = ['' => lang("{$this->getAttribute('class_path')}.label.{$field}") . " 선택"]; $tempOptions = ['' => lang("{$this->getAttribute('class_path')}.label.{$field}") . " 선택"];
switch ($field) { switch ($field) {
case 'user_uid': case 'user_uid':
foreach ($this->getFormOption_process(service('userservice'), $action, $field, $formDatas) as $entity) { foreach ($this->getFormOption_process(service('userservice'), $action, $field, $formDatas) as $entity) {
@ -320,12 +484,14 @@ abstract class CommonForm
} }
$options['options'] = $tempOptions; $options['options'] = $tempOptions;
break; break;
case 'clientinfo_uid': case 'clientinfo_uid':
foreach ($this->getFormOption_process(service('customer_clientservice'), $action, $field, $formDatas) as $entity) { foreach ($this->getFormOption_process(service('customer_clientservice'), $action, $field, $formDatas) as $entity) {
$tempOptions[$entity->getPK()] = $entity->getCustomTitle(); $tempOptions[$entity->getPK()] = $entity->getCustomTitle();
} }
$options['options'] = $tempOptions; $options['options'] = $tempOptions;
break; break;
default: default:
$optionDatas = lang($this->getAttribute('class_path') . "." . strtoupper($field)); $optionDatas = lang($this->getAttribute('class_path') . "." . strtoupper($field));
if (!is_array($optionDatas)) { if (!is_array($optionDatas)) {
@ -337,6 +503,7 @@ abstract class CommonForm
$options['options'] = $tempOptions; $options['options'] = $tempOptions;
break; break;
} }
return $options; return $options;
} }
} }

View File

@ -16,6 +16,7 @@ class ClientForm extends CustomerForm
'email', 'email',
'phone', 'phone',
'role', 'role',
'status',
]; ];
$filters = [ $filters = [
'site', 'site',
@ -28,10 +29,6 @@ class ClientForm extends CustomerForm
case 'create': case 'create':
case 'create_form': case 'create_form':
break; break;
case 'modify':
case 'modify_form':
$fields = [...$fields, 'status'];
break;
case 'view': case 'view':
$fields = [...$fields, 'status', 'created_at']; $fields = [...$fields, 'status', 'created_at'];
break; break;
@ -69,8 +66,8 @@ class ClientForm extends CustomerForm
$formRules[$field] = "required|trim|string"; $formRules[$field] = "required|trim|string";
break; break;
case "role": case "role":
$formRules[$field] = 'required|is_array'; $formRules[$field] = 'required|is_array|at_least_one';
$formRules['role.*'] = 'trim|in_list[user,vip,reseller]'; $formRules['role.*'] = 'permit_empty|trim|in_list[user,vip,reseller]';
break; break;
case "email": case "email":
$formRules[$field] = "permit_empty|trim|valid_email"; $formRules[$field] = "permit_empty|trim|valid_email";
@ -84,6 +81,9 @@ class ClientForm extends CustomerForm
case "point_balance": case "point_balance":
$formRules[$field] = "permit_empty|numeric"; $formRules[$field] = "permit_empty|numeric";
break; break;
case "status":
$formRules[$field] = "required|trim|string";
break;
default: default:
$formRules = parent::getFormRule($action, $field, $formRules); $formRules = parent::getFormRule($action, $field, $formRules);
break; break;

View File

@ -19,7 +19,8 @@ class UserForm extends CommonForm
'name', 'name',
'email', 'email',
'mobile', 'mobile',
'role' 'role',
'status'
]; ];
$filters = ['role', 'status']; $filters = ['role', 'status'];
$indexFilter = $filters; $indexFilter = $filters;
@ -28,10 +29,6 @@ class UserForm extends CommonForm
case 'create': case 'create':
case 'create_form': case 'create_form':
break; break;
case 'modify':
case 'modify_form':
$fields = [...$fields, 'status'];
break;
case 'view': case 'view':
$fields = ['id', 'name', 'email', 'mobile', 'role', 'status', 'created_at']; $fields = ['id', 'name', 'email', 'mobile', 'role', 'status', 'created_at'];
break; break;
@ -63,8 +60,11 @@ 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|is_array'; $formRules[$field] = 'required|is_array|at_least_one';
$formRules['role.*'] = 'trim|in_list[manager,cloudflare,firewall,security,director,master]'; $formRules['role.*'] = 'permit_empty|trim|in_list[manager,cloudflare,firewall,security,director,master]';
break;
case "status":
$formRules[$field] = "required|trim|string";
break; break;
default: default:
$formRules = parent::getFormRule($action, $field, $formRules); $formRules = parent::getFormRule($action, $field, $formRules);

View File

@ -4,35 +4,33 @@
return [ return [
// 여기서부터 각 Validation rule에 대한 메시지를 정의합니다. // 여기서부터 각 Validation rule에 대한 메시지를 정의합니다.
// {field}나 {param} 같은 플레이스홀더는 그대로 유지해야 합니다. // {field}나 {param} 같은 플레이스홀더는 그대로 유지해야 합니다.
'required' => '[{field}] 필수 입력 항목입니다.',
'required' => '[{field}] 필수 입력 항목입니다.', 'isset' => '[{field}] 값이 반드시 있어야 합니다.',
'isset' => '[{field}] 값이 반드시 있어야 합니다.', 'valid_email' => '[{field}] 유효한 이메일 주소여야 합니다.',
'valid_email' => '[{field}] 유효한 이메일 주소여야 합니다.', 'valid_url' => '[{field}] 유효한 URL이어야 합니다.',
'valid_url' => '[{field}] 유효한 URL이어야 합니다.', 'valid_date' => '[{field}] 유효한 날짜여야 합니다.',
'valid_date' => '[{field}] 유효한 날짜여야 합니다.', 'valid_dates' => '[{field}] 유효한 날짜여야 합니다.',
'valid_dates' => '[{field}] 유효한 날짜여야 합니다.', 'valid_ip' => '[{field}] 유효한 IP 주소여야 합니다.',
'valid_ip' => '[{field}] 유효한 IP 주소여야 합니다.', 'valid_mac' => '[{field}] 유효한 MAC 주소여야 합니다.',
'valid_mac' => '[{field}] 유효한 MAC 주소여야 합니다.', 'numeric' => '[{field}] 숫자만 포함해야 합니다.',
'numeric' => '[{field}] 숫자만 포함해야 합니다.', 'integer' => '[{field}] 정수여야 합니다.',
'integer' => '[{field}] 정수여야 합니다.', 'decimal' => '[{field}] 소수점 숫자여야 합니다.',
'decimal' => '[{field}] 소수점 숫자여야 합니다.', 'is_numeric' => '[{field}] 숫자 문자만 포함해야 합니다.',
'is_numeric' => '[{field}] 숫자 문자만 포함해야 합니다.', 'regex_match' => '[{field}] 올바른 형식이어야 합니다.',
'regex_match' => '[{field}] 올바른 형식이어야 합니다.', 'matches' => '{field} 필드가 {param} 필드와 일치하지 않습니다.',
'matches' => '{field} 필드가 {param} 필드와 일치하지 않습니다.', 'differs' => '[{field}] {param} 필드와 달라야 합니다.',
'differs' => '[{field}] {param} 필드와 달라야 합니다.', 'is_unique' => '[{field}] 고유한 값이어야 합니다.',
'is_unique' => '[{field}] 고유한 값이어야 합니다.', 'is_natural' => '[{field}] 숫자여야 합니다.',
'is_natural' => '[{field}] 숫자여야 합니다.', 'is_natural_no_zero' => '[{field}] 0보다 큰 숫자여야 합니다.',
'is_natural_no_zero' => '[{field}] 0보다 큰 숫자여야 합니다.', 'less_than' => '[{field}] {param}보다 작아야 합니다.',
'less_than' => '[{field}] {param}보다 작아야 합니다.', 'less_than_equal_to' => '[{field}] {param}보다 작거나 같아야 합니다.',
'less_than_equal_to' => '[{field}] {param}보다 작거나 같아야 합니다.', 'greater_than' => '[{field}] {param}보다 커야 합니다.',
'greater_than' => '[{field}] {param}보다 커야 합니다.', 'greater_than_equal_to' => '[{field}] {param}보다 크거나 같아야 합니다.',
'greater_than_equal_to' => '[{field}] {param}보다 크거나 같아야 합니다.', 'error_prefix' => '',
'error_prefix' => '', 'error_suffix' => '',
'error_suffix' => '', 'min_length' => '[{field}] 최소 {param}자 이상이어야 합니다.',
'max_length' => '[{field}] 최대 {param}자 이하여야 합니다.',
// 길이(Length) 관련 rule 메시지 'exact_length' => '[{field}] 정확히 {param}자여야 합니다.',
'min_length' => '[{field}] 최소 {param}자 이상이어야 합니다.', 'in_list' => '[{field}] 다음 중 하나여야 합니다: {param}.',
'max_length' => '[{field}] 최대 {param}자 이하여야 합니다.', 'at_least_one' => '{field} 최소 1개 이상 선택해야 합니다.',
'exact_length' => '[{field}] 정확히 {param}자여야 합니다.',
'in_list' => '[{field}] 다음 중 하나여야 합니다: {param}.',
]; ];

View File

@ -2,17 +2,14 @@
namespace App\Services; namespace App\Services;
use App\Forms\CommonForm;
use App\DTOs\CommonDTO; use App\DTOs\CommonDTO;
use App\Entities\CommonEntity; use App\Entities\CommonEntity;
use App\Libraries\AuthContext;
use App\Models\CommonModel; use App\Models\CommonModel;
use App\Libraries\AuthContext;
use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DatabaseException;
use RuntimeException; use RuntimeException;
/**
* @template TEntity of CommonEntity
* @template TDto of CommonDTO
*/
abstract class CommonService abstract class CommonService
{ {
private ?AuthContext $_authContext = null; private ?AuthContext $_authContext = null;
@ -239,6 +236,8 @@ abstract class CommonService
// INSERT 시 Entity의 PK는 0 또는 NULL이어야 함 (DB가 ID를 생성하도록) // INSERT 시 Entity의 PK는 0 또는 NULL이어야 함 (DB가 ID를 생성하도록)
$initialPK = $entity->getPK(); $initialPK = $entity->getPK();
$result = $this->model->save($entity); $result = $this->model->save($entity);
log_message('debug', __FUNCTION__ . ":" . var_export($entity, true));
log_message('debug', __FUNCTION__ . ":" . $this->model->getLastQuery());
// 최종적으로 DB에 반영된 PK를 반환받습니다. (UPDATE이면 기존 PK, INSERT이면 새 PK) // 최종적으로 DB에 반영된 PK를 반환받습니다. (UPDATE이면 기존 PK, INSERT이면 새 PK)
$entity->{$this->getPKField()} = $this->handle_save_result($result, $initialPK); $entity->{$this->getPKField()} = $this->handle_save_result($result, $initialPK);
// handle_save_result에서 확인된 최종 PK를 사용하여 DB에서 최신 엔티티를 가져옴 // handle_save_result에서 확인된 최종 PK를 사용하여 DB에서 최신 엔티티를 가져옴
@ -260,13 +259,18 @@ abstract class CommonService
protected function create_process(array $formDatas): CommonEntity protected function create_process(array $formDatas): CommonEntity
{ {
try { try {
log_message('debug', "*** ENTER" . __METHOD__ . " ***");
$actionForm = $this->getActionForm(); $actionForm = $this->getActionForm();
log_message('debug', 'FORMCLASS=' . $this->formClass . ' / FORMINST=' . (is_object($actionForm) ? get_class($actionForm) : 'NULL'));
log_message('debug', 'IS_COMMONFORM=' . (is_object($actionForm) && $actionForm instanceof CommonForm ? 'YES' : 'NO'));
if ($actionForm instanceof CommonForm) { if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('create', $formDatas); $actionForm->action_init_process('create', $formDatas);
foreach ($formDatas as $field => $value) { foreach ($formDatas as $field => $value) {
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas); $formDatas = $this->action_process_fieldhook($field, $value, $formDatas);
} }
$actionForm->validate($formDatas); log_message('debug', '>>> BEFORE validate: ' . get_class($actionForm));
$actionForm->validate($formDatas); // ✅ 여기서 검증
log_message('debug', '>>> AFTER validate');
} }
$entityClass = $this->getEntityClass(); $entityClass = $this->getEntityClass();
$entity = new $entityClass($formDatas); $entity = new $entityClass($formDatas);
@ -291,13 +295,18 @@ abstract class CommonService
protected function modify_process($entity, array $formDatas): CommonEntity protected function modify_process($entity, array $formDatas): CommonEntity
{ {
try { try {
log_message('debug', "*** ENTER" . __METHOD__ . " ***");
$actionForm = $this->getActionForm(); $actionForm = $this->getActionForm();
log_message('debug', 'FORMCLASS=' . $this->formClass . ' / FORMINST=' . (is_object($actionForm) ? get_class($actionForm) : 'NULL'));
log_message('debug', 'IS_COMMONFORM=' . (is_object($actionForm) && $actionForm instanceof CommonForm ? 'YES' : 'NO'));
if ($actionForm instanceof CommonForm) { if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('modify', $formDatas); $actionForm->action_init_process('modify', $formDatas);
foreach ($formDatas as $field => $value) { foreach ($formDatas as $field => $value) {
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas); $formDatas = $this->action_process_fieldhook($field, $value, $formDatas);
} }
log_message('debug', '>>> BEFORE validate: ' . get_class($actionForm));
$actionForm->validate($formDatas); // ✅ 여기서 검증 $actionForm->validate($formDatas); // ✅ 여기서 검증
log_message('debug', '>>> AFTER validate');
} }
// 검증 통과 후 엔티티 반영 // 검증 통과 후 엔티티 반영
$entity->fill($formDatas); $entity->fill($formDatas);

View File

@ -0,0 +1,32 @@
<?php
namespace App\Validation;
class CustomRules
{
public function at_least_one($value, ?string $params = null, array $data = []): bool
{
if (is_array($value)) {
$clean = array_values(array_filter(array_map(
fn($v) => is_scalar($v) ? trim((string) $v) : '',
$value
), fn($v) => $v !== ''));
return count($clean) >= 1;
}
if (is_string($value)) {
$v = trim($value);
if ($v === '')
return false;
$parts = array_values(array_filter(array_map(
fn($x) => trim((string) $x),
explode(DEFAULTS["DELIMITER_COMMA"], $v)
), fn($x) => $x !== ''));
return count($parts) >= 1;
}
return false;
}
}