daemon-idc init

This commit is contained in:
최준흠 2026-03-02 12:07:14 +09:00
parent a8b50340ac
commit 209b86180c
5 changed files with 118 additions and 454 deletions

View File

@ -1,13 +1,4 @@
<?php <?php
// =========================================================
// AbstractWebController.php (FINAL)
// - runAction / okResponse / failResponse 내장
// - action_redirect_process: AJAX 방어 + 상태코드 정책 고정
// * warning/error => 400
// * critical/alert/emergency => 500
// * info/notice/debug/default => 200
// - RedirectResponse|ResponseInterface로 엄격 정리
// =========================================================
namespace App\Controllers; namespace App\Controllers;
@ -18,26 +9,29 @@ use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use App\Exceptions\FormValidationException;
/**
* CommonController
*
* 클래스를 상속받는 모든 자식 클래스(UserController )
* 반드시 'PATH' 상수를 가지고 있음을 IDE에 알려줍니다.
* * @property-read string PATH // ⭐ 이 부분이 핵심입니다.
*/
abstract class AbstractWebController extends Controller abstract class AbstractWebController extends Controller
{ {
use LogTrait; use LogTrait;
protected $service = null;
private array $_action_paths = []; private array $_action_paths = [];
private array $_viewDatas = []; private array $_viewDatas = [];
private ?string $_title = null; private ?string $_title = null;
protected $layouts = [];
protected $service = null;
// --- 초기화 및 DI --- // --- 초기화 및 DI ---
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger) public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{ {
parent::initController($request, $response, $logger); parent::initController($request, $response, $logger);
helper('util');
} }
final protected function getAuthContext(): AuthContext final protected function getAuthContext(): AuthContext
{ {
return service('myauth')->getAuthContext(); return service('myauth')->getAuthContext();
@ -46,13 +40,15 @@ abstract class AbstractWebController extends Controller
protected function getTitle(): string protected function getTitle(): string
{ {
if ($this->_title === null) { if ($this->_title === null) {
// 이 로직은 하위 클래스에서 service가 초기화되었다고 가정합니다.
$this->_title = lang("{$this->service->getClassPaths(false)}.title"); $this->_title = lang("{$this->service->getClassPaths(false)}.title");
} }
return $this->_title; return $this->_title;
} }
// --- 경로 및 뷰 데이터 관리 --- // --- 경로 및 뷰 데이터 관리 ---
final protected function addActionPaths(string $path): void
final protected function addActionPaths(string $path)
{ {
$this->_action_paths[] = $path; $this->_action_paths[] = $path;
} }
@ -62,19 +58,24 @@ abstract class AbstractWebController extends Controller
return $isArray ? $this->_action_paths : implode($delimeter, $this->_action_paths); return $isArray ? $this->_action_paths : implode($delimeter, $this->_action_paths);
} }
final protected function addViewDatas(string $key, mixed $value): void final protected function addViewDatas(string $key, mixed $value)
{ {
$this->_viewDatas[$key] = $value; $this->_viewDatas[$key] = $value;
} }
final protected function getViewDatas(?string $key = null): mixed final protected function getViewDatas(?string $key = null): mixed
{ {
if ($key === null) if ($key === null) {
return $this->_viewDatas; return $this->_viewDatas;
}
return $this->_viewDatas[$key] ?? null; return $this->_viewDatas[$key] ?? null;
} }
// --- 공통 처리 로직 (Override 가능) --- // --- 공통 처리 로직 (Override 가능) ---
/**
* 모든 액션 실행 공통 초기화 작업
*/
protected function action_init_process(string $action, array $formDatas = []): void protected function action_init_process(string $action, array $formDatas = []): void
{ {
$this->addViewDatas('action', $action); $this->addViewDatas('action', $action);
@ -84,49 +85,10 @@ abstract class AbstractWebController extends Controller
} }
/** /**
* action_redirect_process * 액션 성공 모달을 닫고 부모 창을 리로드하는 스크립트를 반환합니다.
* AJAX 요청이면 RedirectResponse 대신 JSON으로 변환(방어)
*
* 상태코드 정책(고정):
* - warning/error => 400
* - critical/alert/emergency => 500
* - info/notice/debug/default => 200
*/ */
protected function action_redirect_process(string $type, string $message, ?string $redirect_url = null): RedirectResponse|ResponseInterface protected function action_redirect_process(string $type, string $message, ?string $redirect_url = null): RedirectResponse
{ {
$resolvedRedirect = $redirect_url
?? $this->getAuthContext()->popPreviousUrl()
?? implode(DIRECTORY_SEPARATOR, $this->getActionPaths());
if ($this->request->isAJAX()) {
$error400 = ['warning', 'error'];
$error500 = ['critical', 'alert', 'emergency'];
if (in_array($type, $error400, true)) {
log_message($type, $message);
return $this->response->setStatusCode(400)->setJSON([
'ok' => false,
'message' => $message,
'redirect' => $resolvedRedirect,
]);
}
if (in_array($type, $error500, true)) {
log_message($type, $message);
return $this->response->setStatusCode(500)->setJSON([
'ok' => false,
'message' => $message,
'redirect' => $resolvedRedirect,
]);
}
return $this->response->setStatusCode(200)->setJSON([
'ok' => true,
'message' => $message,
'redirect' => $resolvedRedirect,
]);
}
switch ($type) { switch ($type) {
case 'warning': case 'warning':
case 'error': case 'error':
@ -134,104 +96,42 @@ abstract class AbstractWebController extends Controller
case 'alert': case 'alert':
case 'emergency': case 'emergency':
log_message($type, $message); log_message($type, $message);
return redirect()->back()->withInput()->with('message', $message); $result = redirect()->back()->withInput()->with('message', $message);
break;
case 'debug': case 'debug':
case 'info': case 'info':
case 'notice': case 'notice':
default: default:
return redirect()->to($resolvedRedirect)->with('message', $message); $redirect_url = $redirect_url ?? $this->getAuthContext()->popPreviousUrl() ?? implode(DIRECTORY_SEPARATOR, $this->getActionPaths());
$result = redirect()->to($redirect_url)->with('message', $message);
break;
} }
return $result;
} }
/** /**
* 렌더링 * 경로와 데이터를 이용하여 최종 HTML을 렌더링합니다.
*/ */
protected function action_render_process(string $view_file, array $viewDatas, ?string $template_path = null): string protected function action_render_process(string $view_file, array $viewDatas, ?string $template_path = null): string
{ {
helper(['form', 'utility']); helper(['form', 'IconHelper', 'utility']);
$config = config('Layout');
$layoutConfig = $config->layouts[$viewDatas['layout']['path']] ?? [];
$baseViewPath = trim($viewDatas['layout']['path'], '/'); $viewDatas['layout'] = array_merge($layoutConfig, $viewDatas['layout']);
if ($template_path) $view_path = $viewDatas['layout']['path'];
$baseViewPath .= '/' . trim($template_path, '/'); if ($template_path) {
$view_path .= '/' . $template_path;
$viewName = $baseViewPath . '/' . ltrim($view_file, '/'); }
// dd($view_path);
return view($viewName, [ //최종 ViewPath
$viewDatas['view_path'] = $view_path;
helper([__FUNCTION__]);
return view($view_path . '/' . $view_file, [
'viewDatas' => [ 'viewDatas' => [
...$viewDatas, ...$viewDatas,
'forms' => [ 'forms' => ['attributes' => ['method' => "post",], 'hiddens' => []],
'attributes' => ['method' => 'post'], ]
'hiddens' => [],
],
],
]); ]);
} }
// =========================================================
// 공통화: runAction / okResponse / failResponse
// =========================================================
protected function stringifyError(mixed $x): string
{
if (is_string($x))
return $x;
if ($x instanceof \Throwable) {
return $x->getMessage();
}
if (is_array($x) || is_object($x)) {
$json = json_encode($x, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return $json !== false ? $json : print_r($x, true);
}
return (string) $x;
}
protected function runAction(string $action, callable $core): mixed
{
try {
return $core();
} catch (FormValidationException $e) {
return $this->failResponse($action, $e);
} catch (\Throwable $e) {
return $this->failResponse($action, $e);
}
}
protected function okResponse(string $message, array $payload = [], ?string $redirectUrl = null): RedirectResponse|ResponseInterface
{
if ($this->request->isAJAX()) {
return $this->response->setStatusCode(200)->setJSON(array_merge(
['ok' => true, 'message' => $message],
$payload
));
}
return $this->action_redirect_process('info', $message, $redirectUrl);
}
protected function failResponse(string $action, \Throwable $e, ?string $humanPrefix = null): RedirectResponse|ResponseInterface
{
if ($e instanceof FormValidationException) {
if ($this->request->isAJAX()) {
return $this->response->setStatusCode(422)->setJSON([
'ok' => false,
'errors' => $e->errors,
]);
}
// ✅ redirect에는 string만 넣는다
return $this->action_redirect_process('error', $e->getMessage());
}
if ($this->request->isAJAX()) {
return $this->response->setStatusCode(500)->setJSON([
'ok' => false,
'message' => static::class . '->' . $action . "에서 오류:" . $e->getMessage(),
]);
}
$msg = $humanPrefix ? ($humanPrefix . $e->getMessage()) : $e->getMessage();
// ✅ redirect에는 string만 넣는다
return $this->action_redirect_process('error', $msg);
}
} }

View File

@ -10,113 +10,34 @@ class UserEntity extends CommonEntity
const PK = Model::PK; const PK = Model::PK;
const TITLE = Model::TITLE; const TITLE = Model::TITLE;
// ✅ role은 반드시 "문자열" 기본값 (DB 저장형)
protected $attributes = [
'id' => '',
'passwd' => '',
'name' => '',
'email' => '',
'mobile' => null,
'role' => '', // ✅ array 금지
'status' => '',
];
public function __construct(array|null $data = null) public function __construct(array|null $data = null)
{ {
parent::__construct($data); parent::__construct($data);
$this->nullableFields = [
...$this->nullableFields,
'mobile',
];
} }
public function getID(): string public function getID(): string
{ {
return (string) ($this->attributes['id'] ?? ''); return $this->id;
} }
public function getPassword(): string public function getPassword(): string
{ {
return (string) ($this->attributes['passwd'] ?? ''); return $this->passwd;
} }
public function getRole(): string
/**
* role을 "배열" 반환 (DB에는 CSV/JSON/배열 무엇이든 복구)
*/
public function getRole(): array
{ {
$role = $this->attributes['role'] ?? null; return $this->role;
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 [];
} }
public function getEmail(): string
/**
* CI4 뮤테이터: "return 값" attributes에 저장됨
* - 빈값이면 기존값 유지 (create에서 required면 validate에서 걸러짐)
*/
public function setPasswd($password): string
{ {
// null/'' 이면 기존값 유지 return $this->email;
if (!is_string($password) || $password === '') {
return (string) ($this->attributes['passwd'] ?? '');
}
return password_hash($password, PASSWORD_BCRYPT);
} }
public function getMobile(): ?string
/**
* role은 최종적으로 "CSV 문자열" 저장 (DB 안전)
*/
public function setRole($role): string
{ {
$roleArray = []; return $this->mobile;
if (is_string($role)) {
$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);
}
}
} elseif (is_array($role)) {
$roleArray = $role;
} else {
// 그 외 타입은 안전하게 빈값 처리
$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);
} }
} }

View File

@ -9,15 +9,19 @@ abstract class CommonModel extends Model
protected $table = ''; protected $table = '';
protected $primaryKey = ''; protected $primaryKey = '';
protected $useAutoIncrement = true; protected $useAutoIncrement = true;
// protected $returnType = 'array';
//true이면 모든 delete * 메소드 호출은 실제로 행을 삭제하는 것이 아니라 플래그를 데이터베이스로 설정
protected $useSoftDeletes = false; protected $useSoftDeletes = false;
protected $protectFields = true; protected $protectFields = true;
protected $allowedFields = []; protected $allowedFields = [];
// $allowEmptyInserts = false (기본값): 삽입할 데이터가 전혀 없는 경우, CI4는 오류를 발생시키며 쿼리 실행을 막습니다. (보안 및 데이터 무결성 목적)
// $allowEmptyInserts = true: 삽입할 데이터가 없어도 INSERT INTO table_name () VALUES () 같은 빈 쿼리 실행을 허용합니다 (극히 드문 경우에 사용).
protected bool $allowEmptyInserts = false; protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true; protected bool $updateOnlyChanged = true;
// protected $useEmptyStringIfNull = true; (기본값)
protected $useEmptyStringIfNull = false; // NULL도 DB로 보내기 // 이 기본 설정 때문에 PHP의 null 값이 데이터베이스로 전달될 때 실제 SQL의 NULL 키워드가 아닌 **빈 문자열 ('')**로 변환되고 있습니다.
// 그리고 데이터베이스(MySQL 등)의 설정에 따라 빈 문자열이 업데이트 쿼리에서 무시되거나, 해당 컬럼의 기존 값이 유지되는 현상이 발생합니다.
protected $useEmptyStringIfNull = false; //NULL값도 넣을려면 false
protected array $casts = []; protected array $casts = [];
protected array $castHandlers = []; protected array $castHandlers = [];
@ -37,148 +41,37 @@ abstract class CommonModel extends Model
// Callbacks // Callbacks
protected $allowCallbacks = true; protected $allowCallbacks = true;
protected $beforeInsert = []; //Field 값이 NULL일 경우 DB Default값 적용용
/**
* 변경:
* - beforeInsert: emptyStringToNull + applyDbDefaultsOnInsert
* - beforeUpdate: emptyStringToNull만 유지 (UPDATE에서는 DB default를 쓰려고 컬럼을 빼면 위험/의도와 다름)
*/
protected $beforeInsert = ['emptyStringToNull', 'applyDbDefaultsOnInsert'];
protected $afterInsert = []; protected $afterInsert = [];
protected $beforeUpdate = ['emptyStringToNull']; protected $beforeUpdate = []; //Field 값이 NULL일 경우 DB Default값 적용용
protected $afterUpdate = []; protected $afterUpdate = [];
protected $beforeFind = []; protected $beforeFind = [];
protected $afterFind = []; protected $afterFind = [];
protected $beforeDelete = []; protected $beforeDelete = [];
protected $afterDelete = []; protected $afterDelete = [];
/**
* 문자열을 NULL로 바꾸고 싶은 필드들 (모델별 override)
* - : FK, 숫자필드
*/
protected array $nullableFields = [];
/**
* 추가: DB DEFAULT를 “사용하고 싶은” 필드들 (모델별 override)
* - INSERT 값이 null/''/공백이면 payload에서 제거(unset)해서 DB default가 동작하게
* - UPDATE에서는 적용하지 않음 (비우기 가능)
*/
protected array $allowedDbDefaultFields = [];
/**
* 공백문자열도 빈값으로 취급할지 정책
*/
protected bool $dbDefaultTreatWhitespaceAsEmpty = true;
protected function __construct() protected function __construct()
{ {
parent::__construct(); parent::__construct();
} }
final public function getTable(): string final public function getTable(): string
{ {
return constant("static::TABLE"); return constant("static::TABLE");
} }
final public function getPKField(): string final public function getPKField(): string
{ {
return constant("static::PK"); return constant("static::PK");
} }
final public function getTitleField(): string final public function getTitleField(): string
{ {
return constant("static::TITLE"); return constant("static::TITLE");
} }
final public function useAutoIncrement(): bool final public function useAutoIncrement(): bool
{ {
return $this->useAutoIncrement; return $this->useAutoIncrement;
} }
final public function getAllowedFields(): array final public function getAllowedFields(): array
{ {
return $this->allowedFields; return $this->allowedFields;
} }
/**
* 기존 로직 유지:
* - nullableFields에 지정된 필드만 '' => null 변환
*/
protected function emptyStringToNull(array $data): array
{
if (!isset($data['data']) || !is_array($data['data'])) {
return $data;
}
if (empty($this->nullableFields)) {
return $data;
}
foreach ($this->nullableFields as $field) {
if (!array_key_exists($field, $data['data'])) {
continue;
}
$v = $data['data'][$field];
if (is_string($v)) {
$v = trim($v);
$data['data'][$field] = ($v === '') ? null : $v;
} else {
if ($v === '') {
$data['data'][$field] = null;
}
}
}
return $data;
}
/**
* 추가 로직:
* INSERT 때만 DB DEFAULT를 쓰고 싶은 필드를 payload에서 제거(unset)
*
* - allowedDbDefaultFields에 있는 필드만 처리
* - 값이 null / '' / (옵션)공백문자열 이면 unset
* - 이렇게 하면 INSERT 쿼리에서 컬럼 자체가 빠져서 DB default가 적용됨
*/
protected function applyDbDefaultsOnInsert(array $data): array
{
if (!isset($data['data']) || !is_array($data['data'])) {
return $data;
}
if (empty($this->allowedDbDefaultFields)) {
return $data;
}
foreach ($this->allowedDbDefaultFields as $field) {
if (!array_key_exists($field, $data['data'])) {
continue;
}
$v = $data['data'][$field];
// null이면 제거
if ($v === null) {
unset($data['data'][$field]);
continue;
}
// 문자열이면 '' 또는 (옵션)trim 후 '' 이면 제거
if (is_string($v)) {
if ($v === '') {
unset($data['data'][$field]);
continue;
}
if ($this->dbDefaultTreatWhitespaceAsEmpty && trim($v) === '') {
unset($data['data'][$field]);
continue;
}
}
}
return $data;
}
} }

View File

@ -2,13 +2,13 @@
namespace App\Services; namespace App\Services;
use App\Forms\CommonForm;
use App\Entities\CommonEntity;
use App\Models\CommonModel;
use App\Libraries\AuthContext;
use CodeIgniter\Database\Exceptions\DatabaseException;
use App\Exceptions\FormValidationException;
use RuntimeException; use RuntimeException;
use App\Forms\CommonForm;
use App\Models\CommonModel;
use App\Entities\CommonEntity;
use App\Libraries\AuthContext;
use App\Exceptions\FormValidationException;
use CodeIgniter\Database\Exceptions\DatabaseException;
abstract class CommonService abstract class CommonService
{ {
@ -25,6 +25,7 @@ abstract class CommonService
protected function __construct(protected CommonModel $model) protected function __construct(protected CommonModel $model)
{ {
} }
abstract public function getEntityClass(): string; abstract public function getEntityClass(): string;
/** /**
@ -38,9 +39,6 @@ abstract class CommonService
$result = $callback($db); $result = $callback($db);
$db->transComplete(); $db->transComplete();
return $result; return $result;
} catch (FormValidationException $e) {
$db->transRollback();
throw $e; // ✅ 이거 필수
} catch (DatabaseException $e) { } catch (DatabaseException $e) {
$errorMessage = sprintf( $errorMessage = sprintf(
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n", "\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
@ -52,7 +50,7 @@ abstract class CommonService
throw new RuntimeException($errorMessage, $e->getCode(), $e); throw new RuntimeException($errorMessage, $e->getCode(), $e);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$db->transRollback(); $db->transRollback();
throw $e; // ✅ 여기서도 RuntimeException으로 감싸지 말 것 (권장) throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
} }
} }
@ -201,46 +199,19 @@ abstract class CommonService
} }
} }
//CURD 결과처리용 //CURD 결과처리용
protected function handle_save_result(mixed $result, int|string $uid): int|string
{
if ($result === false) {
$errors = $this->model->errors();
$errorMsg = is_array($errors) ? implode(", ", $errors) : "DB 저장 작업이 실패했습니다.";
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: " . $errorMsg);
}
$pk = $uid; // 기본적으로 기존 $uid (업데이트의 경우)
// AUTO_INCREMENT 필드를 사용하는 경우, INSERT 작업이라면 새로 생성된 ID를 가져옵니다.
// INSERT 작업은 보통 $uid가 0 또는 null/빈 문자열일 때 실행됩니다.
if ($this->model->useAutoIncrement() && (empty($uid) || $uid === 0)) {
// CodeIgniter 모델의 getInsertID()를 사용하여 새로 생성된 PK를 확실히 가져옵니다.
$insertID = $this->model->getInsertID();
if ($insertID > 0) {
$pk = $insertID;
}
} elseif ($this->model->useAutoIncrement() && is_numeric($result) && (int) $result > 0) {
// save()가 성공적인 INSERT 후 PK를 반환하는 경우를 대비 (CI4의 동작)
$pk = (int) $result;
}
// 최종적으로 PK가 유효한지 확인합니다.
if (empty($pk)) {
$errors = $this->model->errors();
$errorMsg = is_array($errors) && !empty($errors) ? implode(", ", $errors) : "DB 작업 성공 후 PK를 확인할 수 없거나 모델 오류 발생:{$pk}";
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: " . $errorMsg);
}
return $pk;
}
protected function save_process(CommonEntity $entity): CommonEntity protected function save_process(CommonEntity $entity): CommonEntity
{ {
try { try {
// INSERT 시 Entity의 PK는 0 또는 NULL이어야 함 (DB가 ID를 생성하도록) if (!$this->model->save($entity)) {
$initialPK = $entity->getPK(); $errors = $this->model->errors();
$result = $this->model->save($entity); $errorMsg = is_array($errors) ? implode(", ", $errors) : "DB 저장 작업이 실패했습니다.";
log_message('debug', __FUNCTION__ . ":" . var_export($entity, true)); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: " . $errorMsg);
log_message('debug', __FUNCTION__ . ":" . $this->model->getLastQuery()); }
// 최종적으로 DB에 반영된 PK를 반환받습니다. (UPDATE이면 기존 PK, INSERT이면 새 PK) // CodeIgniter 모델의 getInsertID()를 사용하여 새로 생성된 PK를 확실히 가져옵니다.
$entity->{$this->getPKField()} = $this->handle_save_result($result, $initialPK); if ($this->model->useAutoIncrement()) {
// handle_save_result에서 확인된 최종 PK를 사용하여 DB에서 최신 엔티티를 가져옴 $entity->{$this->getPKField()} = $this->model->getInsertID();
}
return $entity; return $entity;
} catch (\Throwable $e) { } catch (\Throwable $e) {
log_message('debug', __FUNCTION__ . ":" . var_export($entity, true)); log_message('debug', __FUNCTION__ . ":" . var_export($entity, true));
@ -250,7 +221,7 @@ abstract class CommonService
} }
//Action 작업시 field에따른 Hook처리(각 Service에서 override); //Action 작업시 field에따른 Hook처리(각 Service에서 override);
protected function actionForm_fieldhook_process(string $field, $value, array $formDatas): array protected function fieldhook_process(string $field, $value, array $formDatas): array
{ {
return $formDatas; return $formDatas;
} }
@ -260,21 +231,21 @@ abstract class CommonService
{ {
try { try {
$actionForm = $this->getActionForm(); $actionForm = $this->getActionForm();
if ($actionForm instanceof CommonForm) { if (!$actionForm instanceof CommonForm) {
$actionForm->action_init_process('create', $formDatas); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: actionForm이 정의되지 않았습니다.");
// log_message('debug', 'BEFORE hook CREATE FORMDATA:' . print_r($formDatas ?? null, true)); }
foreach ($formDatas as $field => $value) { $actionForm->action_init_process('create', $formDatas);
$formDatas = $this->actionForm_fieldhook_process($field, $value, $formDatas); $actionForm->validate($formDatas); // ✅ 여기서 검증
} // 검증 통과 후 엔티티 반영용
// log_message('debug', 'AFTER hook CREATE FORMDATA:' . print_r($formDatas ?? null, true)); foreach ($formDatas as $field => $value) {
$actionForm->validate($formDatas); // ✅ 여기서 검증 $formDatas = $this->fieldhook_process($field, $value, $formDatas);
} }
$entityClass = $this->getEntityClass(); $entityClass = $this->getEntityClass();
$entity = new $entityClass($formDatas); $entity = new $entityClass($formDatas);
if (!$entity instanceof $entityClass) { if (!$entity instanceof $entityClass) {
throw new RuntimeException("Return Type은 {$entityClass}만 가능"); throw new RuntimeException("Return Type은 {$entityClass}만 가능");
} }
$entity->fill($formDatas);
return $this->save_process($entity); return $this->save_process($entity);
} catch (FormValidationException $e) { } catch (FormValidationException $e) {
throw $e; // ✅ 감싸지 말고 그대로 throw $e; // ✅ 감싸지 말고 그대로
@ -286,34 +257,26 @@ abstract class CommonService
final public function create(array $formDatas): CommonEntity final public function create(array $formDatas): CommonEntity
{ {
return $this->dbTransaction(function () use ($formDatas) { return $this->dbTransaction(function () use ($formDatas) {
$formDatas['user_uid'] = $this->getAuthContext()->getUID(); $formDatas['user_uid'] = (int) $this->getAuthContext()->getUID();
return $this->create_process($formDatas); return $this->create_process($formDatas);
}, __FUNCTION__); }, __FUNCTION__);
} }
//수정용 //수정용
protected function save_before_fill(array $formDatas): array
{
return $formDatas;
}
protected function modify_process($entity, array $formDatas): CommonEntity protected function modify_process($entity, array $formDatas): CommonEntity
{ {
try { try {
$actionForm = $this->getActionForm(); $actionForm = $this->getActionForm();
if ($actionForm instanceof CommonForm) { if (!$actionForm instanceof CommonForm) {
$actionForm->action_init_process('modify', $formDatas); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: actionForm이 정의되지 않았습니다.");
// log_message('debug', 'BEFORE hook MODIFY FORMDATA:' . print_r($formDatas ?? null, true));
foreach ($formDatas as $field => $value) {
$formDatas = $this->actionForm_fieldhook_process($field, $value, $formDatas);
}
// log_message('debug', 'AFTER hook MODIFY FORMDATA:' . print_r($formDatas ?? null, true));
$actionForm->validate($formDatas); // ✅ 여기서 검증
} }
$actionForm->action_init_process('modify', $formDatas);
$actionForm->validate($formDatas); // ✅ 여기서 검증
// 검증 통과 후 엔티티 반영 // 검증 통과 후 엔티티 반영
$formDatas = $this->save_before_fill($formDatas); foreach ($formDatas as $field => $value) {
// log_message('debug', 'BEFORE MODIFY fill Entity:' . print_r($formDatas ?? null, true)); $formDatas = $this->fieldhook_process($field, $value, $formDatas);
}
$entity->fill($formDatas); $entity->fill($formDatas);
// log_message('debug', 'AFTER MODIFY fill Entity:' . print_r($entity ?? null, true));
if (!$entity->hasChanged()) { if (!$entity->hasChanged()) {
return $entity; return $entity;
} }
@ -325,6 +288,7 @@ abstract class CommonService
} }
} }
final public function modify(string|int $uid, array $formDatas): CommonEntity final public function modify(string|int $uid, array $formDatas): CommonEntity
{ {
return $this->dbTransaction(function () use ($uid, $formDatas) { return $this->dbTransaction(function () use ($uid, $formDatas) {
@ -332,7 +296,7 @@ abstract class CommonService
if (!$entity) { if (!$entity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다."); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다.");
} }
$formDatas['user_uid'] = $this->getAuthContext()->getUID(); $formDatas['user_uid'] = (int) $this->getAuthContext()->getUID();
return $this->modify_process($entity, $formDatas); return $this->modify_process($entity, $formDatas);
}, __FUNCTION__); }, __FUNCTION__);
} }

View File

@ -7,7 +7,6 @@ use App\Helpers\UserHelper;
use App\Forms\UserForm; use App\Forms\UserForm;
use App\Entities\UserEntity; use App\Entities\UserEntity;
class UserService extends CommonService class UserService extends CommonService
{ {
protected string $formClass = UserForm::class; protected string $formClass = UserForm::class;
@ -27,44 +26,31 @@ class UserService extends CommonService
{ {
return $entity; return $entity;
} }
protected function actionForm_fieldhook_process(string $field, $value, array $formDatas): array protected function fieldhook_process(string $field, $value, array $formDatas): array
{ {
switch ($field) { switch ($field) {
case 'role': case 'role':
if (is_string($value)) { $arr = is_array($value) ? $value : explode(',', (string) $value);
$formDatas['role'] = explode(',', $value) ?? []; $arr = array_values(array_filter(array_map('trim', $arr)));
sort($arr);
$formDatas[$field] = implode(',', $arr);
break;
case 'passwd':
if ($formDatas[$field] !== '') {
$formDatas[$field] = password_hash($value, PASSWORD_BCRYPT);
} else {
unset($formDatas[$field]);
} }
break; break;
case 'confirmpassword':
unset($formDatas['confirmpassword']);
break;
default: default:
$formDatas = parent::actionForm_fieldhook_process($field, $value, $formDatas); $formDatas = parent::fieldhook_process($field, $value, $formDatas);
break; break;
} }
return $formDatas; return $formDatas;
} }
protected function save_before_fill(array $formDatas): array
{
// 1) DB 컬럼 아닌 값 제거
unset($formDatas['confirmpassword']);
// 2) role은 무조건 문자열로
if (array_key_exists('role', $formDatas)) {
$arr = is_array($formDatas['role'])
? $formDatas['role']
: explode(',', (string) $formDatas['role']);
$arr = array_values(array_filter(array_map('trim', $arr)));
sort($arr);
$formDatas['role'] = implode(',', $arr);
}
// 3) passwd는 빈 값이면 업데이트 제외 (원하면)
if (array_key_exists('passwd', $formDatas) && $formDatas['passwd'] === '') {
unset($formDatas['passwd']);
}
return $formDatas;
}
//List 검색용 //List 검색용
//FormFilter 조건절 처리 //FormFilter 조건절 처리
public function setFilter(string $field, mixed $filter_value): void public function setFilter(string $field, mixed $filter_value): void