dbmsv4 init...5
This commit is contained in:
parent
509cd0e999
commit
40b809949c
@ -25,6 +25,7 @@ class Validation extends BaseConfig
|
||||
FormatRules::class,
|
||||
FileRules::class,
|
||||
CreditCardRules::class,
|
||||
\App\Validation\CustomRules::class, // ✅ 추가
|
||||
];
|
||||
|
||||
/**
|
||||
@ -34,7 +35,7 @@ class Validation extends BaseConfig
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public array $templates = [
|
||||
'list' => 'CodeIgniter\Validation\Views\list',
|
||||
'list' => 'CodeIgniter\Validation\Views\list',
|
||||
'single' => 'CodeIgniter\Validation\Views\single',
|
||||
];
|
||||
|
||||
|
||||
@ -8,10 +8,13 @@ class ClientEntity extends CustomerEntity
|
||||
{
|
||||
const PK = ClientModel::PK;
|
||||
const TITLE = ClientModel::TITLE;
|
||||
|
||||
protected array $nullableFields = [
|
||||
'id',
|
||||
'passwd',
|
||||
];
|
||||
|
||||
// ✅ role은 반드시 string 기본값
|
||||
protected $attributes = [
|
||||
'id' => null,
|
||||
'passwd' => null,
|
||||
@ -19,13 +22,14 @@ class ClientEntity extends CustomerEntity
|
||||
'name' => '',
|
||||
'phone' => '',
|
||||
'email' => '',
|
||||
'role' => [],
|
||||
'role' => '', // ✅ [] 금지
|
||||
'account_balance' => 0,
|
||||
'coupon_balance' => 0,
|
||||
'point_balance' => 0,
|
||||
'status' => '',
|
||||
'history' => ''
|
||||
'history' => '',
|
||||
];
|
||||
|
||||
public function __construct(array|null $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
@ -35,91 +39,104 @@ class ClientEntity extends CustomerEntity
|
||||
{
|
||||
return $this->user_uid ?? null;
|
||||
}
|
||||
//기본기능
|
||||
|
||||
public function getCustomTitle(mixed $title = null): string
|
||||
{
|
||||
return sprintf("%s/%s", $this->getSite(), $title ? $title : $this->getTitle());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
return (string) ($this->attributes['name'] ?? '');
|
||||
}
|
||||
|
||||
public function getSite(): string
|
||||
{
|
||||
return $this->site;
|
||||
return (string) ($this->attributes['site'] ?? '');
|
||||
}
|
||||
|
||||
public function getAccountBalance(): int
|
||||
{
|
||||
return $this->account_balance ?? 0;
|
||||
return (int) ($this->attributes['account_balance'] ?? 0);
|
||||
}
|
||||
|
||||
public function getCouponBalance(): int
|
||||
{
|
||||
return $this->coupon_balance ?? 0;
|
||||
return (int) ($this->attributes['coupon_balance'] ?? 0);
|
||||
}
|
||||
|
||||
public function getPointBalance(): int
|
||||
{
|
||||
return $this->point_balance ?? 0;
|
||||
return (int) ($this->attributes['point_balance'] ?? 0);
|
||||
}
|
||||
|
||||
public function getHistory(): string|null
|
||||
{
|
||||
return $this->history;
|
||||
return $this->attributes['history'] ?? null;
|
||||
}
|
||||
/*
|
||||
* 사용자의 역할을 배열 형태로 반환합니다.
|
||||
* DB의 JSON 또는 CSV 형식 데이터를 모두 배열로 복구할 수 있는 로직을 포함합니다.
|
||||
* @return array
|
||||
|
||||
/**
|
||||
* role을 배열로 반환
|
||||
*/
|
||||
public function getRole(): array
|
||||
{
|
||||
$role = $this->role ?? null;
|
||||
// 1. 이미 배열인 경우 (방어적 코딩)
|
||||
$role = $this->attributes['role'] ?? null;
|
||||
|
||||
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)) {
|
||||
// 2-a. JSON 디코딩 시도 (기존 DB의 JSON 형식 처리)
|
||||
$decodedRole = json_decode($role, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decodedRole)) {
|
||||
return $decodedRole;
|
||||
|
||||
if (is_string($role) && $role !== '') {
|
||||
$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 !== ''));
|
||||
}
|
||||
// 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);
|
||||
$clean = array_map(
|
||||
fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
|
||||
$parts
|
||||
);
|
||||
return array_values(array_filter($clean, fn($v) => $v !== ''));
|
||||
}
|
||||
// 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환
|
||||
|
||||
return [];
|
||||
}
|
||||
// --- Setter Methods ---
|
||||
|
||||
/**
|
||||
* Role 데이터가 Entity에 설정될 때 호출되어, 입력된 CSV/JSON 문자열을 정리 후
|
||||
* DB에 적합한 CSV 문자열로 최종 저장합니다.
|
||||
* @param mixed $role 입력 데이터 (문자열 또는 배열)
|
||||
* ✅ role은 DB 저장용 CSV 문자열로 반환
|
||||
*/
|
||||
public function setRole(mixed $role)
|
||||
public function setRole($role): string
|
||||
{
|
||||
$roleArray = [];
|
||||
// 입력된 데이터가 문자열인 경우에만 trim 및 explode 처리
|
||||
|
||||
if (is_string($role)) {
|
||||
// trim()은 여기서 안전하게 호출됩니다.
|
||||
$cleanRoleString = trim($role, " \t\n\r\0\x0B\"");
|
||||
if (!empty($cleanRoleString)) {
|
||||
// 문자열을 구분자로 분리하여 배열로 만듭니다.
|
||||
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $cleanRoleString);
|
||||
$clean = trim($role, " \t\n\r\0\x0B\"");
|
||||
if ($clean !== '') {
|
||||
$decoded = json_decode($clean, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$roleArray = $decoded;
|
||||
} else {
|
||||
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 입력된 데이터가 이미 배열인 경우 (modify_process에서 $formDatas가 배열로 넘어옴)
|
||||
elseif (is_array($role)) {
|
||||
} elseif (is_array($role)) {
|
||||
$roleArray = $role;
|
||||
} else {
|
||||
$roleArray = [];
|
||||
}
|
||||
// 배열의 각 요소를 정리. null이나 scalar 타입이 섞여있을 경우에 대비해 string으로 명시적 형변환 후 trim 수행
|
||||
$cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $roleArray);
|
||||
$roleArray = array_filter($cleanedRoles);
|
||||
// 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다.
|
||||
// ✅ setter함수는 반드시 attributes에 저장
|
||||
$this->attributes['role'] = implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
|
||||
|
||||
$cleaned = array_map(
|
||||
fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
|
||||
$roleArray
|
||||
);
|
||||
|
||||
$roleArray = array_values(array_filter($cleaned, fn($v) => $v !== ''));
|
||||
|
||||
return implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,95 +9,119 @@ class UserEntity extends CommonEntity
|
||||
{
|
||||
const PK = Model::PK;
|
||||
const TITLE = Model::TITLE;
|
||||
|
||||
protected array $nullableFields = [
|
||||
'mobile',
|
||||
// uid 같은 숫자 PK가 nullable이면 여기에 추가
|
||||
];
|
||||
|
||||
// ✅ role은 반드시 "문자열" 기본값 (DB 저장형)
|
||||
protected $attributes = [
|
||||
'id' => '',
|
||||
'passwd' => '',
|
||||
'name' => "",
|
||||
'email' => "",
|
||||
'name' => '',
|
||||
'email' => '',
|
||||
'mobile' => null,
|
||||
'role' => "",
|
||||
'role' => '', // ✅ array 금지
|
||||
'status' => '',
|
||||
];
|
||||
|
||||
public function __construct(array|null $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
}
|
||||
|
||||
public function getID(): string
|
||||
{
|
||||
return (string) $this->id;
|
||||
return (string) ($this->attributes['id'] ?? '');
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->passwd;
|
||||
return (string) ($this->attributes['passwd'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 역할을 배열 형태로 반환합니다.
|
||||
* DB의 JSON 또는 CSV 형식 데이터를 모두 배열로 복구할 수 있는 로직을 포함합니다.
|
||||
* @return array
|
||||
* role을 "배열"로 반환 (DB에는 CSV/JSON/배열 무엇이든 복구)
|
||||
*/
|
||||
public function getRole(): array
|
||||
{
|
||||
$role = $this->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 ---
|
||||
$role = $this->attributes['role'] ?? null;
|
||||
|
||||
public function setPasswd(string|null $password = null)
|
||||
{
|
||||
// 입력된 비밀번호가 null이 아니고 비어있지 않을 때만 해시 처리
|
||||
if (!empty($password)) {
|
||||
// ✅ setter함수는 반드시 attributes에 저장
|
||||
$this->attributes['passwd'] = password_hash($password, PASSWORD_BCRYPT);
|
||||
if (is_array($role)) {
|
||||
return array_values(array_filter($role, fn($v) => (string) $v !== ''));
|
||||
}
|
||||
|
||||
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 문자열을 정리 후
|
||||
* DB에 적합한 CSV 문자열로 최종 저장합니다.
|
||||
* @param mixed $role 입력 데이터 (문자열 또는 배열)
|
||||
* ✅ CI4 뮤테이터: "return 값"이 attributes에 저장됨
|
||||
* - 빈값이면 기존값 유지 (create에서 required면 validate에서 걸러짐)
|
||||
*/
|
||||
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 = [];
|
||||
// 입력된 데이터가 문자열인 경우에만 trim 및 explode 처리
|
||||
|
||||
if (is_string($role)) {
|
||||
// trim()은 여기서 안전하게 호출됩니다.
|
||||
$cleanRoleString = trim($role, " \t\n\r\0\x0B\"");
|
||||
if (!empty($cleanRoleString)) {
|
||||
// 문자열을 구분자로 분리하여 배열로 만듭니다.
|
||||
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $cleanRoleString);
|
||||
$clean = trim($role, " \t\n\r\0\x0B\"");
|
||||
if ($clean !== '') {
|
||||
// JSON 문자열 가능성도 있어서 먼저 JSON 시도
|
||||
$decoded = json_decode($clean, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$roleArray = $decoded;
|
||||
} else {
|
||||
$roleArray = explode(DEFAULTS["DELIMITER_COMMA"], $clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 입력된 데이터가 이미 배열인 경우 (modify_process에서 $formDatas가 배열로 넘어옴)
|
||||
elseif (is_array($role)) {
|
||||
} elseif (is_array($role)) {
|
||||
$roleArray = $role;
|
||||
} else {
|
||||
// 그 외 타입은 안전하게 빈값 처리
|
||||
$roleArray = [];
|
||||
}
|
||||
// 배열의 각 요소를 정리. null이나 scalar 타입이 섞여있을 경우에 대비해 string으로 명시적 형변환 후 trim 수행
|
||||
$cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $roleArray);
|
||||
$roleArray = array_filter($cleanedRoles);
|
||||
// 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다.
|
||||
// ✅ setter함수는 반드시 attributes에 저장
|
||||
$this->attributes['role'] = implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
|
||||
|
||||
$cleaned = array_map(
|
||||
fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""),
|
||||
$roleArray
|
||||
);
|
||||
|
||||
$roleArray = array_values(array_filter($cleaned, fn($v) => $v !== ''));
|
||||
|
||||
// ✅ 무조건 문자열 반환 (빈 배열이면 '')
|
||||
return implode(DEFAULTS["DELIMITER_COMMA"], $roleArray);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,20 @@ namespace App\Forms;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* CommonForm
|
||||
* - 모든 Form의 공통 베이스
|
||||
* - 핵심 개선점:
|
||||
* 1) FK/숫자 필드 미입력('')을 NULL로 정규화 ('' -> null)
|
||||
* 2) 전역 null -> '' 변환 제거 (FK/숫자/날짜 타입 깨짐 방지)
|
||||
* 3) validate()에서 dynamicRules 누적 버그 수정 (마지막 규칙만 남는 문제 해결)
|
||||
* 4) "필드 존재 보장"으로 임의 '' 삽입 제거 (미입력 필드가 FK/숫자 규칙을 깨는 문제 방지)
|
||||
* 5) role.* 같은 배열 원소 규칙을 위해 부모 배열 보정 로직 유지/강화
|
||||
*/
|
||||
abstract class CommonForm
|
||||
{
|
||||
private $_validation = null;
|
||||
|
||||
private array $_attributes = [];
|
||||
private array $_formFields = [];
|
||||
private array $_formRules = [];
|
||||
@ -16,10 +27,12 @@ abstract class CommonForm
|
||||
private array $_formOptions = [];
|
||||
private array $_actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
|
||||
private array $_batchjobButtons = ['batchjob' => '일괄처리', 'batchjob_delete' => '일괄삭제'];
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$this->_validation = service('validation');
|
||||
}
|
||||
|
||||
public function action_init_process(string $action, array &$formDatas = []): void
|
||||
{
|
||||
$actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
|
||||
@ -27,10 +40,12 @@ abstract class CommonForm
|
||||
$this->setActionButtons($actionButtons);
|
||||
$this->setBatchjobButtons($batchjobButtons);
|
||||
}
|
||||
|
||||
final public function setAttributes(array $attributes): void
|
||||
{
|
||||
$this->_attributes = $attributes;
|
||||
}
|
||||
|
||||
final public function getAttribute(string $key): string
|
||||
{
|
||||
if (!array_key_exists($key, $this->_attributes)) {
|
||||
@ -38,21 +53,23 @@ abstract class CommonForm
|
||||
}
|
||||
return $this->_attributes[$key];
|
||||
}
|
||||
|
||||
final public function setFormFields(array $fields): void
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$this->_formFields[$field] = $this->getFormFieldLabel($field);
|
||||
}
|
||||
}
|
||||
//$fields 매치된것만 반환, []->전체
|
||||
|
||||
// $fields 매치된것만 반환, []->전체
|
||||
final public function getFormFields(array $fields = []): array
|
||||
{
|
||||
if (empty($fields)) {
|
||||
return $this->_formFields;
|
||||
}
|
||||
// _formFields와 키를 비교하여 교집합을 반환합니다. $fields에 지정된 필드 정의만 추출됩니다.
|
||||
return array_intersect_key($this->_formFields, array_flip($fields));
|
||||
}
|
||||
|
||||
public function setFormRules(string $action, array $fields, $formRules = []): void
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
@ -60,6 +77,7 @@ abstract class CommonForm
|
||||
}
|
||||
$this->_formRules = $formRules;
|
||||
}
|
||||
|
||||
final public function getFormRules(array $fields = []): array
|
||||
{
|
||||
if (empty($fields)) {
|
||||
@ -67,6 +85,7 @@ abstract class CommonForm
|
||||
}
|
||||
return array_intersect_key($this->_formRules, array_flip($fields));
|
||||
}
|
||||
|
||||
final public function setFormOptions(string $action, array $fields, array $formDatas = [], $formOptions = []): void
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
@ -74,7 +93,8 @@ abstract class CommonForm
|
||||
}
|
||||
$this->_formOptions = $formOptions;
|
||||
}
|
||||
//$fields 매치된것만 반환, []->전체
|
||||
|
||||
// $fields 매치된것만 반환, []->전체
|
||||
final public function getFormOptions(array $fields = []): array
|
||||
{
|
||||
if (empty($fields)) {
|
||||
@ -82,163 +102,285 @@ abstract class CommonForm
|
||||
}
|
||||
return array_intersect_key($this->_formOptions, array_flip($fields));
|
||||
}
|
||||
|
||||
final public function setFormFilters(array $fields): void
|
||||
{
|
||||
$this->_formFilters = $fields;
|
||||
}
|
||||
|
||||
final public function getFormFilters(): array
|
||||
{
|
||||
return $this->_formFilters;
|
||||
}
|
||||
|
||||
final public function setIndexFilters(array $fields): void
|
||||
{
|
||||
$this->_indexFilters = $fields;
|
||||
;
|
||||
}
|
||||
|
||||
final public function getIndexFilters(): array
|
||||
{
|
||||
return $this->_indexFilters;
|
||||
}
|
||||
|
||||
final public function setBatchjobFilters(array $fields): void
|
||||
{
|
||||
$this->_batchjobFilters = $fields;
|
||||
;
|
||||
}
|
||||
|
||||
final public function getBatchjobFilters(): array
|
||||
{
|
||||
return $this->_batchjobFilters;
|
||||
}
|
||||
|
||||
final public function setActionButtons(array $buttons): array
|
||||
{
|
||||
return $this->_actionButtons = $buttons;
|
||||
}
|
||||
|
||||
final public function getActionButtons(): array
|
||||
{
|
||||
return $this->_actionButtons;
|
||||
}
|
||||
|
||||
final public function setBatchjobButtons(array $buttons): array
|
||||
{
|
||||
return $this->_batchjobButtons = $buttons;
|
||||
}
|
||||
|
||||
final public function getBatchjobButtons(): array
|
||||
{
|
||||
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 = '')
|
||||
{
|
||||
if (!is_array($data))
|
||||
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));
|
||||
} elseif ($v === null) {
|
||||
$data[$k] = '';
|
||||
$data[$k] = $this->sanitizeFormDatas($v, ($path !== '' ? "{$path}.{$k}" : (string) $k));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class);
|
||||
if ($this->_validation === null) {
|
||||
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Validation 서비스가 초기화되지 않았습니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
//목적은 검증 전에 데이터 형태를 안전하게 정리
|
||||
// 0) 데이터 구조 정리 (null 변환 X)
|
||||
$formDatas = $this->sanitizeFormDatas($formDatas);
|
||||
// 1. 필드 라벨/규칙
|
||||
|
||||
// 1) 필드 라벨/규칙
|
||||
$formFields = $this->getFormFields();
|
||||
$formRules = $this->getFormRules();
|
||||
|
||||
if (empty($formRules)) {
|
||||
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
|
||||
}
|
||||
|
||||
// ✅ 1.5 배열 원소 규칙(role.*)이 존재할 경우, 부모 필드(role)가 없으면 빈 배열로 보장
|
||||
// 이렇게 해두면 엔진 내부에서 role.* 처리 중 trim(null) 류가 재발할 확률이 확 줄어듦
|
||||
foreach ($formRules as $fieldKey => $ruleDef) {
|
||||
$fieldName = is_array($ruleDef) ? $fieldKey : $fieldKey;
|
||||
// 2) wildcard(role.*) 부모 배열 보정
|
||||
$this->ensureParentArrayForWildcardRules($formDatas, $formRules);
|
||||
|
||||
// role.* 형태면 부모 role을 배열로 보장
|
||||
if (str_contains($fieldName, '.*')) {
|
||||
$parent = str_replace('.*', '', $fieldName);
|
||||
if (!array_key_exists($parent, $formDatas) || $formDatas[$parent] === '') {
|
||||
$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] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3) numeric(FK 포함) 필드: '' -> null, 숫자 문자열 -> int
|
||||
// (규칙 기반 자동 수집)
|
||||
$numericFields = $this->collectNumericFieldsFromRules($formRules);
|
||||
$formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields);
|
||||
|
||||
// 4) dynamicRules 누적 구성 (버그 수정: 루프마다 초기화 금지)
|
||||
$dynamicRules = [];
|
||||
foreach ($formRules as $field => $rule) {
|
||||
try {
|
||||
// 2. 필드명/규칙 추출
|
||||
list($field, $rule) = $this->getValidationRule($field, $rule);
|
||||
// 필드명/규칙 추출(확장 포인트)
|
||||
[$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
|
||||
|
||||
// 3. label 결정
|
||||
if (isset($formFields[$field])) {
|
||||
$label = $formFields[$field];
|
||||
} elseif (str_contains($field, '.*')) {
|
||||
$parentField = str_replace('.*', '', $field);
|
||||
$label = ($formFields[$parentField] ?? $field) . " 항목";
|
||||
// label 결정
|
||||
if (isset($formFields[$fieldName])) {
|
||||
$label = $formFields[$fieldName];
|
||||
} elseif (str_contains($fieldName, '.*')) {
|
||||
$parentField = str_replace('.*', '', $fieldName);
|
||||
$label = ($formFields[$parentField] ?? $fieldName) . " 항목";
|
||||
} else {
|
||||
$label = $field;
|
||||
$label = $fieldName;
|
||||
}
|
||||
|
||||
// 4. rules 설정
|
||||
$dynamicRules = [];
|
||||
$dynamicRules[$field] = [
|
||||
$dynamicRules[$fieldName] = [
|
||||
'label' => $label,
|
||||
'rules' => $rule
|
||||
'rules' => $ruleStr,
|
||||
];
|
||||
|
||||
// ✅ 4.5 존재 보장 로직 수정
|
||||
// - 일반 필드: 없으면 '' 세팅
|
||||
// - 배열 원소 필드(role.*): 여기서 만들면 안 됨 (부모 role에서 처리해야 함)
|
||||
if (!array_key_exists($field, $formDatas) && !str_contains($field, '.*')) {
|
||||
$formDatas[$field] = '';
|
||||
}
|
||||
// ❌ 존재 보장으로 '' 삽입하지 않음
|
||||
// - required는 CI4가 "키 없음"도 실패 처리 가능(일반적으로)
|
||||
// - permit_empty는 키 없어도 통과 (강제로 '' 만들면 FK/숫자 문제 발생)
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
throw new RuntimeException("유효성 검사 규칙 준비 중 오류 발생 (필드: {$field}): " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->_validation->setRules($dynamicRules);
|
||||
|
||||
try {
|
||||
if (!$this->_validation->run($formDatas)) {
|
||||
$errors = $this->_validation->getErrors();
|
||||
throw new RuntimeException(implode("\n", $errors));
|
||||
}
|
||||
} catch (\TypeError $e) {
|
||||
// 너의 상세 디버깅 로직은 그대로 둬도 됨 (생략 가능)
|
||||
throw new RuntimeException("검증 도중 타입 오류 발생: " . $e->getMessage());
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
if ($e instanceof RuntimeException)
|
||||
if ($e instanceof RuntimeException) {
|
||||
throw $e;
|
||||
}
|
||||
throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Overridable hooks
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
//필수함수
|
||||
//사용자정의 함수
|
||||
// 사용자 정의 hook: 필드/룰 커스터마이즈
|
||||
protected function getValidationRule(string $field, string $rule): array
|
||||
{
|
||||
return array($field, $rule);
|
||||
return [$field, $rule];
|
||||
}
|
||||
|
||||
public function getFormFieldLabel(string $field, ?string $label = null): string
|
||||
@ -251,22 +393,35 @@ abstract class CommonForm
|
||||
return $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form rule 정의
|
||||
* - permit_empty|numeric 인 FK들이 여기서 정의되면,
|
||||
* validate()에서 자동으로 ''->null 정규화 대상에 포함됩니다.
|
||||
*/
|
||||
public function getFormRule(string $action, string $field, array $formRules): array
|
||||
{
|
||||
switch ($field) {
|
||||
case $this->getAttribute('pk_field'):
|
||||
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 {
|
||||
$formRules[$field] = "required|numeric";
|
||||
}
|
||||
break;
|
||||
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;
|
||||
case "code":
|
||||
// a-zA-Z → 영문 대소문자,0-9 → 숫자,가-힣 → 한글 완성형,\- → 하이픈
|
||||
$formRules[$field] = sprintf("required|regex_match[/^[a-zA-Z0-9가-힣\-\_]+$/]|min_length[4]%s", in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : "");
|
||||
$formRules[$field] = sprintf(
|
||||
"required|regex_match[/^[a-zA-Z0-9가-힣\-\_]+$/]|min_length[4]%s",
|
||||
in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
|
||||
);
|
||||
break;
|
||||
case "user_uid":
|
||||
$formRules[$field] = "required|numeric";
|
||||
@ -288,11 +443,18 @@ abstract class CommonForm
|
||||
$formRules[$field] = "permit_empty|trim|string";
|
||||
break;
|
||||
}
|
||||
|
||||
return $formRules;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Options
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
protected function getFormOption_process($service, string $action, string $field, array $formDatas = []): array
|
||||
{
|
||||
$entities = [];
|
||||
|
||||
switch ($field) {
|
||||
default:
|
||||
if (in_array($action, ['create_form', 'modify_form', 'alternative_create_form'])) {
|
||||
@ -307,12 +469,14 @@ abstract class CommonForm
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
public function getFormOption(string $action, string $field, array $formDatas = [], array $options = ['options' => [], 'atttributes' => []]): array
|
||||
{
|
||||
$tempOptions = ['' => lang("{$this->getAttribute('class_path')}.label.{$field}") . " 선택"];
|
||||
|
||||
switch ($field) {
|
||||
case 'user_uid':
|
||||
foreach ($this->getFormOption_process(service('userservice'), $action, $field, $formDatas) as $entity) {
|
||||
@ -320,12 +484,14 @@ abstract class CommonForm
|
||||
}
|
||||
$options['options'] = $tempOptions;
|
||||
break;
|
||||
|
||||
case 'clientinfo_uid':
|
||||
foreach ($this->getFormOption_process(service('customer_clientservice'), $action, $field, $formDatas) as $entity) {
|
||||
$tempOptions[$entity->getPK()] = $entity->getCustomTitle();
|
||||
}
|
||||
$options['options'] = $tempOptions;
|
||||
break;
|
||||
|
||||
default:
|
||||
$optionDatas = lang($this->getAttribute('class_path') . "." . strtoupper($field));
|
||||
if (!is_array($optionDatas)) {
|
||||
@ -337,6 +503,7 @@ abstract class CommonForm
|
||||
$options['options'] = $tempOptions;
|
||||
break;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ class ClientForm extends CustomerForm
|
||||
'email',
|
||||
'phone',
|
||||
'role',
|
||||
'status',
|
||||
];
|
||||
$filters = [
|
||||
'site',
|
||||
@ -28,10 +29,6 @@ class ClientForm extends CustomerForm
|
||||
case 'create':
|
||||
case 'create_form':
|
||||
break;
|
||||
case 'modify':
|
||||
case 'modify_form':
|
||||
$fields = [...$fields, 'status'];
|
||||
break;
|
||||
case 'view':
|
||||
$fields = [...$fields, 'status', 'created_at'];
|
||||
break;
|
||||
@ -69,8 +66,8 @@ class ClientForm extends CustomerForm
|
||||
$formRules[$field] = "required|trim|string";
|
||||
break;
|
||||
case "role":
|
||||
$formRules[$field] = 'required|is_array';
|
||||
$formRules['role.*'] = 'trim|in_list[user,vip,reseller]';
|
||||
$formRules[$field] = 'required|is_array|at_least_one';
|
||||
$formRules['role.*'] = 'permit_empty|trim|in_list[user,vip,reseller]';
|
||||
break;
|
||||
case "email":
|
||||
$formRules[$field] = "permit_empty|trim|valid_email";
|
||||
@ -84,6 +81,9 @@ class ClientForm extends CustomerForm
|
||||
case "point_balance":
|
||||
$formRules[$field] = "permit_empty|numeric";
|
||||
break;
|
||||
case "status":
|
||||
$formRules[$field] = "required|trim|string";
|
||||
break;
|
||||
default:
|
||||
$formRules = parent::getFormRule($action, $field, $formRules);
|
||||
break;
|
||||
|
||||
@ -19,7 +19,8 @@ class UserForm extends CommonForm
|
||||
'name',
|
||||
'email',
|
||||
'mobile',
|
||||
'role'
|
||||
'role',
|
||||
'status'
|
||||
];
|
||||
$filters = ['role', 'status'];
|
||||
$indexFilter = $filters;
|
||||
@ -28,10 +29,6 @@ class UserForm extends CommonForm
|
||||
case 'create':
|
||||
case 'create_form':
|
||||
break;
|
||||
case 'modify':
|
||||
case 'modify_form':
|
||||
$fields = [...$fields, 'status'];
|
||||
break;
|
||||
case 'view':
|
||||
$fields = ['id', 'name', 'email', 'mobile', 'role', 'status', 'created_at'];
|
||||
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}]" : "");
|
||||
break;
|
||||
case "role":
|
||||
$formRules[$field] = 'required|is_array';
|
||||
$formRules['role.*'] = 'trim|in_list[manager,cloudflare,firewall,security,director,master]';
|
||||
$formRules[$field] = 'required|is_array|at_least_one';
|
||||
$formRules['role.*'] = 'permit_empty|trim|in_list[manager,cloudflare,firewall,security,director,master]';
|
||||
break;
|
||||
case "status":
|
||||
$formRules[$field] = "required|trim|string";
|
||||
break;
|
||||
default:
|
||||
$formRules = parent::getFormRule($action, $field, $formRules);
|
||||
|
||||
@ -4,35 +4,33 @@
|
||||
return [
|
||||
// 여기서부터 각 Validation rule에 대한 메시지를 정의합니다.
|
||||
// {field}나 {param} 같은 플레이스홀더는 그대로 유지해야 합니다.
|
||||
|
||||
'required' => '[{field}] 필수 입력 항목입니다.',
|
||||
'isset' => '[{field}] 값이 반드시 있어야 합니다.',
|
||||
'valid_email' => '[{field}] 유효한 이메일 주소여야 합니다.',
|
||||
'valid_url' => '[{field}] 유효한 URL이어야 합니다.',
|
||||
'valid_date' => '[{field}] 유효한 날짜여야 합니다.',
|
||||
'valid_dates' => '[{field}] 유효한 날짜여야 합니다.',
|
||||
'valid_ip' => '[{field}] 유효한 IP 주소여야 합니다.',
|
||||
'valid_mac' => '[{field}] 유효한 MAC 주소여야 합니다.',
|
||||
'numeric' => '[{field}] 숫자만 포함해야 합니다.',
|
||||
'integer' => '[{field}] 정수여야 합니다.',
|
||||
'decimal' => '[{field}] 소수점 숫자여야 합니다.',
|
||||
'is_numeric' => '[{field}] 숫자 문자만 포함해야 합니다.',
|
||||
'regex_match' => '[{field}] 올바른 형식이어야 합니다.',
|
||||
'matches' => '{field} 필드가 {param} 필드와 일치하지 않습니다.',
|
||||
'differs' => '[{field}] {param} 필드와 달라야 합니다.',
|
||||
'is_unique' => '[{field}] 고유한 값이어야 합니다.',
|
||||
'is_natural' => '[{field}] 숫자여야 합니다.',
|
||||
'is_natural_no_zero' => '[{field}] 0보다 큰 숫자여야 합니다.',
|
||||
'less_than' => '[{field}] {param}보다 작아야 합니다.',
|
||||
'less_than_equal_to' => '[{field}] {param}보다 작거나 같아야 합니다.',
|
||||
'greater_than' => '[{field}] {param}보다 커야 합니다.',
|
||||
'greater_than_equal_to' => '[{field}] {param}보다 크거나 같아야 합니다.',
|
||||
'error_prefix' => '',
|
||||
'error_suffix' => '',
|
||||
|
||||
// 길이(Length) 관련 rule 메시지
|
||||
'min_length' => '[{field}] 최소 {param}자 이상이어야 합니다.',
|
||||
'max_length' => '[{field}] 최대 {param}자 이하여야 합니다.',
|
||||
'exact_length' => '[{field}] 정확히 {param}자여야 합니다.',
|
||||
'in_list' => '[{field}] 다음 중 하나여야 합니다: {param}.',
|
||||
'required' => '[{field}] 필수 입력 항목입니다.',
|
||||
'isset' => '[{field}] 값이 반드시 있어야 합니다.',
|
||||
'valid_email' => '[{field}] 유효한 이메일 주소여야 합니다.',
|
||||
'valid_url' => '[{field}] 유효한 URL이어야 합니다.',
|
||||
'valid_date' => '[{field}] 유효한 날짜여야 합니다.',
|
||||
'valid_dates' => '[{field}] 유효한 날짜여야 합니다.',
|
||||
'valid_ip' => '[{field}] 유효한 IP 주소여야 합니다.',
|
||||
'valid_mac' => '[{field}] 유효한 MAC 주소여야 합니다.',
|
||||
'numeric' => '[{field}] 숫자만 포함해야 합니다.',
|
||||
'integer' => '[{field}] 정수여야 합니다.',
|
||||
'decimal' => '[{field}] 소수점 숫자여야 합니다.',
|
||||
'is_numeric' => '[{field}] 숫자 문자만 포함해야 합니다.',
|
||||
'regex_match' => '[{field}] 올바른 형식이어야 합니다.',
|
||||
'matches' => '{field} 필드가 {param} 필드와 일치하지 않습니다.',
|
||||
'differs' => '[{field}] {param} 필드와 달라야 합니다.',
|
||||
'is_unique' => '[{field}] 고유한 값이어야 합니다.',
|
||||
'is_natural' => '[{field}] 숫자여야 합니다.',
|
||||
'is_natural_no_zero' => '[{field}] 0보다 큰 숫자여야 합니다.',
|
||||
'less_than' => '[{field}] {param}보다 작아야 합니다.',
|
||||
'less_than_equal_to' => '[{field}] {param}보다 작거나 같아야 합니다.',
|
||||
'greater_than' => '[{field}] {param}보다 커야 합니다.',
|
||||
'greater_than_equal_to' => '[{field}] {param}보다 크거나 같아야 합니다.',
|
||||
'error_prefix' => '',
|
||||
'error_suffix' => '',
|
||||
'min_length' => '[{field}] 최소 {param}자 이상이어야 합니다.',
|
||||
'max_length' => '[{field}] 최대 {param}자 이하여야 합니다.',
|
||||
'exact_length' => '[{field}] 정확히 {param}자여야 합니다.',
|
||||
'in_list' => '[{field}] 다음 중 하나여야 합니다: {param}.',
|
||||
'at_least_one' => '{field} 최소 1개 이상 선택해야 합니다.',
|
||||
];
|
||||
|
||||
@ -2,17 +2,14 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Forms\CommonForm;
|
||||
use App\DTOs\CommonDTO;
|
||||
use App\Entities\CommonEntity;
|
||||
use App\Libraries\AuthContext;
|
||||
use App\Models\CommonModel;
|
||||
use App\Libraries\AuthContext;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @template TEntity of CommonEntity
|
||||
* @template TDto of CommonDTO
|
||||
*/
|
||||
abstract class CommonService
|
||||
{
|
||||
private ?AuthContext $_authContext = null;
|
||||
@ -239,6 +236,8 @@ abstract class CommonService
|
||||
// INSERT 시 Entity의 PK는 0 또는 NULL이어야 함 (DB가 ID를 생성하도록)
|
||||
$initialPK = $entity->getPK();
|
||||
$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)
|
||||
$entity->{$this->getPKField()} = $this->handle_save_result($result, $initialPK);
|
||||
// handle_save_result에서 확인된 최종 PK를 사용하여 DB에서 최신 엔티티를 가져옴
|
||||
@ -260,13 +259,18 @@ abstract class CommonService
|
||||
protected function create_process(array $formDatas): CommonEntity
|
||||
{
|
||||
try {
|
||||
log_message('debug', "*** ENTER" . __METHOD__ . " ***");
|
||||
$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) {
|
||||
$actionForm->action_init_process('create', $formDatas);
|
||||
foreach ($formDatas as $field => $value) {
|
||||
$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();
|
||||
$entity = new $entityClass($formDatas);
|
||||
@ -291,13 +295,18 @@ abstract class CommonService
|
||||
protected function modify_process($entity, array $formDatas): CommonEntity
|
||||
{
|
||||
try {
|
||||
log_message('debug', "*** ENTER" . __METHOD__ . " ***");
|
||||
$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) {
|
||||
$actionForm->action_init_process('modify', $formDatas);
|
||||
foreach ($formDatas as $field => $value) {
|
||||
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas);
|
||||
}
|
||||
log_message('debug', '>>> BEFORE validate: ' . get_class($actionForm));
|
||||
$actionForm->validate($formDatas); // ✅ 여기서 검증
|
||||
log_message('debug', '>>> AFTER validate');
|
||||
}
|
||||
// 검증 통과 후 엔티티 반영
|
||||
$entity->fill($formDatas);
|
||||
|
||||
32
app/Validation/CustomRules.php
Normal file
32
app/Validation/CustomRules.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user