daemon-idc init
This commit is contained in:
parent
a8b50340ac
commit
209b86180c
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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__);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user