diff --git a/app/Config/Constants.php b/app/Config/Constants.php index 3099215..8837c71 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -384,3 +384,16 @@ define("STATUS", [ 'PAID' => 'paid', 'UNPAID' => 'unpaid', ]); + +//ROLE +define("ROLE", [ + 'USER' => [ + 'MANAGER' => "manager", + 'CLOUDFLARE' => "cloudflare", + 'FIREWALL' => "firewall", + 'SECURITY' => "security", + 'DIRECTOR' => "director", + 'MASTER' => "master", + ], + ['CLIENT'] => [] +]); diff --git a/app/Controllers/Admin/UserController.php b/app/Controllers/Admin/UserController.php index f9e7a66..510f6ae 100644 --- a/app/Controllers/Admin/UserController.php +++ b/app/Controllers/Admin/UserController.php @@ -37,10 +37,23 @@ class UserController extends AdminController return parent::getFormRule($action, $field, $rule); } //Index,FieldForm관련. - protected function create_process(): UserEntity + protected function create_process(): RedirectResponse { //요청 데이터를 DTO 객체로 변환 $dto = new UserDTO($this->request->getPost()); - return $this->service->create($dto); + $entity = $this->service->create($dto); + $redirect_url = $this->authService->popPreviousUrl() ?? implode(DIRECTORY_SEPARATOR, $this->getActionPaths()); + return redirect()->to($redirect_url)->with('success', "{$entity->getTitle()} 계정 생성이 완료되었습니다."); + } + protected function modify_form_process($uid): UserEntity + { + if (!$uid) { + throw new \Exception("계정 번호가 정의 되지 않았습니다."); + } + $entity = $this->service->getEntity($uid); + if (!$entity instanceof UserEntity) { + throw new \Exception("{$uid}에 해당하는 계정을 찾을수 없습니다."); + } + return $entity; } } diff --git a/app/Controllers/CommonController.php b/app/Controllers/CommonController.php index c4f55f7..e04a820 100644 --- a/app/Controllers/CommonController.php +++ b/app/Controllers/CommonController.php @@ -117,7 +117,7 @@ abstract class CommonController extends BaseController return view($full_path, ['viewDatas' => $view_datas]); } protected function create_form_process(): void {} - final public function create_form(): string|RedirectResponse + final public function create_form(): string { $action = __FUNCTION__; try { @@ -138,19 +138,14 @@ abstract class CommonController extends BaseController $this->getViewDatas() ); } - protected function create_process(): mixed - { - return $this->service->create(); - } + abstract protected function create_process(): RedirectResponse; final public function create(): RedirectResponse { $action = __FUNCTION__; try { //초기화 $this->action_init_process($action); - $this->create_process(); - $redirect_url = $this->authService->popPreviousUrl() ?? implode(DIRECTORY_SEPARATOR, $this->getActionPaths()); - return redirect()->to($redirect_url)->with('success', static::class . "/{$action}이 완료되었습니다."); + return $this->create_process(); } catch (ValidationException $e) { // 검증 실패 시 폼으로 돌아가서 오류 메시지 표시 log_message('error', $e->getMessage()); @@ -160,7 +155,29 @@ abstract class CommonController extends BaseController return redirect()->back()->withInput()->with('error', $e->getMessage()); } } - + abstract protected function modify_form_process($uid): mixed; + final public function modify_form($uid): string + { + $action = __FUNCTION__; + try { + //초기화 + $this->action_init_process($action); + $entity = $this->modify_form_process($uid); + $this->addViewDatas('entity', $entity); + $this->action_render_process($action); + // dd($this->getViewDatas()); + } catch (\Exception $e) { + log_message('error', $e->getMessage()); + $this->addViewDatas(self::ACTION_RESULT, 'error'); + $this->addViewDatas(self::ACTION_MESSAGE, $e->getMessage()); + //오류발생시 리디렉션 대신 폼 뷰를 다시 렌더링하도록 action_view_process 호출 + } + return $this->action_result_process( + $this->getActionPaths(), + $action, + $this->getViewDatas() + ); + } //리스트관련 //조건절 처리 // protected function index_condition_process(): void diff --git a/app/DTOs/UserDTO.php b/app/DTOs/UserDTO.php index 0a37ab0..fcdf344 100644 --- a/app/DTOs/UserDTO.php +++ b/app/DTOs/UserDTO.php @@ -7,10 +7,11 @@ class UserDTO extends CommonDTO public ?int $uid = null; public ?string $id = null; public ?string $passwd = null; - public ?string $passwd_confirm = null; + public ?string $confirmpassword = null; + public ?string $name = null; public ?string $email = null; public ?string $mobile = null; - public ?string $role = null; + public ?string $role = null; public ?string $status = null; public function __construct(array $datas = []) @@ -18,7 +19,12 @@ class UserDTO extends CommonDTO parent::__construct(); foreach ($datas as $key => $value) { if (property_exists($this, $key)) { - $this->{$key} = $value; + if ($key === 'role' && is_array($value)) { + // 배열일 경우, 쉼표로 구분된 문자열로 변환하여 저장 + $this->role = implode(DEFAULTS["DELIMITER_ROLE"], $value); + } else { + $this->{$key} = $value; + } } } } @@ -28,7 +34,8 @@ class UserDTO extends CommonDTO 'uid' => $this->uid, 'id' => $this->id, 'passwd' => $this->passwd, - 'passwd_confirm' => $this->passwd_confirm, + 'confirmpassword' => $this->confirmpassword, + 'name' => $this->name, 'email' => $this->email, 'mobile' => $this->mobile, 'role' => $this->role, diff --git a/app/Entities/UserEntity.php b/app/Entities/UserEntity.php index 5fdce70..57403ce 100644 --- a/app/Entities/UserEntity.php +++ b/app/Entities/UserEntity.php @@ -9,18 +9,38 @@ class UserEntity extends CommonEntity { const PK = Model::PK; const TITLE = Model::TITLE; - const DEFAULT_STATUS = STATUS['AVAILABLE']; + const DEFAULT_STATUS = STATUS['AVAILABLE']; + + // 💡 1. $casts 속성 추가: + // DB에 JSON 형태로 저장된 'role' 컬럼을 PHP에서 배열로 자동 변환합니다. + protected $casts = [ + 'role' => 'json-array', + ]; public function getID(): string { - return $this->attributes['id']; + return (string) $this->attributes['id']; } + public function getPassword(): string { return $this->attributes['passwd']; } - public function getRole(): string + // $formDatas['passwd']에 평문 비밀번호가 들어있으면, + //Model->insert시 Entity 생성자($formDatas)가 setPasswd()를 자동으로 호출합니다. + public function setPasswd(string $password) { - return $this->attributes['role']; + $this->attributes['passwd'] = password_hash($password, PASSWORD_BCRYPT); + } + /** + * 사용자의 역할을 배열 형태로 반환합니다. + * $casts에 의해 DB에서 읽어올 때 이미 배열로 변환되어 있습니다. + * @return array + */ + public function getRole(): array + { + // 💡 2. 반환 타입을 array로 변경하고, + // null일 경우를 대비해 빈 배열을 반환하도록 처리합니다. + return $this->attributes['role'] ?? []; } } diff --git a/app/Helpers/UserHelper.php b/app/Helpers/UserHelper.php index 9604921..b849a02 100644 --- a/app/Helpers/UserHelper.php +++ b/app/Helpers/UserHelper.php @@ -15,6 +15,29 @@ class UserHelper extends CommonHelper case 'confirmpassword': $form = form_password($field, "", [...$extras]); break; + case 'role': + // 💡 $value는 수정 폼 로드시 현재 Entity의 'role' 배열입니다. + // (old() 값이 있다면 old() 배열이 됩니다.) + $currentRoles = is_array($value) ? $value : []; + // 사용 가능한 모든 역할 목록 (Service 등에서 가져온다고 가정) + $allRoles = $viewDatas['control']['formOptions']['role'] ?? ROLE['USER']; + $form = ''; + // 2. 각 역할에 대한 체크박스를 순회하며 생성 + foreach ($allRoles as $roleValue => $roleLabel) { + + // $roleLabel이 배열이 아닌 문자열만 있는 경우를 대비 + if (is_int($roleValue)) { + $roleValue = $roleLabel; + $roleLabel = ucfirst($roleValue); + } + $checked = in_array($roleValue, $currentRoles); + // 3. name="role[]" 형태로 배열 데이터 전송 준비 + $form .= ''; + } + break; default: $form = parent::getFieldForm($field, $value, $viewDatas, $extras); break; diff --git a/app/Language/en/User.php b/app/Language/en/User.php index 319c7e3..42a5777 100644 --- a/app/Language/en/User.php +++ b/app/Language/en/User.php @@ -16,12 +16,12 @@ return [ 'deleted_at' => "삭제일", ], "ROLE" => [ - "manager" => "관리자", - "cloudflare" => "Cloudflare관리자", - "firewall" => "firewall관리자", - "security" => "보안관리자", - "director" => "감독자", - "master" => "마스터", + ROLE['USER']["MANAGER"] => "관리자", + ROLE['USER']["CLOUDFLARE"] => "Cloudflare관리자", + ROLE['USER']["FIREWALL"] => "firewall관리자", + ROLE['USER']["SECURITY"] => "보안관리자", + ROLE['USER']["DIRECTOR"] => "감독자", + ROLE['USER']["MASTER"] => "마스터", ], "STATUS" => [ STATUS['AVAILABLE'] => "사용중", diff --git a/app/Models/CommonModel.php b/app/Models/CommonModel.php index 3835201..bd29a71 100644 --- a/app/Models/CommonModel.php +++ b/app/Models/CommonModel.php @@ -64,149 +64,104 @@ abstract class CommonModel extends Model { return $this->useAutoIncrement; } - //Primary Key로 uuid를 사용시 해당 모델에 아래 변수 반드시 추가 필요 - // protected $useAutoIncrement = false; - // protected $beforeInsert = ['generateUUID']; - // allowedFields에는 PK넣으면 않됨, Column Type: CHAR(36) - // - final protected function generateUUID(): string + /** + * Entity 삽입을 처리하고, $useAutoIncrement가 false일 경우 UUID를 자동으로 생성합니다. + * @param object|array $data Entity 객체 또는 데이터 배열 + * @param bool $returnID 삽입 후 ID를 반환할지 여부 (자동 증가 키일 경우 true) + * @return bool|int|string + */ + public function insert($data = null, bool $returnID = true) { - $data = random_bytes(16); - // UUID version 4 (random) - $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // version 0100 - $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // variant 10 - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); - } - public function getFormRule(string $action, string $field): string - { - if (is_array($field)) { - throw new \Exception(__FUNCTION__ . "=> field가 array 입니다.\n" . var_export($field, true)); + // 1. 공통 전처리 로직 (예: 모든 모델에 대한 로그 기록, 공통 필드 설정 등) + $data = $this->preProcessData($data); + + // 2. PK 자동 할당 로직 + if (!$this->useAutoIncrement) { + $data = $this->ensurePrimaryKey($data); } - switch ($field) { - case $this->getPKField(): - // 수동입력인 경우 - if (!$this->useAutoIncrement) { - $rule = "required|regex_match[/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/]"; - $rule .= in_array($action, ["create", "create_form"]) ? "|is_unique[" . $this->getTable() . "." . $field . "]" : ""; - } else { - $rule = "required|numeric"; - } - break; - case $this->getTitleField(): - $rule = "required|trim|string"; - break; - case "code": - // a-zA-Z → 영문 대소문자,0-9 → 숫자,가-힣 → 한글 완성형,\- → 하이픈 - $rule = "required|regex_match[/^[a-zA-Z0-9가-힣\-]+$/]|min_length[4]|max_length[20]"; - $rule .= in_array($action, ["create", "create_form"]) ? "|is_unique[" . $this->getTable() . "." . $field . "]" : ""; - break; - case 'picture': - $rule = "is_image[{$field}]|mime_in[{$field},image/jpg,image/jpeg,image/gif,image/png,image/webp]|max_size[{$field},300]|max_dims[{$field},2048,768]"; - break; - case "updated_at": - case "created_at": - case "deleted_at": - $rule = "permit_empty|valid_date"; - break; - default: - $rule = "permit_empty|trim|string"; - break; - } - return $rule; - } - protected function convertFormDatas(string $action, string $field, array $formDatas): mixed - { - // 필드 값 존재 여부 확인 - $value = array_key_exists($field, $formDatas) ? $formDatas[$field] : null; - switch ($field) { - case $this->getPKField(): - // 수동입력인 경우 - if (!$this->useAutoIncrement) { - $randomBytes = bin2hex(random_bytes(32)); - $value = sprintf( - '%08s-%04s-%04x-%04x-%12s', - substr($randomBytes, 0, 8), - substr($randomBytes, 8, 4), - substr($randomBytes, 12, 4), - substr($randomBytes, 16, 4), - substr($randomBytes, 20, 12) - ); - } - break; - case "editor": - case "detail": - case "content": - case "discription": - case "history": - if ($value !== '' && $value !== null) { - $value = htmlentities($value, ENT_QUOTES, 'UTF-8'); - } - break; - } - return $value; - } - //기본 기능 - public function create(array $formDatas): mixed - { - $convertedFormDatas = []; - foreach ($this->allowedFields as $field) { - $value = $this->convertFormDatas( - __FUNCTION__, - $field, - $formDatas - ); - if ($value !== '' && $value !== null) { - $convertedFormDatas[$field] = $value; - } - } - // 최종 저장 시 오류 발생하면 - // dd($convertedFormDatas); - if (!$this->save($convertedFormDatas)) { - $message = sprintf( - "\n------%s 오류-----\n%s\n%s\n------------------------------\n", - __METHOD__, - var_export($this->errors(), true), - $this->getLastQuery() - ); - log_message('error', $message); - throw new \Exception($message); - } - //Model별 returntype형의 Entity 호출 - if (!class_exists($this->returnType)) { - throw new \RuntimeException(__METHOD__ . "에서 returnType: {$this->returnType}이 정의되지 않았습니다."); - } - $entity = new $this->returnType($convertedFormDatas); - // primaryKey가 자동입력이면 + + // 3. BaseModel의 insert 호출 + // 문자열 PK일 경우, $returnID를 false로 넘겨서 ID 반환을 강제하지 않는 것이 좋습니다. + $baseReturn = parent::insert($data, $this->useAutoIncrement); + + // 4. 후처리 및 반환 if ($this->useAutoIncrement) { - $pkField = $this->getPKField(); - $entity->$pkField = $this->getInsertID(); + // 정수 PK: BaseModel이 반환한 새로운 ID를 그대로 반환 + return $baseReturn; + } else { + // 문자열 PK: BaseModel의 insert는 성공 시 true/1을 반환. true로 통일하여 반환 + return $baseReturn !== false; } - return $entity; } - public function modify(mixed $entity, array $formDatas): mixed + + /** + * Entity나 데이터 배열을 받아서 필요하다면 Primary Key를 생성하여 할당합니다. + * @param object|array $data + * @return object|array + */ + protected function ensurePrimaryKey($data) { - //수정일추가 - $formDatas['updated_at'] = date("Y-m-d H:i:s"); - foreach (array_keys($formDatas) as $field) { - $value = $this->convertFormDatas( - __FUNCTION__, - $field, - $formDatas - ); - if ($entity->$field !== $value) { - $entity->$field = $value; + $pkName = $this->primaryKey; + $entityData = is_object($data) ? $data->attributes : (array) $data; + + // PK가 설정되어 있지 않은 경우 + if (empty($entityData[$pkName])) { + $newUuid = service('uuid')->uuid4()->toString(); + + if (is_object($data)) { + // Entity 객체일 경우 __set()을 통해 Setter를 호출하며 할당 + $data->$pkName = $newUuid; + } else { + // 배열일 경우 배열에 할당 + $data[$pkName] = $newUuid; } } - // 최종 저장 시 오류 발생하면 - if (!$this->save($entity)) { - $message = sprintf( - "\n------%s 오류-----\n%s\n------------------------------\n", - __METHOD__, - var_export($this->errors(), true) - ); - log_message('error', $message); - throw new \Exception($message); - } - return $entity; + return $data; + } + + /** + * 모든 삽입/수정 작업 전 공통 로직을 처리하는 확장 지점. + */ + protected function preProcessData($data) + { + // 예: $data['data']['updated_by'] = session('user_id'); + return $data; + } + + // ****************************************************** + // 2. UPDATE 및 DELETE 확장 + // ****************************************************** + + /** + * BaseModel의 update 메서드를 호출하기 전/후에 공통 로직을 적용하는 확장 지점. + * @param array|int|string|null $id + * @param array|object|null $data + * @return bool + */ + public function update($id = null, $data = null): bool + { + // 1. 업데이트 전 공통 로직 (preUpdate 이벤트) + $data = $this->preProcessData($data); + + // 2. BaseModel의 update 호출 + return parent::update($id, $data); + + // 3. 업데이트 후 공통 로직 (postUpdate 이벤트) + } + + /** + * BaseModel의 delete 메서드를 호출하기 전/후에 공통 로직을 적용하는 확장 지점. + * @param array|int|string|null $id + * @param bool $purge + * @return bool + */ + public function delete($id = null, bool $purge = false): bool + { + // 1. 삭제 전 공통 로직 (preDelete 이벤트) + + // 2. BaseModel의 delete 호출 + return parent::delete($id, $purge); + + // 3. 삭제 후 공통 로직 (postDelete 이벤트) } } diff --git a/app/Models/TrafficModel.php b/app/Models/TrafficModel.php index eb642ee..b17a35e 100644 --- a/app/Models/TrafficModel.php +++ b/app/Models/TrafficModel.php @@ -40,45 +40,6 @@ class TrafficModel extends CommonModel parent::__construct(); } public function getFormRule(string $action, string $field): string - { - if (is_array($field)) { - throw new \Exception(__FUNCTION__ . "=> field가 array 입니다.\n" . var_export($field, true)); - } - switch ($field) { - case "user_uid": - case "clientinfo_uid": - case "serverinfo_uid": - case "amount": - case "rack": - case "line": - $rule = "required|numeric"; - break; - case "sale": - case "payment_uid": - $rule = "permit_empty|numeric"; - break; - case "site": - case "location": - case "status": - $rule = "required|trim|string"; - break; - case "billing_at": - case "start_at": - $rule = "required|valid_date"; - break; - case "end_at": - $rule = "permit_empty|valid_date"; - break; - case 'title': - case "history": - $rule = "permit_empty|trim|string"; - break; - default: - $rule = parent::getFormRule($action, $field); - break; - } - return $rule; - } //입력전 코드처리 final public function create(array $formDatas): ServiceEntity { diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php index 99ffc49..c3c623a 100644 --- a/app/Models/UserModel.php +++ b/app/Models/UserModel.php @@ -27,50 +27,4 @@ class UserModel extends CommonModel { parent::__construct(); } - public function getFormRule(string $action, string $field): string - { - if (is_array($field)) { - throw new \Exception(__FUNCTION__ . "=> field가 array 입니다.\n" . var_export($field, true)); - } - switch ($field) { - case "id": - $rule = "required|trim|min_length[4]|max_length[20]"; - $rule .= in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->table}.{$field}]" : ""; - break; - case "passwd": - $rule = in_array($action, ["create", "create_form"]) ? "required|trim|string" : "permit_empty|trim|string"; - break; - case "confirmpassword": - $rule = in_array($action, ["create", "create_form"]) ? "required|trim|string|matches[passwd]" : "permit_empty|trim|string|matches[passwd]"; - break; - case "email": - $rule = "required|trim|valid_email"; - $rule .= in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->table}.{$field}]" : ""; - break; - case "role": - $rule = "required|trim|string"; - break; - default: - $rule = parent::getFormRule($action, $field); - break; - } - return $rule; - } - protected function convertFormDatas(string $action, string $field, array $formDatas): mixed - { - // 필드 값 존재 여부 확인 - $value = array_key_exists($field, $formDatas) ? $formDatas[$field] : null; - switch ($field) { - case "passwd": - $value = password_hash($value, PASSWORD_DEFAULT); - break; - case "confirmpassword": - $value = password_hash($value, PASSWORD_DEFAULT); - break; - default: - $value = parent::convertFormDatas($action, $field, $formDatas); - break; - } - return $value; - } } diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 6935a15..414f9b6 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -50,13 +50,31 @@ class UserService extends CommonService { // DTO 객체를 배열로 변환하여 검증기에 전달 $formDatas = (array) $dto; + // dd($formDatas); if (!service('validation')->setRules($this->getFormRules(__FUNCTION__))->run($formDatas)) { - // 검증 실패 시, ValidationException을 던집니다. throw new ValidationException(implode("\n", service('validation')->getErrors())); } - $formDatas['role'] = implode(DEFAULTS["DELIMITER_ROLE"], $formDatas['role']); - $entity = $this->model->create($formDatas); - return $entity; + $entity = new UserEntity($formDatas); + $result = $this->model->insert($entity); + if (!$result) { + throw new \Exception("{$entity->getTitle()} 등록 중 DB 오류가 발생하였습니다."); + } + // 💡 PK 타입에 따른 최종 Entity 반환 로직 분기 + if ($this->model->useAutoIncrement) { + // 1. 정수 PK인 경우: $result는 ID 번호이므로, 저장된 레코드를 DB에서 다시 조회합니다. + // (자동 설정된 필드(created_at 등)를 포함하기 위해 재조회) + $savedEntity = $this->model->find($result); + } else { + // 2. 문자열 PK인 경우: $result는 true/1 이므로, + // 이미 ID가 설정된 $entity 객체를 반환하거나, ID로 재조회합니다. + // Entity에 모든 필드(created_at 등)가 설정되지 않았다면 재조회가 안전합니다. + $pkValue = $entity->{$this->model->primaryKey}; + $savedEntity = $this->model->find($pkValue); + } + if (!$savedEntity) { + throw new \Exception("등록된 데이터를 찾을 수 없습니다."); + } + return $savedEntity; } // protected function modify_process(mixed $entity, array $formDatas): UserEntity // { diff --git a/app/Views/admin/user/modify_form.php b/app/Views/admin/user/modify_form.php index 6006c52..e4af212 100644 --- a/app/Views/admin/user/modify_form.php +++ b/app/Views/admin/user/modify_form.php @@ -1,22 +1,23 @@ extend(LAYOUTS[$viewDatas['control']['layout']]['path']) ?> section('content') ?> -getHelper()->alertTrait($error) ?> +alertTrait(session('error')) ?>
include("templates/{$viewDatas['control']['layout']}/form_content_top"); ?>
- 'action_form', ...$viewDatas['forms']['attributes']], $viewDatas['forms']['hiddens']) ?> +
- + +
getHelper()->getFieldLabel($field, lang("{$viewDatas['class_path']}.label.{$field}"), $viewDatas) ?>getFieldLabel($field, "", $viewDatas) ?> - getHelper()->getFieldForm($field, old($field) ?? $viewDatas['entity']->$field ?? null, $viewDatas) ?> -
+ getFieldForm($field, old($field) ?? ($viewDatas['control']['entity']->$field ?? null), $viewDatas) ?> +
-
"btn btn-outline btn-primary"]) ?>
+
"btn btn-outline btn-primary")); ?>
include("templates/{$viewDatas['control']['layout']}/form_content_bottom"); ?>