dbmsv4 init...5

This commit is contained in:
최준흠 2026-03-10 18:32:35 +09:00
parent 4a1b40c39f
commit 43dd7caf02
7 changed files with 262 additions and 357 deletions

View File

@ -180,34 +180,6 @@ abstract class CommonForm
return $data;
}
/**
* 2) 숫자/FK 필드 정규화
* - 폼에서 미선택은 보통 '' 들어옴 -> NULL로 변환
* - 숫자 문자열은 int 캐스팅 (선택)
*
* 주의:
* - "빈값을 0으로 취급" 같은 정책이 있다면 여기에서 조정해야 .
*/
protected function normalizeNumericEmptyToNull(array $data, array $numericFields): array
{
foreach ($numericFields as $f) {
if (!array_key_exists($f, $data)) {
continue;
}
if ($data[$f] === '') {
$data[$f] = null;
continue;
}
if (is_string($data[$f]) && ctype_digit($data[$f])) {
$data[$f] = (int) $data[$f];
}
}
return $data;
}
/**
* 3) role.* 같은 배열 원소 규칙이 있을 , 부모 배열 존재/타입 보정
*/
@ -259,38 +231,6 @@ abstract class CommonForm
}
}
/**
* 4) 검증 rule에 따라 "numeric(특히 FK)" 취급할 필드를 수집
* - getFormRule()에서 permit_empty|numeric 정의되는 필드를 공통 처리하기 위함
*
* 구현 전략:
* - formRules에서 rule 문자열에 'numeric' 포함된 필드를 모음
* - wildcard(role.*) 제외
*/
protected function collectNumericFieldsFromRules(array $formRules): array
{
$numericFields = [];
foreach ($formRules as $field => $rule) {
$fieldName = (string) $field;
if (str_contains($fieldName, '.*')) {
continue;
}
// getValidationRule hook 적용 (필드명/룰이 바뀔 수 있으니)
[$fieldName, $ruleStr] = $this->getValidationRule($fieldName, (string) $rule);
if (is_string($ruleStr) && str_contains($ruleStr, 'numeric')) {
$numericFields[] = $fieldName;
}
}
// 중복 제거
return array_values(array_unique($numericFields));
}
/* ---------------------------------------------------------------------
* Validation
* --------------------------------------------------------------------- */
@ -299,13 +239,8 @@ abstract class CommonForm
* 데이터를 검증하고 유효하지 않을 경우 예외를 발생시킵니다.
* 2025 CI4 표준: 규칙 배열 내에 label을 포함하여 한글 메시지 출력을 보장합니다.
*/
final public function validate(array &$formDatas): void
public function validate_process(array &$formDatas, $isDebug = false): array
{
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class . ', formAction:' . $this->formAction);
try {
// 0) 데이터 구조 정리 (null 변환 X)
$formDatas = $this->sanitizeFormDatas($formDatas);
// 1) 전체 라벨/룰
$allFormFields = $this->getFormFields(); // ['field' => '라벨', ...]
$allFormRules = $this->getFormRules(); // ['field' => 'rules', ...]
@ -313,54 +248,61 @@ abstract class CommonForm
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
}
// 2) 액션별 "검증 대상 필드" 결정
$formRules = $allFormRules;
$formFields = $allFormFields;
if ($this->formAction === 'modify') {
// (1) formDatas에 실제로 넘어온 필드만
$targetFields = array_keys($formDatas);
// (2) 내부 제어용 키 제거(프로젝트에 맞게 추가/삭제)
$exclude = ['_method', 'csrf_test_name', 'submit', 'token', 'action'];
$targetFields = array_values(array_diff($targetFields, $exclude));
// (3) wildcard(role.*) 같은 규칙이 있으면 부모 기반으로 같이 포함
// - formDatas에 role이 있으면 role.* 규칙도 함께 검사되도록 추가
foreach ($allFormRules as $ruleField => $_ruleStr) {
$ruleField = (string) $ruleField;
if (!str_contains($ruleField, '.*')) {
continue;
if ($isDebug) {
log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate targetFields Begin DEBUG");
log_message('debug', var_export($targetFields, true));
}
$parent = str_replace('.*', '', $ruleField);
if (in_array($parent, $targetFields, true)) {
$targetFields[] = $ruleField; // e.g. 'role.*'
// // (2) 내부 제어용 키 제거(프로젝트에 맞게 추가/삭제)
// $exclude = ['_method', 'csrf_test_name', 'submit', 'token', 'action'];
// $targetFields = array_values(array_diff($targetFields, $exclude));
// // (3) wildcard(role.*) 같은 규칙이 있으면 부모 기반으로 같이 포함
// // - formDatas에 role이 있으면 role.* 규칙도 함께 검사되도록 추가
// foreach ($allFormRules as $ruleField => $_ruleStr) {
// $ruleField = (string) $ruleField;
// if (!str_contains($ruleField, '.*')) {
// continue;
// }
// $parent = str_replace('.*', '', $ruleField);
// if (in_array($parent, $targetFields, true)) {
// $targetFields[] = $ruleField; // e.g. 'role.*'
// }
// }
// // (4) 실제로 룰이 정의된 필드만 남김
// $targetFields = array_values(array_intersect($targetFields, array_keys($allFormRules)));
if ($isDebug) {
log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate targetFields End DEBUG");
log_message('debug', var_export($targetFields, true));
}
}
// (4) 실제로 룰이 정의된 필드만 남김
$targetFields = array_values(array_intersect($targetFields, array_keys($allFormRules)));
// 최종: modify에서는 "타겟만" 룰/라벨 세팅
$formRules = $this->getFormRules($targetFields);
$formFields = $this->getFormFields($targetFields);
$formRules = $this->getFormRules($targetFields);
// throw new RuntimeException(static::class . "->targetFields" . implode(",", $targetFields) . "\nformRules:" . implode(",", array_keys($formRules)) . "\nformFields:" . implode(",", array_keys($formFields)));
}//modify
// 2) 액션별 "검증 대상 필드" 결정
// $formFields = $allFormFields;
// $formRules = $allFormRules;
return array($formFields, $formRules, $allFormFields);
}
final public function validate(array &$formDatas, $isDebug = false): void
{
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class . ', formAction:' . $this->formAction);
try {
// 0) 데이터 구조 정리 (null 변환 X)
$formDatas = $this->sanitizeFormDatas($formDatas);
list($formFields, $formRules, $allFormFields) = $this->validate_process($formDatas, $isDebug);
if (empty($formRules)) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 검증할 대상 RULE이 없습니다.");
}
// 3) wildcard(role.*) 부모 배열 보정
$this->ensureParentArrayForWildcardRules($formDatas, $formRules);
// 4) numeric(FK 포함) 필드: '' -> null, 숫자 문자열 -> int
$numericFields = $this->collectNumericFieldsFromRules($formRules);
$formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields);
// 5) dynamicRules 구성
$dynamicRules = [];
foreach ($formRules as $field => $rule) {
[$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
// label 결정
if (isset($formFields[$fieldName])) {
$label = $formFields[$fieldName];
@ -370,13 +312,18 @@ abstract class CommonForm
} else {
$label = $fieldName;
}
$dynamicRules[$fieldName] = [
'label' => $label,
'rules' => $ruleStr,
];
}
if ($isDebug) {
log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate dynamicRules DEBUG");
log_message('debug', var_export($formDatas, true));
log_message('debug', var_export($dynamicRules, true));
}
$this->_validation->setRules($dynamicRules);
if (!$this->_validation->run($formDatas)) {
$errors = $this->_validation->getErrors();
@ -394,7 +341,7 @@ abstract class CommonForm
if ($e instanceof RuntimeException) {
throw $e;
}
throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
// throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
}
}

View File

@ -43,6 +43,9 @@ class ServiceForm extends CustomerForm
'status'
];
switch ($action) {
case 'modify':
$fields = [...$fields, 'amount'];
break;
case 'view':
$fields = [...$fields, 'created_at'];
break;

View File

@ -232,6 +232,10 @@ abstract class CommonService
}
//생성용
protected function create_process_validate(CommonForm &$actionForm, array $formDatas)
{
$actionForm->validate($formDatas); // ✅ 여기서 검증
}
protected function create_process(array $formDatas): CommonEntity
{
try {
@ -240,7 +244,7 @@ abstract class CommonService
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: actionForm이 정의되지 않았습니다.");
}
$actionForm->form_init_process('create', $formDatas);
$actionForm->validate($formDatas); // ✅ 여기서 검증
$this->create_process_validate($actionForm, $formDatas);
// 검증 통과 후 엔티티 반영용
foreach ($formDatas as $field => $value) {
$formDatas = $this->fieldhook_process($field, $value, $formDatas);
@ -273,6 +277,10 @@ abstract class CommonService
}
//수정용
protected function modify_process_validate(CommonForm &$actionForm, array $formDatas)
{
$actionForm->validate($formDatas); // ✅ 여기서 검증
}
protected function modify_process($entity, array $formDatas): CommonEntity
{
try {
@ -281,7 +289,7 @@ abstract class CommonService
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: actionForm이 정의되지 않았습니다.");
}
$actionForm->form_init_process('modify', $formDatas);
$actionForm->validate($formDatas); // ✅ 여기서 검증
$this->modify_process_validate($actionForm, $formDatas);
// 검증 통과 후 엔티티 반영
foreach ($formDatas as $field => $value) {
$formDatas = $this->fieldhook_process($field, $value, $formDatas);

View File

@ -2,14 +2,15 @@
namespace App\Services\Customer;
use App\Entities\CommonEntity;
use App\Entities\Customer\ServiceEntity;
use App\Forms\CommonForm;
use App\Forms\Customer\ServiceForm;
use App\Helpers\Customer\ServiceHelper;
use App\Models\Customer\ServiceModel;
use DateTimeImmutable;
use DateTimeZone;
use RuntimeException;
use DateTimeImmutable;
use App\Entities\CommonEntity;
use App\Forms\Customer\ServiceForm;
use App\Models\Customer\ServiceModel;
use App\Helpers\Customer\ServiceHelper;
use App\Entities\Customer\ServiceEntity;
class ServiceService extends CustomerService
{
@ -61,46 +62,31 @@ class ServiceService extends CustomerService
return $date->format('Y-m-d');
}
private function getCalculatedAmount(int $rack_price, int $line_price, int $sale_price, int $serverinfo_uid): int
{
$server_amount = service('equipment_serverservice')->getCalculatedAmount($serverinfo_uid);
return (int) $server_amount + $rack_price + $line_price - $sale_price;
}
final protected function updateAmount(ServiceEntity $entity): ServiceEntity
{
$serverUid = $entity->getServerInfoUid();
// ✅ 서버 미지정(해지/분리 상태) 방어: 계산 시도 자체를 막는다
if (empty($serverUid)) { // null, 0, '' 모두 방어
throw new RuntimeException(
static::class . '->' . __FUNCTION__
. " 오류: 서비스({$entity->getPK()})에 serverinfo_uid가 없습니다. (해지/분리 상태)"
);
}
$entity->amount = $this->getCalculatedAmount(
$entity->getRack(),
$entity->getLine(),
$entity->getSale(),
(int) $serverUid
);
// dd($entity);
if (!$this->model->save($entity)) {
throw new RuntimeException("금액 업데이트 중 DB 저장 오류: " . implode(', ', $this->model->errors()));
}
return $entity;
}
final public function updateBillingAt($uid, string $billing_at): ServiceEntity
final public function updateBillingAt(int $uid, string $billing_at): CommonEntity
{
$entity = $this->getEntity($uid);
if (!$entity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:{$uid}에 해당하는 서비스정보를 찾을 수 업습니다.");
}
$formDatas = ['billing_at' => $billing_at];
return parent::modify_process($entity, $formDatas);
}
$entity->billing_at = $billing_at;
if (!$this->model->save($entity)) {
$errors = $this->model->errors();
throw new RuntimeException("금액 업데이트 중 DB 저장 오류: " . implode(', ', $errors));
//서비스 관련 총 금액
private function getCalculatedAmount(ServiceEntity $entity): int
{
//서버 관련 금액
$server_amount = service('equipment_serverservice')->getCalculatedAmount($entity->getServerInfoUid());
return (int) $server_amount + (int) $entity->getRack() + (int) $entity->getLine() - $entity->getSale();
}
//서비스 금액 설정
final public function recalcAmount(ServiceEntity $entity): ServiceEntity
{
$formDatas = ['amount' => $this->getCalculatedAmount($entity)];
$entity = parent::modify_process($entity, $formDatas);
if (!$entity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
return $entity;
}
@ -110,74 +96,74 @@ class ServiceService extends CustomerService
return $entity;
}
// ✅ (internal) payment까지 동기화하는 일반용 로직 (트랜잭션 없음)
final public function recalcAmountAndSyncPaymentInternal(ServiceEntity $old, ServiceEntity $current): ServiceEntity
{
$current = $this->updateAmount($current);
service('paymentservice')->setByService($old, $current);
return $current;
}
// ✅ (internal) amount만 재계산 (해지 시 0원될 수 있으나 payment 건드리지 않음)
final public function recalcAmountInternal(ServiceEntity $current): ServiceEntity
{
return $this->updateAmount($current);
}
// ✅ (public) dbTransaction 보류: 내부 로직만 호출
final public function recalcAmountAndSyncPayment(ServiceEntity $serviceEntity): ServiceEntity
{
$old = clone $serviceEntity;
return $this->recalcAmountAndSyncPaymentInternal($old, $serviceEntity);
}
final public function recalcAmountAndSyncPaymentByOld(ServiceEntity $old, ServiceEntity $current): ServiceEntity
{
return $this->recalcAmountAndSyncPaymentInternal($old, $current);
}
protected function create_process(array $formDatas): ServiceEntity
{
if (empty($formDatas['site'])) {
if (!array_key_exists('site', $formDatas) || empty($formDatas['site'])) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 사이트가 지정되지 않았습니다.");
}
if (empty($formDatas['serverinfo_uid'])) {
if (!array_key_exists('serverinfo_uid', $formDatas) || empty($formDatas['serverinfo_uid'])) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 서버가 지정되지 않았습니다.");
}
$formDatas['code'] = $formDatas['site'] . "_s" . uniqid();
$formDatas['amount'] = 0;
$entity = parent::create_process($formDatas);
if (!$entity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
//서버 설정
service('equipment_serverservice')->attatchToService($entity, $entity->getServerInfoUid());
return $this->recalcAmountAndSyncPayment($entity);
//서비스 금액 설정
$entity = $this->recalcAmount($entity);
//결제 추가
service('paymentservice')->createByService($entity);
return $entity;
}
protected function modify_process_validate(CommonForm &$actionForm, array $formDatas)
{
$actionForm->validate($formDatas, true); // ✅ 여기서 검증
}
protected function modify_process($entity, array $formDatas): ServiceEntity
{
if (empty($formDatas['serverinfo_uid'])) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 서버가 지정되지 않았습니다.");
}
//기존 정보 저장
$oldEntity = clone $entity;
$formDatas['code'] = $entity->getCode();
$formDatas['amount'] = 0;
$entity = parent::modify_process($entity, $formDatas);
if (!$entity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
//서버 설정
if ($oldEntity->getServerInfoUid() !== $entity->getServerInfoUid()) {
service('equipment_serverservice')->modifyByService($oldEntity, $entity);
}
//서비스 금액 설정
$entity = $this->recalcAmount($entity);
//결제비 설정
service('paymentservice')->modifyByService($oldEntity, $entity);
return $entity;
}
return $this->recalcAmountAndSyncPaymentByOld($oldEntity, $entity);
protected function delete_process($entity): ServiceEntity
{
if (!$entity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
//기존정보 우선 저장
$oldEntity = clone $entity;
$entity = parent::delete_process($entity);
if (!$entity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
if ($oldEntity->getServerInfoUid()) {
service('equipment_serverservice')->deatchFromService($oldEntity->getServerInfoUid());
}
return $oldEntity;
}
public function history(string|int $uid, string $history): CommonEntity

View File

@ -93,55 +93,18 @@ class ServerPartService extends EquipmentService
return $formDatas;
}
/**
* MONTH 파트 변경 :
* - "서비스가 유지중"이면: (serverinfo_uid 기준) 현재 서버가 붙은 서비스로 amount 재계산 + Payment upsert
* - "서버가 서비스에서 분리된 상태(server.serviceinfo_uid==null)"이면: 미납 결제 status=TERMINATED (amount 재계산 금지)
*
* 핵심: ServerPartService는 'serviceUid' 아니라 'serverUid' 기준으로 판단한다.
*/
private function syncMonthlyServiceAndPaymentByServer(
int $serverUid,
?ServiceEntity $oldServiceSnapshot = null,
bool $serverDetached = false
): void {
$server = service('equipment_serverservice')->getEntity($serverUid);
if (!$server instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$serverUid} 서버정보를 찾을수 없습니다.");
//서비스 금액 재계산용
private function recalcAmount(ServerPartEntity $entity): void
{
//월비용 서버파트 정보일 경우 서버금액 재계산
if ($entity->getBilling() == PAYMENT['BILLING']['MONTH'] && $entity->getServerInfoUid()) {
$serverEntity = service('equipment_serverservice')->getEntity($entity->getServerInfoUid());
if ($serverEntity instanceof ServerEntity) {
service('equipment_serverservice')->recalcAmount($serverEntity);
}
// ✅ 해지 케이스
if ($serverDetached) {
if ($oldServiceSnapshot instanceof ServiceEntity) {
service('paymentservice')->terminateUnpaidMonthlyByService($oldServiceSnapshot);
}
return;
}
$serviceUid = $server->getServiceInfoUid();
if (!$serviceUid) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 서비스가 정의되지 않은 서버정보입니다.");
}
$svcService = service('customer_serviceservice');
$current = $svcService->getEntity($serviceUid);
if (!$current instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$serviceUid} 서비스정보를 찾을수 없습니다.");
}
// ✅ 핵심 추가: 메인서버만 amount/payment 동기화 허용
if ((int) $current->getServerInfoUid() !== (int) $serverUid) {
// 대체서버(alternative)에서는 서비스금액/결제 동기화 금지
return; // 또는 정책상 필요하면 예외로 변경 가능
// throw new RuntimeException("메인서버가 아닌 서버({$serverUid})에서 월동기화 금지");
}
// ✅ 일반 케이스: amount + payment upsert
$old = $oldServiceSnapshot instanceof ServiceEntity ? $oldServiceSnapshot : clone $current;
$svcService->recalcAmountAndSyncPaymentInternal($old, $current);
}
protected function create_process(array $formDatas): CommonEntity
{
$serverEntity = service('equipment_serverservice')->getEntity($formDatas['serverinfo_uid']);
@ -162,7 +125,7 @@ class ServerPartService extends EquipmentService
// ✅ 서버가 서비스에 붙어 있을 때만 결제/동기화
if ($entity->getServiceInfoUid()) {
if ($entity->getBilling() == PAYMENT['BILLING']['MONTH']) {
$this->syncMonthlyServiceAndPaymentByServer((int) $entity->getServerInfoUid());
$this->recalcAmount($entity);
}
if ($entity->getBilling() == PAYMENT['BILLING']['ONETIME']) {
service('paymentservice')->createByServerPart($entity);
@ -178,45 +141,34 @@ class ServerPartService extends EquipmentService
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$formDatas['serverinfo_uid']}에 해당하는 서버정보을 찾을수 없습니다.");
}
$formDatas = $this->getFormDatasForServerPart($formDatas, $serverEntity);
//기존정보 우선 저장
$oldEntity = clone $entity;
$entity = parent::modify_process($entity, $formDatas);
$entity = parent::modify_process($entity, $this->getFormDatasForServerPart($formDatas, $serverEntity));
if (!$entity instanceof ServerPartEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Return Type은 ServerPartEntity만 가능");
}
$this->getPartService($entity->getType())->modifyServerPart($oldEntity, $entity);
// ✅ MONTH 동기화는 serviceinfo_uid가 아니라 serverinfo_uid 기준
$serverUidsToSync = [];
if ($oldEntity->getBilling() == PAYMENT['BILLING']['MONTH'] && $oldEntity->getServerInfoUid()) {
$serverUidsToSync[(int) $oldEntity->getServerInfoUid()] = true;
}
// ✅ 월별용 처리 (서버파트가 변경되었으므로 서버파트의 Billing이 MONTH이면 서비스/결제 동기화)
if ($entity->getBilling() == PAYMENT['BILLING']['MONTH'] && $entity->getServerInfoUid()) {
$serverUidsToSync[(int) $entity->getServerInfoUid()] = true;
$this->recalcAmount($entity);
}
foreach (array_keys($serverUidsToSync) as $serverUid) {
$this->syncMonthlyServiceAndPaymentByServer((int) $serverUid);
}
// ✅ 일회성용 처리
if ($entity->getBilling() == PAYMENT['BILLING']['ONETIME']) {
service('paymentservice')->modifyByServerPart($oldEntity, $entity);
}
return $entity;
}
protected function delete_process($entity): CommonEntity
protected function delete_process($entity): ServerPartEntity
{
if (!$entity instanceof ServerPartEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: ServerPartEntity만 가능");
}
$old = clone $entity;
//기존정보 우선 저장
$oldEntity = clone $entity;
$this->getPartService($entity->getType())->detachFromServerPart($entity);
@ -224,12 +176,10 @@ class ServerPartService extends EquipmentService
if (!$entity instanceof ServerPartEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Return Type은 ServerPartEntity만 가능");
}
// ✅ MONTH면 서버 기준으로 서비스/결제 동기화
if ($old->getBilling() == PAYMENT['BILLING']['MONTH'] && $old->getServerInfoUid()) {
$this->syncMonthlyServiceAndPaymentByServer((int) $old->getServerInfoUid());
// ✅서버파트가 삭제되었으므로 서버파트의 Billing이 MONTH이면 서비스/결제 동기화
if ($oldEntity->getBilling() == PAYMENT['BILLING']['MONTH'] && $oldEntity->getServerInfoUid()) {
$this->recalcAmount($oldEntity);
}
// ✅ ONETIME 미납 결제 삭제는 "보류" (여기서는 아무것도 안함)
return $entity;
}
@ -268,37 +218,21 @@ class ServerPartService extends EquipmentService
/**
* 서버 해지/분리 서버파트 회수 처리
* - BASE 제외 전부 삭제(정책상 OK)
* - MONTH 삭제가 있었다면:
* - server.serviceinfo_uid == null 상태에서만 TERMINATED 처리 (amount 재계산 금지)
* - ONETIME 미납 삭제는 보류
*
* @param ServiceEntity|null $oldServiceEntity 분리 서비스 스냅샷(권장: TERMINATED 매칭용)
*/
public function detachFromServer(ServerEntity $serverEntity, ?ServiceEntity $oldServiceEntity = null): void
public function detachFromServer(ServerEntity $serverEntity): void
{
$monthlyChanged = false;
foreach ($this->getEntities(['serverinfo_uid' => $serverEntity->getPK(), "billing !=" => PAYMENT['BILLING']['BASE']]) as $entity) {
if (!$entity instanceof ServerPartEntity)
if (!$entity instanceof ServerPartEntity) {
continue;
}
//파트정보 해지
$this->getPartService($entity->getType())->detachFromServerPart($entity);
if ($entity->getBilling() == PAYMENT['BILLING']['MONTH']) {
$monthlyChanged = true;
}
//서버파트 해지
parent::delete_process($entity);
//서비스금액 재계산
if ($entity->getBilling() == PAYMENT['BILLING']['MONTH'] && $entity->getServerInfoUid()) {
$this->recalcAmount($entity);
}
if (!$monthlyChanged) {
return;
}
// ✅ 분리 완료 후(server.serviceinfo_uid == null)에서만 TERMINATED 처리
$serverDetached = ($serverEntity->getServiceInfoUid() === null);
if ($serverDetached) {
if ($oldServiceEntity instanceof ServiceEntity) {
$this->syncMonthlyServiceAndPaymentByServer((int) $serverEntity->getPK(), $oldServiceEntity, true);
}
return;
}
// ✅ (이 케이스는 정상 플로우에서는 거의 없지만) 서비스 유지중이면 정상 upsert
$this->syncMonthlyServiceAndPaymentByServer((int) $serverEntity->getPK());
}
}

View File

@ -25,6 +25,12 @@ class ServerService extends EquipmentService
return ServerEntity::class;
}
protected function getEntity_process(mixed $entity): ServerEntity
{
return $entity;
}
final public function getTotalServiceCount(array $where = []): array
{
$totalCounts = [
@ -100,18 +106,29 @@ class ServerService extends EquipmentService
if (!$entity instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid} 서버 정보를 찾을수 없습니다.");
}
$serverPartService = service('equipment_serverpartservice');
$caculatedAmount = $entity->getPrice();
foreach ($serverPartService->getEntities(['serverinfo_uid' => $entity->getPK(), 'billing' => PAYMENT['BILLING']['MONTH']]) as $serverPartEntity) {
foreach (service('equipment_serverpartservice')->getEntities(['serverinfo_uid' => $entity->getPK(), 'billing' => PAYMENT['BILLING']['MONTH']]) as $serverPartEntity) {
log_message('debug', $serverPartEntity->getCustomTitle() . '::' . $serverPartEntity->getCalculatedAmount());
$caculatedAmount += $serverPartEntity->getCalculatedAmount();
}
return $caculatedAmount;
}
protected function getEntity_process(mixed $entity): ServerEntity
public function recalcAmount(ServerEntity $entity): void
{
return $entity;
if ($entity->getServiceInfoUid()) {
$serviceEntity = service('customer_serviceservice')->getEntity($entity->getServiceInfoUid());
if (!$serviceEntity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$entity->getServiceInfoUid()} 서비스정보를 찾을수 없습니다.");
}
// ✅ 핵심 추가: 서비스금액 변경 및 payment Sync용
if ($serviceEntity->getServerInfoUid() === $entity->getServiceInfoUid()) {
$oldServiceEntity = clone $serviceEntity;
$serviceEntity = service('customer_serviceservice')->recalcAmount($serviceEntity);
//결제비 설정
service('paymentservice')->modifyByService($oldServiceEntity, $serviceEntity);
}
}
}
protected function create_process(array $formDatas): ServerEntity
@ -120,22 +137,14 @@ class ServerService extends EquipmentService
if (!$entity instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServerEntity만 가능");
}
try {
if ($entity->getIP()) {
service('part_ipservice')->attachToServer($entity);
}
} catch (\Throwable $e) {
log_message('debug', static::class . '->' . __FUNCTION__ . '에서:' . $e->getMessage());
}
if ($entity->getSwitchInfoUid()) {
service('part_switchservice')->attachToServer($entity);
}
service('equipment_chassisservice')->attachToServer($entity);
service('equipment_serverpartservice')->attachToServer($entity);
return $entity;
}
@ -145,23 +154,21 @@ class ServerService extends EquipmentService
throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생: 샷시정보가 정의되지 않았습니다.');
}
//기존정보 저장
$oldEntity = clone $entity;
$entity = parent::modify_process($entity, $formDatas);
if (!$entity instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServerEntity만 가능");
}
if ($oldEntity->getIP() !== $entity->getIP()) {
try {
if ($oldEntity->getIP()) {
service('part_ipservice')->detachFromServer($oldEntity);
}
if ($entity->getIP()) {
service('part_ipservice')->attachToServer($entity);
}
} catch (\Throwable $e) {
log_message('debug', static::class . '->' . __FUNCTION__ . '에서:' . $e->getMessage());
}
}
if ($oldEntity->getSwitchInfoUid() !== $entity->getSwitchInfoUid()) {
@ -179,22 +186,51 @@ class ServerService extends EquipmentService
service('equipment_chassisservice')->attachToServer($entity);
}
}
//가격 변동이 있는 경우
if ($oldEntity->getPrice() !== $entity->getPrice()) {
// ✅ 서비스 유지중이면 정상 동기화 (해지 시는 detachFromService에서 따로 처리)
if ($entity->getServiceInfoUid() !== null) {
$serviceService = service('customer_serviceservice');
$serviceEntity = $serviceService->getEntity($entity->getServiceInfoUid());
if (!$serviceEntity instanceof ServiceEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$entity->getServiceInfoUid()}에 해당하는 서비스정보을 찾을수 없습니다.");
}
$serviceService->recalcAmountAndSyncPayment($serviceEntity);
}
$this->recalcAmount($entity);
}
return $entity;
}
protected function delete_process($entity): ServerEntity
{
if (!$entity instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
//기존정보 우선 저장
$oldEntity = clone $entity;
//서버정보
$entity = parent::delete_process($entity);
if (!$entity instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
//서비스연결이 유지된 상태인경우
if ($entity->getServiceInfoUid() !== null) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:{$entity->getTitle()} 서버는 아직 서비스와 연결된 상태입니다.");
}
//IP정보
if ($oldEntity->getIP()) {
service('part_ipservice')->detachFromServer($oldEntity);
}
//Switch정보
if ($oldEntity->getSwitchInfoUid()) {
if (is_int($oldEntity->getSwitchInfoUid())) { //null이거나 공백인경우
service('part_switchservice')->detachFromServer($oldEntity);
}
}
//샷시정보
if ($oldEntity->getChassisInfoUid()) {
service('equipment_chassisservice')->detachFromServer($oldEntity);
}
//파트정보
service('equipment_serverpartservice')->detachFromServer($oldEntity);
return $oldEntity;
}
public function setSearchWord(string $word): void
{
$this->model->orLike($this->model->getTable() . '.ip', $word, 'both');
@ -208,11 +244,9 @@ class ServerService extends EquipmentService
if (!$entity instanceof ServerEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 서버정보을 찾을수 없습니다.");
}
$formDatas['serviceinfo_uid'] = $serviceEntity->getPK();
$formDatas["clientinfo_uid"] = $serviceEntity->getClientInfoUid();
$formDatas['status'] = $formDatas['status'] ?? STATUS['OCCUPIED'];
parent::modify_process($entity, $formDatas);
}

View File

@ -93,41 +93,6 @@ class PaymentService extends CommonService
return $walletService;
}
// ✅ 서비스 해지(서버 분리) 시: 월 미납 청구는 "삭제/0원수정" 금지, 상태만 TERMINATED
public function terminateUnpaidMonthlyByService(ServiceEntity $oldServiceEntity): void
{
$entity = $this->getEntity([
'serviceinfo_uid' => $oldServiceEntity->getPK(),
'billing' => PAYMENT['BILLING']['MONTH'],
'billing_at' => $oldServiceEntity->getBillingAt(),
'status' => STATUS['UNPAID'],
]);
if (!$entity instanceof PaymentEntity) {
return;
}
// amount/title 건드리지 않고 status만 변경
parent::modify_process($entity, ['status' => STATUS['TERMINATED']]);
}
//서비스정보로 결제정보 생성 또는 수정 (일반 운영용: upsert 유지)
public function setByService(ServiceEntity $oldServiceEntity, ServiceEntity $serviceEntity): PaymentEntity
{
$formDatas = $this->getFormDatasFromService($serviceEntity);
$entity = $this->getEntity([
'serviceinfo_uid' => $oldServiceEntity->getPK(),
'billing' => PAYMENT['BILLING']['MONTH'],
'billing_at' => $oldServiceEntity->getBillingAt(),
'status' => STATUS['UNPAID']
]);
//매칭되는게 있으면
if ($entity instanceof PaymentEntity) {
return $this->modify_process($entity, $formDatas);
}
return $this->create_process($formDatas);
}
//일회성,선결제,쿠폰,포인트 입력 관련
protected function create_process(array $formDatas): PaymentEntity
{
@ -251,6 +216,23 @@ class PaymentService extends CommonService
$formDatas = $this->getFormDatasFromService($serviceEntity);
return $this->create_process($formDatas);
}
//서비스정보로 결제정보 생성 또는 수정 (일반 운영용: upsert 유지)
public function modifyByService(ServiceEntity $oldServiceEntity, ServiceEntity $serviceEntity): PaymentEntity
{
$entity = $this->getEntity([
'serviceinfo_uid' => $oldServiceEntity->getPK(),
'billing' => PAYMENT['BILLING']['MONTH'],
'billing_at' => $oldServiceEntity->getBillingAt(),
'status' => STATUS['UNPAID']
]);
if (!$entity instanceof PaymentEntity) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능");
}
//매칭되는게 있으면(기존 서비스인경우) 아니면 신규등록
$formDatas = $this->getFormDatasFromService($serviceEntity);
return $this->modify_process($entity, $formDatas);
}
//서버파트별 일회성 관련
private function getFormDatasFromServerPart(ServerPartEntity $serverPartEntity, array $formDatas = []): array
@ -287,15 +269,26 @@ class PaymentService extends CommonService
]);
if (!$entity instanceof PaymentEntity) {
log_message('error', sprintf(
"\n------Last Query (%s)-----\nQuery: %s\n------------------------------\n",
static::class . '->' . __FUNCTION__,
$this->model->getLastQuery() ?? "No Query Available",
));
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 기존 서버파트정보의 {$oldServerPartEntity->getTitle()}에 해당하는 결제정보가 존재하지 않습니다.");
}
$formDatas = $this->getFormDatasFromServerPart($serverPartEntity);
return parent::modify_process($entity, $formDatas);
}
// ✅ 서비스 해지(서버 분리) 시: 월비용 파트정보는 고객만 연결
public function terminateByServerPart(ServiceEntity $oldServiceEntity): void
{
$entity = $this->getEntity([
'serviceinfo_uid' => $oldServiceEntity->getPK(),
'billing' => PAYMENT['BILLING']['MONTH'],
'billing_at' => $oldServiceEntity->getBillingAt(),
'status' => STATUS['UNPAID'],
]);
if (!$entity instanceof PaymentEntity) {
return;
}
//매칭되는게 있으면(기존 파트정보서비스인경우)
parent::modify_process($entity, ['status' => STATUS['TERMINATED']]);
}
}