dbmsv4 init...5
This commit is contained in:
parent
509cd0e999
commit
40b809949c
@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}.',
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
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