dbmsv4 init...4

This commit is contained in:
최준흠 2026-01-06 13:28:43 +09:00
parent cba596fa0a
commit 298e498cf5
4 changed files with 104 additions and 47 deletions

View File

@ -15,8 +15,8 @@ class ClientEntity extends CustomerEntity
'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' => ''
]; ];
@ -79,8 +79,8 @@ class ClientEntity extends CustomerEntity
} }
// 2-b. JSON이 아니면 CSV로 가정하고 변환 // 2-b. JSON이 아니면 CSV로 가정하고 변환
$parts = explode(DEFAULTS["DELIMITER_ROLE"], $role); $parts = explode(DEFAULTS["DELIMITER_ROLE"], $role);
// 각 요소의 불필요한 공백과 따옴표 제거 // 각 요소의 불필요한 공백과 따옴표 제거. null 가능성에 대비해 string 형변환 추가
$cleanedRoles = array_map(fn($item) => trim($item, " \t\n\r\0\x0B\""), $parts); $cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $parts);
return array_filter($cleanedRoles); return array_filter($cleanedRoles);
} }
// 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환 // 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환
@ -109,8 +109,8 @@ class ClientEntity extends CustomerEntity
elseif (is_array($role)) { elseif (is_array($role)) {
$roleArray = $role; $roleArray = $role;
} }
// 배열의 각 요소를 정리 // 배열의 각 요소를 정리. null이나 scalar 타입이 섞여있을 경우에 대비해 string으로 명시적 형변환 후 trim 수행
$cleanedRoles = array_map(fn($item) => trim($item, " \t\n\r\0\x0B\""), $roleArray); $cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $roleArray);
$roleArray = array_filter($cleanedRoles); $roleArray = array_filter($cleanedRoles);
// 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다. // 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다.
$this->attributes['role'] = implode(DEFAULTS["DELIMITER_ROLE"], $roleArray); $this->attributes['role'] = implode(DEFAULTS["DELIMITER_ROLE"], $roleArray);

View File

@ -57,8 +57,8 @@ class UserEntity extends CommonEntity
} }
// 2-b. JSON이 아니면 CSV로 가정하고 변환 // 2-b. JSON이 아니면 CSV로 가정하고 변환
$parts = explode(DEFAULTS["DELIMITER_ROLE"], $role); $parts = explode(DEFAULTS["DELIMITER_ROLE"], $role);
// 각 요소의 불필요한 공백과 따옴표 제거 // 각 요소의 불필요한 공백과 따옴표 제거. null 가능성에 대비해 string 형변환 추가
$cleanedRoles = array_map(fn($item) => trim($item, " \t\n\r\0\x0B\""), $parts); $cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $parts);
return array_filter($cleanedRoles); return array_filter($cleanedRoles);
} }
// 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환 // 3. 변환에 실패했거나 데이터가 없는 경우 빈 배열 반환
@ -95,8 +95,8 @@ class UserEntity extends CommonEntity
elseif (is_array($role)) { elseif (is_array($role)) {
$roleArray = $role; $roleArray = $role;
} }
// 배열의 각 요소를 정리 // 배열의 각 요소를 정리. null이나 scalar 타입이 섞여있을 경우에 대비해 string으로 명시적 형변환 후 trim 수행
$cleanedRoles = array_map(fn($item) => trim($item, " \t\n\r\0\x0B\""), $roleArray); $cleanedRoles = array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $roleArray);
$roleArray = array_filter($cleanedRoles); $roleArray = array_filter($cleanedRoles);
// 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다. // 최종적으로 DB에 삽입될 단일 CSV 문자열로 변환하여 저장합니다.
$this->attributes['role'] = implode(DEFAULTS["DELIMITER_ROLE"], $roleArray); $this->attributes['role'] = implode(DEFAULTS["DELIMITER_ROLE"], $roleArray);

View File

@ -132,7 +132,7 @@ abstract class CommonForm
* @param array $formDatas 검증할 데이터 * @param array $formDatas 검증할 데이터
* @throws RuntimeException * @throws RuntimeException
*/ */
final public function validate(array $formDatas): void final public function validate(array &$formDatas): void
{ {
if ($this->_validation === null) { if ($this->_validation === null) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Validation 서비스가 초기화되지 않았습니다."); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Validation 서비스가 초기화되지 않았습니다.");
@ -141,12 +141,23 @@ abstract class CommonForm
try { try {
$dynamicRules = []; $dynamicRules = [];
// 0. Ensure all scalar inputs are strings to prevent trim() error on PHP 8.1+ // 0. Ensure all scalar/null inputs are strings to prevent trim() error on PHP 8.1+
foreach ($formDatas as $key => $value) { $castToString = function (&$data, $path = '') use (&$castToString) {
if (is_scalar($value) && !is_string($value)) { foreach ($data as $key => &$value) {
$formDatas[$key] = (string) $value; $currentField = $path ? "{$path}.{$key}" : $key;
try {
if (is_array($value)) {
$castToString($value, $currentField);
} elseif ($value === null || (is_scalar($value) && !is_string($value))) {
$data[$key] = (string) ($value ?? '');
}
} catch (\Throwable $e) {
throw new RuntimeException("데이터 정제 중 오류 발생 (필드: {$currentField}): " . $e->getMessage());
}
} }
} };
$castToString($formDatas);
// 1. 현재 서비스의 필드 라벨 정보 로드 (언어 파일 기반) // 1. 현재 서비스의 필드 라벨 정보 로드 (언어 파일 기반)
$formFields = $this->getFormFields(); $formFields = $this->getFormFields();
$formRules = $this->getFormRules(); $formRules = $this->getFormRules();
@ -156,48 +167,94 @@ abstract class CommonForm
} }
foreach ($formRules as $field => $rule) { foreach ($formRules as $field => $rule) {
// 2. 필드명과 규칙 추출 try {
list($field, $rule) = $this->getValidationRule($field, $rule); // 2. 필드명과 규칙 추출
list($field, $rule) = $this->getValidationRule($field, $rule);
// 3. 라벨 결정 로직 (한글 라벨 매핑) // 3. 라벨 결정 로직 (한글 라벨 매핑)
if (isset($formFields[$field])) { if (isset($formFields[$field])) {
$label = $formFields[$field]; $label = $formFields[$field];
} elseif (str_contains($field, '.*')) { } elseif (str_contains($field, '.*')) {
// 배열 검증(role.* 등)의 경우 부모 필드의 라벨을 활용 // 배열 검증(role.* 등)의 경우 부모 필드의 라벨을 활용
$parentField = str_replace('.*', '', $field); $parentField = str_replace('.*', '', $field);
$label = ($formFields[$parentField] ?? $field) . " 항목"; $label = ($formFields[$parentField] ?? $field) . " 항목";
} else { } else {
$label = $field; // 언어 파일에 정의가 없는 경우 필드명 유지 $label = $field; // 언어 파일에 정의가 없는 경우 필드명 유지
}
// 4. [핵심 해결책] 규칙 배열 자체에 label을 포함시킴
// 이렇게 하면 CI4 엔진이 {field} 자리에 이 label 값을 최우선으로 사용합니다.
$dynamicRules[$field] = [
'label' => $label,
'rules' => $rule
];
// 4.5. Ensure the field exists in formDatas to prevent trim(null) in the engine
if (!array_key_exists($field, $formDatas) && !str_contains($field, '.*')) {
$formDatas[$field] = '';
}
} catch (\Throwable $e) {
throw new RuntimeException("유효성 검사 규칙 준비 중 오류 발생 (필드: {$field}): " . $e->getMessage());
} }
// 4. [핵심 해결책] 규칙 배열 자체에 label을 포함시킴
// 이렇게 하면 CI4 엔진이 {field} 자리에 이 label 값을 최우선으로 사용합니다.
$dynamicRules[$field] = [
'label' => $label,
'rules' => $rule
];
} }
// 5. 검증 규칙 설정 (인자를 하나만 전달하여 설정 충돌 방지) // 5. 검증 규칙 설정 (인자를 하나만 전달하여 설정 충돌 방지)
$this->_validation->setRules($dynamicRules); $this->_validation->setRules($dynamicRules);
// 6. 검증 실행 // 6. 검증 실행
if (!$this->_validation->run($formDatas)) { try {
// 한글 라벨이 적용된 에러 메시지들을 배열로 가져와 한 줄씩 합침 if (!$this->_validation->run($formDatas)) {
$errors = $this->_validation->getErrors(); // 한글 라벨이 적용된 에러 메시지들을 배열로 가져와 한 줄씩 합침
throw new RuntimeException(implode("\n", $errors)); $errors = $this->_validation->getErrors();
throw new RuntimeException(implode("\n", $errors));
}
} catch (\TypeError $e) {
// TypeError(예: trim(null) 등) 발생 시 범인(field) 찾기 시도
$culpritField = "알 수 없음";
$culpritValue = "null";
foreach ($dynamicRules as $f => $r) {
if (str_contains($r['rules'], 'trim')) {
// 중첩 필드(.*)와 일반 필드 구분하여 null 체크
if (str_contains($f, '.*')) {
$parentKey = str_replace('.*', '', $f);
if (isset($formDatas[$parentKey]) && is_array($formDatas[$parentKey])) {
foreach ($formDatas[$parentKey] as $k => $v) {
if ($v === null) {
$culpritField = "{$parentKey}.{$k} ({$r['label']})";
break 2;
}
}
}
} else {
if (!isset($formDatas[$f]) || $formDatas[$f] === null) {
$culpritField = "{$f} ({$r['label']})";
break;
}
}
}
}
$errorMsg = $e->getMessage();
// "trim(): Argument #1 ($string) must be of type string, null given" 문구에서 Argument #1을 필드명으로 교체
$errorMsg = str_replace("Argument #1 (\$string)", "데이터(필드: {$culpritField}, 값: {$culpritValue})", $errorMsg);
throw new RuntimeException("검증 도중 타입 오류 발생: " . $errorMsg);
} }
// 검증 성공 시 추가 로직 없이 종료 // 검증 성공 시 추가 로직 없이 종료
} catch (\Throwable $e) { } catch (\Throwable $e) {
// 이미 RuntimeException으로 포장된 경우 그대로 던짐
if ($e instanceof RuntimeException) {
throw $e;
}
// 오류 발생 시 디버깅을 위해 로그 기록 // 오류 발생 시 디버깅을 위해 로그 기록
log_message('debug', '--- Validation Error Detail ---'); log_message('debug', '--- Validation Error Detail ---');
log_message('debug', 'Rules: ' . var_export($this->getFormRules(), true)); log_message('debug', 'Rules: ' . var_export($this->getFormRules(), true));
log_message('debug', 'Data: ' . var_export($formDatas, true)); log_message('debug', 'Data: ' . var_export($formDatas, true));
log_message('debug', 'Message: ' . $e->getMessage()); log_message('debug', 'Message: ' . $e->getMessage());
throw new RuntimeException($e->getMessage()); throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
} }
} }

View File

@ -22,7 +22,7 @@ class ClientHelper extends CustomerHelper
break; break;
case 'role': case 'role':
$currentRoles = is_array($value) $currentRoles = is_array($value)
? array_map('strtolower', array_map('trim', $value)) ? array_map('strtolower', array_map(fn($item) => trim((string) ($item ?? ''), " \t\n\r\0\x0B\""), $value))
: []; : [];
$form = ''; $form = '';
//Form페이지에서는 맨앞에것 제외하기 위함 //Form페이지에서는 맨앞에것 제외하기 위함
@ -60,7 +60,7 @@ class ClientHelper extends CustomerHelper
"data-src" => "/admin/customer/wallet/account?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup", "data-src" => "/admin/customer/wallet/account?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup",
"data-bs-toggle" => "modal", "data-bs-toggle" => "modal",
"data-bs-target" => "#modal_action_form", "data-bs-target" => "#modal_action_form",
"class" => "text-primary", "class" => "text-primary",
...$extras, ...$extras,
] ]
); );
@ -73,7 +73,7 @@ class ClientHelper extends CustomerHelper
"data-src" => "/admin/customer/wallet/coupon?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup", "data-src" => "/admin/customer/wallet/coupon?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup",
"data-bs-toggle" => "modal", "data-bs-toggle" => "modal",
"data-bs-target" => "#modal_action_form", "data-bs-target" => "#modal_action_form",
"class" => "text-primary", "class" => "text-primary",
...$extras, ...$extras,
] ]
); );
@ -86,7 +86,7 @@ class ClientHelper extends CustomerHelper
"data-src" => "/admin/customer/wallet/point?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup", "data-src" => "/admin/customer/wallet/point?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup",
"data-bs-toggle" => "modal", "data-bs-toggle" => "modal",
"data-bs-target" => "#modal_action_form", "data-bs-target" => "#modal_action_form",
"class" => "text-primary", "class" => "text-primary",
...$extras, ...$extras,
] ]
); );
@ -107,7 +107,7 @@ class ClientHelper extends CustomerHelper
case 'batchjob': case 'batchjob':
case 'batchjob_delete': case 'batchjob_delete':
//역활이 보안관리자가 아니면 사용불가 //역활이 보안관리자가 아니면 사용불가
$action = $this->getAuthContext()->isAccessRole([ROLE['USER']['SECURITY']]) ? parent::getListButton($action, $label, $viewDatas, $extras) : ""; $action = $this->getAuthContext()->isAccessRole([ROLE['USER']['SECURITY']]) ? parent::getListButton($action, $label, $viewDatas, $extras) : "";
break; break;
case 'modify': case 'modify':
//역활이 보안관리자가 아니면 수정불가 //역활이 보안관리자가 아니면 수정불가
@ -133,7 +133,7 @@ class ClientHelper extends CustomerHelper
$label, $label,
$action, $action,
[ [
"data-src" => "/admin/customer/wallet/{$action}?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup", "data-src" => "/admin/customer/wallet/{$action}?clientinfo_uid={$viewDatas['entity']->getPK()}&ActionTemplate=popup",
"data-bs-toggle" => "modal", "data-bs-toggle" => "modal",
"data-bs-target" => "#modal_action_form", "data-bs-target" => "#modal_action_form",
"class" => "text-primary", "class" => "text-primary",
@ -169,10 +169,10 @@ class ClientHelper extends CustomerHelper
$action = "{$label} 0원"; $action = "{$label} 0원";
if (array_key_exists($viewDatas['entity']->getPK(), $viewDatas['unPaids'])) { if (array_key_exists($viewDatas['entity']->getPK(), $viewDatas['unPaids'])) {
$action = form_label( $action = form_label(
sprintf("%s건/%s원", $viewDatas['unPaids'][$viewDatas['entity']->getPK()]['cnt'], number_format($viewDatas['unPaids'][$viewDatas['entity']->getPK()]['amount'])), sprintf("%s건/%s원", $viewDatas['unPaids'][$viewDatas['entity']->getPK()]['cnt'], number_format($viewDatas['unPaids'][$viewDatas['entity']->getPK()]['amount'])),
'payment_unpaid', 'payment_unpaid',
[ [
"data-src" => "/admin/payment?clientinfo_uid={$viewDatas['entity']->getPK()}&status=unpaid&ActionTemplate=popup", "data-src" => "/admin/payment?clientinfo_uid={$viewDatas['entity']->getPK()}&status=unpaid&ActionTemplate=popup",
"data-bs-toggle" => "modal", "data-bs-toggle" => "modal",
"data-bs-target" => "#modal_action_form", "data-bs-target" => "#modal_action_form",
"class" => "text-primary form-label-sm", "class" => "text-primary form-label-sm",