addClassPaths('Client'); } public function getEntityClass(): string { return ClientEntity::class; } //기본 기능부분 protected function getEntity_process(mixed $entity): ClientEntity { return $entity; } //List 검색용 //FormFilter 조건절 처리 //검색어조건절처리 //OrderBy 처리 public function setOrderBy(mixed $field = null, mixed $value = null): void { $this->model->orderBy("site ASC,name ASC"); parent::setOrderBy($field, $value); } protected function fieldhook_process(string $field, $value, array $formDatas): array { switch ($field) { case 'role': $arr = is_array($value) ? $value : explode(',', (string) $value); $arr = array_values(array_filter(array_map('trim', $arr))); sort($arr); $formDatas[$field] = implode(',', $arr); break; default: $formDatas = parent::fieldhook_process($field, $value, $formDatas); break; } return $formDatas; } public function history(string|int $uid, string $history): CommonEntity { return $this->dbTransaction(function () use ($uid, $history) { $entity = $this->getEntity($uid); if (!$entity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다."); } $formDatas['user_uid'] = (int) $this->getAuthContext()->getUID(); $formDatas['history'] = $history; // 검증 통과 후 엔티티 반영 $entity->fill($formDatas); if (!$entity->hasChanged()) { return $entity; } return $this->save_process($entity); }, __FUNCTION__); } /** * ✅ 원자 증가 * - DB에서 field = field + amount 로 처리 * - (선택) 음수로 내려가면 안되면 아래에서 막거나 decreaseBalanceIfEnough 사용 */ public function increaseBalance(int $pk, string $field, int $amount): void { //field가 예치금 , 쿠폰 , 포인트에 해당하는 것인지 확인 if (!in_array($field, $this->balanceFields, true)) { throw new RuntimeException(static::class . "->" . __FUNCTION__ . "에서 오류발생: 허용되지 않은 field={$field}"); } if ($amount === 0) { return; } // update()는 set()에서 RawSQL/escape=false 사용 가능 $builder = $this->model->builder(); $builder->where($this->model->primaryKey, $pk); // field = field + (amount) $expr = "{$field} + (" . (int) $amount . ")"; $ok = $builder->set($field, $expr, false)->update(); if (!$ok) { $errors = $this->model->errors(); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생[{$field}]: " . (is_array($errors) ? implode(", ", $errors) : 'DB update 실패')); } // 영향 받은 row가 없으면 PK가 없거나 update 실패 if ($this->model->db->affectedRows() < 1) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 대상 PK({$pk}) 업데이트 실패"); } } /** * ✅ 출금(감소) - 잔액이 충분할 때만 감소 (원자 처리) * - where field >= amount 조건으로 동시에 들어와도 안전 * - 성공 true / 잔액부족 false */ public function decreaseBalance(int $pk, string $field, int $amount): bool { //field가 예치금 , 쿠폰 , 포인트에 해당하는 것인지 확인 if (!in_array($field, $this->balanceFields, true)) { throw new RuntimeException(static::class . "->" . __FUNCTION__ . "에서 오류발생: 허용되지 않은 field={$field}"); } if ($amount < 0) { throw new RuntimeException(static::class . "->" . __FUNCTION__ . "에서 오류발생: balance는 0 이상이어야 합니다."); } if ($amount === 0) { return true; } $builder = $this->model->builder(); $builder->where($this->model->primaryKey, $pk); $builder->where("{$field} >=", (int) $amount); // field = field - amount $expr = "{$field} - (" . (int) $amount . ")"; $ok = $builder->set($field, $expr, false)->update(); if (!$ok) { $errors = $this->model->errors(); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생[{$field}]: " . (is_array($errors) ? implode(", ", $errors) : 'DB update 실패')); } // 조건을 만족하지 못하면 affectedRows=0 => 잔액 부족 또는 PK 없음 return ($this->model->db->affectedRows() > 0); } }