daemon-idc init

This commit is contained in:
최준흠 2026-02-12 17:23:11 +09:00
parent ae9527dc74
commit 0fd4a82f28
45 changed files with 1147 additions and 544 deletions

View File

@ -17,15 +17,18 @@ $routes->addPlaceholder('uuid', '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}
$routes->group('cli', ['namespace' => 'App\Controllers\CLI'], function ($routes) {
});
$routes->group('', ['namespace' => 'App\Controllers'], function ($routes) {
$routes->get('/', 'Welcome::index');
$routes->group('auth', ['namespace' => 'App\Controllers\Auth'], function ($routes) {
$routes->get('login', 'LocalController::login_form');
$routes->post('login', 'LocalController::login');
$routes->get('google_login', 'GoogleController::login');
$routes->get('logout', 'LocalController::logout');
$routes->group('', ['namespace' => 'App\Controllers\Front'], function ($routes) {
$routes->get('/', 'WelcomeController::index');
$routes->group('inquiry', function ($routes) {
$routes->post('create', 'InquiryController::create');
});
});
$routes->group('auth', ['namespace' => 'App\Controllers\Auth'], function ($routes) {
$routes->get('login', 'LocalController::login_form');
$routes->post('login', 'LocalController::login');
$routes->get('google_login', 'GoogleController::login');
$routes->get('logout', 'LocalController::logout');
});
//Admin 관련
$routes->group('admin', ['namespace' => 'App\Controllers\Admin', 'filter' => 'authFilter:manager'], function ($routes) {
$routes->get('/', 'Welcome::index');
@ -59,5 +62,18 @@ $routes->group('admin', ['namespace' => 'App\Controllers\Admin', 'filter' => 'au
$routes->get('download/(:alpha)', 'BoardController::download/$1');
$routes->get('latest/(:alpha)', 'BoardController::latest/$1');
});
$routes->group('inquiry', function ($routes) {
$routes->get('/', 'InquiryController::index');
$routes->get('create', 'InquiryController::create_form');
$routes->post('create', 'InquiryController::create');
$routes->get('modify/(:num)', 'InquiryController::modify_form/$1');
$routes->post('modify/(:num)', 'InquiryController::modify/$1');
$routes->get('view/(:num)', 'InquiryController::view/$1');
$routes->get('delete/(:num)', 'InquiryController::delete/$1');
$routes->get('toggle/(:num)/(:any)', 'InquiryController::toggle/$1/$2');
$routes->post('batchjob', 'InquiryController::batchjob');
$routes->post('batchjob_delete', 'InquiryController::batchjob_delete');
$routes->get('download/(:alpha)', 'InquiryController::download/$1');
});
});
//choi.jh

View File

@ -7,8 +7,9 @@ use CodeIgniter\Config\BaseService;
//choi.jh
use App\Services\Auth\GoogleService;
use App\Services\Auth\LocalService;
use App\Services\BoardService;
use App\Services\UserService;
use App\Services\BoardService;
use App\Services\InquiryService;
//choi.jh
/**
@ -88,5 +89,14 @@ class Services extends BaseService
new \App\Models\BoardModel(),
);
}
public static function inquiryservice($getShared = true): InquiryService
{
if ($getShared) {
return static::getSharedInstance(__FUNCTION__);
}
return new InquiryService(
new \App\Models\InquiryModel(),
);
}
//choi.jh
}

View File

@ -2,9 +2,11 @@
namespace App\Controllers;
use RuntimeException;
use App\Entities\CommonEntity;
use CodeIgniter\HTTP\RedirectResponse;
use RuntimeException;
use CodeIgniter\HTTP\ResponseInterface;
use App\Exceptions\FormValidationException;
/**
* AbstractCRUDController
@ -12,8 +14,6 @@ use RuntimeException;
*/
abstract class AbstractCRUDController extends AbstractWebController
{
// 💡 핵심 1: 각 자식 클래스가 사용할 Entity 클래스 경로를 반환하도록 강제
// 이 메서드는 자식 클래스에서 반드시 구현되어야 합니다.
// --- 생성 (Create) ---
protected function create_form_process(array $formDatas = []): array
{
@ -39,20 +39,24 @@ abstract class AbstractCRUDController extends AbstractWebController
return $this->action_redirect_process('error', static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()} 생성폼 오류:" . $e->getMessage());
}
}
protected function create_process(array $formDatas): CommonEntity
{
// POST 데이터를 DTO 객체로 변환
$dto = $this->service->createDTO($formDatas);
// dd($dto->toArray());
//DTO 타입 체크 로직을 일반화
$dtoClass = $this->service->getDTOClass();
if (!$dto instanceof $dtoClass) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: " . get_class($dto) . "는 사용할 수 없습니다. ({$dtoClass} 필요)");
}
// 💡 여기서 service->create() 내부에서 CommonForm::validate()가 실행되고
// 실패 시 FormValidationException이 throw 된다고 가정(권장)
return $this->service->create($dto->toArray());
}
protected function create_result_process($entity, ?string $redirect_url = null): string|RedirectResponse
protected function create_result_process(CommonEntity $entity, ?string $redirect_url = null): string|RedirectResponse
{
return $this->action_redirect_process(
'info',
@ -61,19 +65,61 @@ abstract class AbstractCRUDController extends AbstractWebController
);
}
final public function create(): string|RedirectResponse
final public function create(): string|RedirectResponse|ResponseInterface
{
try {
$action = __FUNCTION__;
$this->action_init_process($action);
$entity = $this->create_process($this->request->getPost());
// 💡 동적으로 가져온 Entity 클래스 이름으로 instanceof 검사
$entityClass = $this->service->getEntityClass();
if (!$entity instanceof $entityClass) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 {$entityClass}만 가능");
}
// ✅ AJAX 요청이면 JSON 성공 응답
if ($this->request->isAJAX()) {
return $this->response->setJSON([
'ok' => true,
'message' => "{$this->getTitle()}에서 {$entity->getTitle()} 생성이 완료되었습니다.",
'id' => $entity->getPK(),
]);
}
return $this->create_result_process($entity);
} catch (FormValidationException $e) {
// ✅ AJAX 요청이면 422 + 필드별 오류
if ($this->request->isAJAX()) {
return $this->response
->setStatusCode(422)
->setJSON([
'ok' => false,
'errors' => $e->errors,
]);
}
// 기존 redirect 방식 유지
return $this->action_redirect_process(
'error',
static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()} 생성 오류:\n" . implode("\n", $e->errors)
);
} catch (\Throwable $e) {
log_message('error', 'EXCEPTION_CLASS=' . get_class($e));
// ✅ AJAX면 500 JSON
if ($this->request->isAJAX()) {
return $this->response
->setStatusCode(500)
->setJSON([
'ok' => false,
'message' => static::class . '->' . __FUNCTION__ . "에서 오류:" . $e->getMessage(),
]);
}
return $this->action_redirect_process('error', static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()} 생성 오류:" . $e->getMessage());
}
}
@ -98,7 +144,6 @@ abstract class AbstractCRUDController extends AbstractWebController
$entity = $this->modify_form_process($uid);
$this->addViewDatas('entity', $entity);
$action = __FUNCTION__;
//FormService에서 필요한 기존 데이터를 $entity에서 추출해서 넘김
$this->action_init_process($action, $entity->toArray());
return $this->modify_form_result_process($action);
} catch (\Throwable $e) {
@ -108,7 +153,6 @@ abstract class AbstractCRUDController extends AbstractWebController
protected function modify_process($uid, array $formDatas): CommonEntity
{
// POST 데이터를 DTO 객체로 변환
$formDatas[$this->service->getPKField()] = $uid;
$dto = $this->service->createDTO($formDatas);
//DTO 타입 체크 로직을 일반화
@ -119,7 +163,76 @@ abstract class AbstractCRUDController extends AbstractWebController
return $this->service->modify($uid, $dto->toArray());
}
protected function modify_result_process($entity, ?string $redirect_url = null): string|RedirectResponse
final public function modify($uid): string|RedirectResponse|ResponseInterface
{
try {
if (!$uid) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()}에 번호가 정의 되지 않았습니다.");
}
$action = __FUNCTION__;
$this->action_init_process($action);
$entity = $this->modify_process($uid, $this->request->getPost());
// 💡 동적으로 가져온 Entity 클래스 이름으로 instanceof 검사
$entityClass = $this->service->getEntityClass();
if (!$entity instanceof $entityClass) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 {$entityClass}만 가능");
}
$this->addViewDatas('entity', $entity);
// ✅ AJAX 요청이면 JSON 성공 응답
if ($this->request->isAJAX()) {
return $this->response->setJSON([
'ok' => true,
'message' => "{$this->getTitle()}에서 {$entity->getTitle()} 수정이 완료되었습니다.",
'id' => $entity->getPK(),
]);
}
return $this->modify_result_process($entity);
} catch (FormValidationException $e) {
// ✅ AJAX 요청이면 422 + 필드별 오류
if ($this->request->isAJAX()) {
return $this->response
->setStatusCode(422)
->setJSON([
'ok' => false,
'errors' => $e->errors,
]);
}
// 기존 redirect 방식 유지
return $this->action_redirect_process(
'error',
static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()} 수정 오류:\n" . implode("\n", $e->errors)
);
} catch (\Throwable $e) {
log_message('error', 'EXCEPTION_CLASS=' . get_class($e));
// ✅ AJAX면 500 JSON
if ($this->request->isAJAX()) {
return $this->response
->setStatusCode(500)
->setJSON([
'ok' => false,
'message' => static::class . '->' . __FUNCTION__ . "에서 오류:" . $e->getMessage(),
]);
}
return $this->action_redirect_process(
'error',
static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()} 수정 오류:" . $e->getMessage()
);
}
}
protected function modify_result_process(CommonEntity $entity, ?string $redirect_url = null): string|RedirectResponse
{
return $this->action_redirect_process(
'info',
@ -127,31 +240,18 @@ abstract class AbstractCRUDController extends AbstractWebController
$redirect_url ?? '/' . implode('/', [...$this->getActionPaths(), 'view']) . '/' . $entity->getPK()
);
}
final public function modify($uid): string|RedirectResponse
{
try {
if (!$uid) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()}에 번호가 정의 되지 않았습니다.");
}
$action = __FUNCTION__;
$this->action_init_process($action);
$entity = $this->modify_process($uid, $this->request->getPost());
$this->addViewDatas('entity', $entity);
return $this->modify_result_process($entity);
} catch (\Throwable $e) {
return $this->action_redirect_process('error', static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()} 수정 오류:" . $e->getMessage());
}
}
// --- 삭제 (Delete) ---
protected function delete_process($uid): CommonEntity
{
return $this->service->delete($uid);
}
protected function delete_result_process($entity, ?string $redirect_url = null): string|RedirectResponse
{
return $this->action_redirect_process('info', "{$this->getTitle()}에서 {$entity->getTitle()} 삭제가 완료되었습니다.", $redirect_url);
}
final public function delete($uid): RedirectResponse
{
try {
@ -159,7 +259,6 @@ abstract class AbstractCRUDController extends AbstractWebController
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()}에 번호가 정의 되지 않았습니다.");
}
$entity = $this->service->getEntity($uid);
//Delete처리
$entity = $this->delete_process($uid);
return $this->delete_result_process($entity);
} catch (\Throwable $e) {
@ -172,20 +271,20 @@ abstract class AbstractCRUDController extends AbstractWebController
{
return $this->service->getEntity($uid);
}
protected function view_result_process(string $action): string
{
return $this->action_render_process($action, $this->getViewDatas(), $this->request->getVar('ActionTemplate'));
}
final public function view($uid): string|RedirectResponse
{
try {
if (!$uid) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 {$this->getTitle()}에 번호가 정의 되지 않았습니다.");
}
//View처리
$entity = $this->view_process($uid);
$action = __FUNCTION__;
//FormService에서 필요한 기존 데이터를 $entity에서 추출해서 넘김
$this->action_init_process($action, $entity->toArray());
$this->addViewDatas('entity', $entity);
return $this->view_result_process($action);

View File

@ -0,0 +1,22 @@
<?php
namespace App\Controllers\Admin;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
class InquiryController extends AdminController
{
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
parent::initController($request, $response, $logger);
if ($this->service === null) {
$this->service = service('inquiryservice');
}
$this->addActionPaths('Inquiry');
}
//Action작업관련
//기본 함수 작업
//Custom 추가 함수
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Controllers;
namespace App\Controllers\Front;
use App\Controllers\CommonController;
use CodeIgniter\HTTP\RequestInterface;
@ -15,6 +15,7 @@ abstract class FrontController extends CommonController
parent::initController($request, $response, $logger);
$this->addActionPaths($this->_layout);
$this->layouts = LAYOUTS[$this->_layout];
helper('util');
}
protected function action_init_process(string $action, array $formDatas = []): void
{

View File

@ -0,0 +1,31 @@
<?php
namespace App\Controllers\Front;
use Psr\Log\LoggerInterface;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class InquiryController extends FrontController
{
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
parent::initController($request, $response, $logger);
if ($this->service === null) {
$this->service = service('inquiryservice');
}
$this->addActionPaths('Inquiry');
}
//Action작업관련
//기본 함수 작업
//Custom 추가 함수
protected function create_result_process($entity, ?string $redirect_url = null): string|RedirectResponse
{
return $this->action_redirect_process(
'info',
"{$this->getTitle()}에서 {$entity->getTitle()} 문의 등록이 완료되었습니다.",
);
}
}

View File

@ -1,12 +1,12 @@
<?php
namespace App\Controllers;
namespace App\Controllers\Front;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
class Welcome extends FrontController
class WelcomeController extends FrontController
{
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{

17
app/DTOs/InquiryDTO.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace App\DTOs;
class InquiryDTO extends CommonDTO
{
public ?int $uid = null;
public string $title = '';
public string $email = '';
public string $content = '';
public string $status = '';
public function __construct(array $datas = [])
{
parent::__construct($datas);
}
}

View File

@ -47,6 +47,35 @@ LOCK TABLES `boardinfo` WRITE;
/*!40000 ALTER TABLE `boardinfo` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `inquiryinfo`
--
DROP TABLE IF EXISTS `inquiryinfo`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `inquiryinfo` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`content` text NOT NULL,
`status` varchar(20) NOT NULL DEFAULT 'available',
`updated_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `inquiryinfo`
--
LOCK TABLES `inquiryinfo` WRITE;
/*!40000 ALTER TABLE `inquiryinfo` DISABLE KEYS */;
/*!40000 ALTER TABLE `inquiryinfo` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user`
--

View File

@ -0,0 +1,22 @@
<?php
namespace App\Entities;
use App\Entities\CommonEntity;
use App\Models\InquiryModel as Model;
class InquiryEntity extends CommonEntity
{
const PK = Model::PK;
const TITLE = Model::TITLE;
protected $attributes = [
'title' => '',
'email' => '',
'status' => '',
'content' => ''
];
public function __construct(array|null $data = null)
{
parent::__construct($data);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class FormValidationException extends RuntimeException
{
public array $errors;
public function __construct(array $errors, string $message = 'Validation failed', int $code = 0, ?\Throwable $previous = null)
{
$this->errors = $errors;
parent::__construct($message, $code, $previous);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Forms;
use App\Exceptions\FormValidationException;
use RuntimeException;
/**
@ -13,11 +14,14 @@ use RuntimeException;
* 3) validate()에서 dynamicRules 누적 버그 수정 (마지막 규칙만 남는 문제 해결)
* 4) "필드 존재 보장"으로 임의 '' 삽입 제거 (미입력 필드가 FK/숫자 규칙을 깨는 문제 방지)
* 5) role.* 같은 배열 원소 규칙을 위해 부모 배열 보정 로직 유지/강화
*
* 추가:
* - validate() 실패 RuntimeException(implode) 대신
* FormValidationException(errors 배열) throw하여
* Controller에서 AJAX(422 JSON errors) 응답이 가능하게
*/
abstract class CommonForm
{
private $_validation = null;
private array $_attributes = [];
private array $_formFields = [];
private array $_formRules = [];
@ -27,10 +31,11 @@ abstract class CommonForm
private array $_formOptions = [];
private array $_actionButtons = ['view' => ICONS['SEARCH'], 'delete' => ICONS['DELETE']];
private array $_batchjobButtons = ['batchjob' => '일괄처리', 'batchjob_delete' => '일괄삭제'];
protected $validation = null;
protected function __construct()
{
$this->_validation = service('validation');
$this->validation = service('validation');
}
public function action_init_process(string $action, array &$formDatas = []): void
@ -160,7 +165,6 @@ abstract class CommonForm
/**
* 1) 깊은 배열 구조 정리(배열은 유지)
* - 여기서는 null -> '' 같은 변환을 절대 하지 않습니다.
* - 이유: FK/숫자/날짜 필드가 '' 변하면 validation/DB에서 문제가 발생함.
*/
protected function sanitizeFormDatas($data, string $path = '')
{
@ -179,11 +183,8 @@ abstract class CommonForm
/**
* 2) 숫자/FK 필드 정규화
* - 폼에서 미선택은 보통 '' 들어옴 -> NULL로 변환
* - 숫자 문자열은 int 캐스팅 (선택)
*
* 주의:
* - "빈값을 0으로 취급" 같은 정책이 있다면 여기에서 조정해야 .
* - '' -> null
* - 숫자 문자열 -> int
*/
protected function normalizeNumericEmptyToNull(array $data, array $numericFields): array
{
@ -245,7 +246,7 @@ abstract class CommonForm
$formDatas[$parent] = [];
}
// ✅ 4) 핵심: 배열 원소의 null/'' 제거 + 문자열화(Trim이 null 받지 않도록)
// 4) 배열 원소 정리
$clean = array_map(
fn($v) => is_scalar($v) ? trim((string) $v) : '',
$formDatas[$parent]
@ -256,14 +257,8 @@ abstract class CommonForm
}
}
/**
* 4) 검증 rule에 따라 "numeric(특히 FK)" 취급할 필드를 수집
* - getFormRule()에서 permit_empty|numeric 정의되는 필드를 공통 처리하기 위함
*
* 구현 전략:
* - formRules에서 rule 문자열에 'numeric' 포함된 필드를 모음
* - wildcard(role.*) 제외
* 4) 검증 rule에 따라 numeric(FK 포함) 필드 수집
*/
protected function collectNumericFieldsFromRules(array $formRules): array
{
@ -276,7 +271,7 @@ abstract class CommonForm
continue;
}
// getValidationRule hook 적용 (필드명/룰이 바뀔 수 있으니)
// hook 적용
[$fieldName, $ruleStr] = $this->getValidationRule($fieldName, (string) $rule);
if (is_string($ruleStr) && str_contains($ruleStr, 'numeric')) {
@ -284,7 +279,6 @@ abstract class CommonForm
}
}
// 중복 제거
return array_values(array_unique($numericFields));
}
@ -294,20 +288,15 @@ abstract class CommonForm
/**
* 데이터를 검증하고 유효하지 않을 경우 예외를 발생시킵니다.
* 2025 CI4 표준: 규칙 배열 내에 label을 포함하여 한글 메시지 출력을 보장합니다.
* 변경점:
* - 실패 FormValidationException(errors 배열) throw
* (AJAX에서 422 내려보내기 위함)
*/
final public function validate(array &$formDatas): void
{
log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class);
if ($this->_validation === null) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Validation 서비스가 초기화되지 않았습니다.");
}
try {
// 0) 데이터 구조 정리 (null 변환 X)
$formDatas = $this->sanitizeFormDatas($formDatas);
// 1) 필드 라벨/규칙
$formFields = $this->getFormFields();
$formRules = $this->getFormRules();
@ -315,61 +304,42 @@ abstract class CommonForm
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다.");
}
// 2) wildcard(role.*) 부모 배열 보정
$this->ensureParentArrayForWildcardRules($formDatas, $formRules);
// 3) numeric(FK 포함) 필드: '' -> null, 숫자 문자열 -> int
// (규칙 기반 자동 수집)
$numericFields = $this->collectNumericFieldsFromRules($formRules);
$formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields);
// 4) dynamicRules 누적 구성 (버그 수정: 루프마다 초기화 금지)
$dynamicRules = [];
foreach ($formRules as $field => $rule) {
try {
// 필드명/규칙 추출(확장 포인트)
[$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
[$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule);
// label 결정
if (isset($formFields[$fieldName])) {
$label = $formFields[$fieldName];
} elseif (str_contains($fieldName, '.*')) {
$parentField = str_replace('.*', '', $fieldName);
$label = ($formFields[$parentField] ?? $fieldName) . " 항목";
} else {
$label = $fieldName;
}
$dynamicRules[$fieldName] = [
'label' => $label,
'rules' => $ruleStr,
];
// ❌ 존재 보장으로 '' 삽입하지 않음
// - required는 CI4가 "키 없음"도 실패 처리 가능(일반적으로)
// - permit_empty는 키 없어도 통과 (강제로 '' 만들면 FK/숫자 문제 발생)
} catch (\Throwable $e) {
throw new RuntimeException("유효성 검사 규칙 준비 중 오류 발생 (필드: {$field}): " . $e->getMessage());
if (isset($formFields[$fieldName])) {
$label = $formFields[$fieldName];
} elseif (str_contains($fieldName, '.*')) {
$parentField = str_replace('.*', '', $fieldName);
$label = ($formFields[$parentField] ?? $fieldName) . ' 항목';
} else {
$label = $fieldName;
}
$dynamicRules[$fieldName] = [
'label' => $label,
'rules' => $ruleStr,
];
}
$this->_validation->setRules($dynamicRules);
$this->validation->setRules($dynamicRules);
try {
if (!$this->_validation->run($formDatas)) {
$errors = $this->_validation->getErrors();
throw new RuntimeException(implode("\n", $errors));
}
} catch (\TypeError $e) {
throw new RuntimeException("검증 도중 타입 오류 발생: " . $e->getMessage());
if (!$this->validation->run($formDatas)) {
throw new FormValidationException($this->validation->getErrors());
}
} catch (FormValidationException $e) {
throw $e; // ✅ 필드별 errors 유지
} catch (\TypeError $e) {
throw new RuntimeException('검증 도중 타입 오류 발생: ' . $e->getMessage());
} catch (\Throwable $e) {
if ($e instanceof RuntimeException) {
throw $e;
}
throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage());
throw new RuntimeException('유효성 검사 중 시스템 오류 발생: ' . $e->getMessage());
}
}
@ -377,7 +347,6 @@ abstract class CommonForm
* Overridable hooks
* --------------------------------------------------------------------- */
// 사용자 정의 hook: 필드/룰 커스터마이즈
protected function getValidationRule(string $field, string $rule): array
{
return [$field, $rule];
@ -393,11 +362,6 @@ abstract class CommonForm
return $label;
}
/**
* Form rule 정의
* - permit_empty|numeric FK들이 여기서 정의되면,
* validate()에서 자동으로 ''->null 정규화 대상에 포함됩니다.
*/
public function getFormRule(string $action, string $field, array $formRules): array
{
switch ($field) {
@ -411,32 +375,39 @@ abstract class CommonForm
$formRules[$field] = "required|numeric";
}
break;
case $this->getAttribute('title_field'):
$formRules[$field] = sprintf(
"required|trim|string%s",
in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
);
break;
case "code":
$formRules[$field] = sprintf(
"required|regex_match[/^[a-zA-Z0-9가-힣\-\_]+$/]|min_length[4]%s",
in_array($action, ["create"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : ""
);
break;
case "user_uid":
$formRules[$field] = "required|numeric";
break;
case "status":
$formRules[$field] = "required|trim|string";
break;
case 'picture':
$formRules[$field] = "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":
$formRules[$field] = "permit_empty|trim|valid_date";
break;
default:
$formRules[$field] = "permit_empty|trim|string";
break;

82
app/Forms/InquiryForm.php Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace App\Forms;
use RuntimeException;
use App\Forms\CommonForm;
class InquiryForm extends CommonForm
{
public function __construct()
{
parent::__construct();
}
public function action_init_process(string $action, array &$formDatas = []): void
{
$fields = [
'title',
'email',
'content',
'status',
];
$filters = [
'status',
];
$indexFilter = $filters;
$batchjobFilters = ['status'];
switch ($action) {
case 'view':
$fields = [
'title',
'email',
'status',
'created_at',
'content'
];
break;
case 'index':
$fields = [
'title',
'email',
'status',
'created_at'
];
break;
case 'download':
$fields = [
'title',
'email',
'status',
'created_at',
'content'
];
break;
}
$this->setFormFields($fields);
$this->setFormRules($action, $fields);
$this->setFormFilters($filters);
$this->setFormOptions($action, $filters, $formDatas);
$this->setIndexFilters($indexFilter);
$this->setBatchjobFilters($batchjobFilters);
}
public function getFormRule(string $action, string $field, array $formRules): array
{
switch ($field) {
case "title":
case "content":
$formRules[$field] = "required|trim|string";
break;
case "email":
$formRules[$field] = sprintf("required|trim|valid_email%s", in_array($action, ["create", "create_form"]) ? "|is_unique[{$this->getAttribute('table')}.{$field}]" : "");
break;
case "status":
$formRules[$field] = "permit_empty|trim|string";
break;
default:
$formRules = parent::getFormRule($action, $field, $formRules);
break;
}
return $formRules;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Helpers;
class InquiryHelper extends CommonHelper
{
public function __construct()
{
parent::__construct();
}
}

View File

@ -0,0 +1,22 @@
<?php
// 로딩 전체에서 자동 로딩: app/Config/Autoload.php
// public $helpers = ['util'];
// BaseController에서:
// helper('util');
use App\Traits\UtilTrait;
if (!function_exists('alertTrait')) {
function alertTrait(string $msg, $url = null): string
{
static $util = null;
if ($util === null) {
$util = new class {
use UtilTrait;
};
}
return $util->alertTrait($msg, $url);
}
}

View File

@ -0,0 +1,19 @@
<?php
return [
'title' => "문의정보",
'label' => [
'uid' => "번호",
'title' => "제목",
'email' => "이메일",
'content' => "내용",
'status' => "상태",
'updated_at' => "수정일",
'created_at' => "작성일",
'deleted_at' => "삭제일",
],
"STATUS" => [
STATUS['AVAILABLE'] => "문의",
STATUS['PAUSE'] => "일시정지",
STATUS['TERMINATED'] => "완료",
],
];

View File

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

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use App\Entities\InquiryEntity;
class InquiryModel extends CommonModel
{
const TABLE = "inquiryinfo";
const PK = "uid";
const TITLE = "title";
protected $table = self::TABLE;
protected $primaryKey = self::PK;
protected $returnType = InquiryEntity::class;
protected $allowedFields = [
"uid",
"title",
"email",
"content",
"status",
];
protected array $allowedDbDefaultFields = ['status']; // ✅ INSERT에서 status가 비면 DB default 사용
public function __construct()
{
parent::__construct();
}
}

View File

@ -8,6 +8,7 @@ use App\Entities\CommonEntity;
use App\Models\CommonModel;
use App\Libraries\AuthContext;
use CodeIgniter\Database\Exceptions\DatabaseException;
use App\Exceptions\FormValidationException;
use RuntimeException;
abstract class CommonService
@ -41,6 +42,9 @@ abstract class CommonService
$result = $callback($db);
$db->transComplete();
return $result;
} catch (FormValidationException $e) {
$db->transRollback();
throw $e; // ✅ 이거 필수
} catch (DatabaseException $e) {
$errorMessage = sprintf(
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
@ -52,7 +56,7 @@ abstract class CommonService
throw new RuntimeException($errorMessage, $e->getCode(), $e);
} catch (\Throwable $e) {
$db->transRollback();
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
throw $e; // ✅ 여기서도 RuntimeException으로 감싸지 말 것 (권장)
}
}
@ -259,18 +263,13 @@ abstract class CommonService
protected function create_process(array $formDatas): CommonEntity
{
try {
log_message('debug', "*** ENTER" . __METHOD__ . " ***");
$actionForm = $this->getActionForm();
log_message('debug', 'FORMCLASS=' . $this->formClass . ' / FORMINST=' . (is_object($actionForm) ? get_class($actionForm) : 'NULL'));
log_message('debug', 'IS_COMMONFORM=' . (is_object($actionForm) && $actionForm instanceof CommonForm ? 'YES' : 'NO'));
if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('create', $formDatas);
foreach ($formDatas as $field => $value) {
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas);
}
log_message('debug', '>>> BEFORE validate: ' . get_class($actionForm));
$actionForm->validate($formDatas); // ✅ 여기서 검증
log_message('debug', '>>> AFTER validate');
}
$entityClass = $this->getEntityClass();
$entity = new $entityClass($formDatas);
@ -278,6 +277,8 @@ abstract class CommonService
throw new RuntimeException("Return Type은 {$entityClass}만 가능");
}
return $this->save_process($entity);
} catch (FormValidationException $e) {
throw $e; // ✅ 감싸지 말고 그대로
} catch (\Throwable $e) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:" . $e->getMessage());
}
@ -295,18 +296,13 @@ abstract class CommonService
protected function modify_process($entity, array $formDatas): CommonEntity
{
try {
log_message('debug', "*** ENTER" . __METHOD__ . " ***");
$actionForm = $this->getActionForm();
log_message('debug', 'FORMCLASS=' . $this->formClass . ' / FORMINST=' . (is_object($actionForm) ? get_class($actionForm) : 'NULL'));
log_message('debug', 'IS_COMMONFORM=' . (is_object($actionForm) && $actionForm instanceof CommonForm ? 'YES' : 'NO'));
if ($actionForm instanceof CommonForm) {
$actionForm->action_init_process('modify', $formDatas);
foreach ($formDatas as $field => $value) {
$formDatas = $this->action_process_fieldhook($field, $value, $formDatas);
}
log_message('debug', '>>> BEFORE validate: ' . get_class($actionForm));
$actionForm->validate($formDatas); // ✅ 여기서 검증
log_message('debug', '>>> AFTER validate');
}
// 검증 통과 후 엔티티 반영
$entity->fill($formDatas);
@ -314,9 +310,10 @@ abstract class CommonService
return $entity;
}
return $this->save_process($entity);
} catch (FormValidationException $e) {
throw $e; // ✅ 감싸지 말고 그대로
} catch (\Throwable $e) {
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:" . $e->getMessage() . "\n" . var_export($entity, true));
throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:" . $e->getMessage());
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Services;
use App\DTOs\InquiryDTO;
use App\Forms\InquiryForm;
use App\Models\InquiryModel;
use App\Entities\CommonEntity;
use App\Helpers\InquiryHelper;
use App\Entities\InquiryEntity;
class InquiryService extends CommonService
{
protected string $formClass = InquiryForm::class;
protected string $helperClass = InquiryHelper::class;
public function __construct(InquiryModel $model)
{
parent::__construct($model);
$this->addClassPaths('Inquiry');
}
public function getDTOClass(): string
{
return InquiryDTO::class;
}
public function createDTO(array $formDatas): InquiryDTO
{
return new InquiryDTO($formDatas);
}
public function getEntityClass(): string
{
return InquiryEntity::class;
}
//기본 기능부분
protected function getEntity_process(mixed $entity): InquiryEntity
{
return $entity;
}
//List 검색용
//FormFilter 조건절 처리
//검색어조건절처리
//추가기능부분
protected function create_process(array $formDatas): CommonEntity
{
if (!isset($formDatas['status']) || $formDatas['status'] === '' || $formDatas['status'] === null) {
$formDatas['status'] = STATUS['AVAILABLE']; // 'available'
}
return parent::create_process($formDatas);
}
}

View File

@ -1,6 +1,5 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div id="container" class="content">
<div class="form_top"><?= $this->include("{$viewDatas['layout']['template']}/form_content_top"); ?></div>
<?= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
@ -10,7 +9,8 @@
</tr>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?>
<tr>
<th nowrap class="text-end bg-light" width="20%"><?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?></th>
<th nowrap class="text-end bg-light" width="20%">
<?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?></th>
<td nowrap class="text-start">
<?= $viewDatas['helper']->getFieldForm($field, old($field) ?? ($viewDatas['formDatas'][$field] ?? null), $viewDatas) ?>
<div><?= validation_show_error($field); ?></div>

View File

@ -1,6 +1,6 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div class="layout_top"><?= $this->include("{$viewDatas['layout']['layout']}/top"); ?></div>
<table class="layout_middle">
<tr>
@ -8,37 +8,41 @@
<td class="layout_right">
<div class="layout_header"><?= $this->include("{$viewDatas['layout']['template']}/index_header"); ?></div>
<div id="container" class="layout_content">
<link href="/css/<?= $viewDatas['layout']['path'] ?>/index.css" media="screen" rel="stylesheet" type="text/css" />
<link href="/css/<?= $viewDatas['layout']['path'] ?>/index.css" media="screen" rel="stylesheet" type="text/css" />
<div class="index_body">
<?= $this->include("{$viewDatas['layout']['template']}/index_content_filter"); ?>
<?= form_open(current_url(), ['id' => 'batchjob_form', 'method' => "post"]) ?>
<table class="index_table data table table-bordered table-hover table-striped" data-rtc-resizable-table="reisze_table">
<thead>
<tr>
<th class="text-center bg-light">번호</th>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?>
<th class="text-center bg-light"><?= $viewDatas['helper']->getListLabel($field, $label, $viewDatas) ?></th>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?>
<th class="text-center bg-light"><?= $viewDatas['helper']->getListLabel($field, $label, $viewDatas) ?></th>
<?php endforeach ?>
<th class="text-center bg-light">작업</th>
</tr>
</thead>
<tbody>
<?php $cnt = 0 ?>
<?php foreach ($viewDatas['entities'] as $entity): ?>
<?php
$viewDatas['entity'] = $entity;
$num = $viewDatas['index_totalcount'] - (($viewDatas['page'] - 1) * $viewDatas['perpage'] + $cnt);
?>
<?php foreach ($viewDatas['entities'] as $entity): ?>
<?php
$viewDatas['entity'] = $entity;
$num = $viewDatas['index_totalcount'] - (($viewDatas['page'] - 1) * $viewDatas['perpage'] + $cnt);
?>
<tr>
<td nowrap><?= $viewDatas['helper']->getListButton('modify', $num, $viewDatas) ?></td>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?><td><?= $viewDatas['helper']->getFieldView($field, $entity->$field, $viewDatas) ?></td><?php endforeach ?>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?><td><?= $viewDatas['helper']->getFieldView($field, $entity->$field, $viewDatas) ?></td><?php endforeach ?>
<td nowrap>
<?php foreach ($viewDatas['index_actionButtons'] as $action => $label): ?>
<?= $viewDatas['helper']->getListButton($action, $label, $viewDatas) ?>&nbsp;
<?php foreach ($viewDatas['index_actionButtons'] as $action => $label): ?>
<?= $viewDatas['helper']->getListButton($action, $label, $viewDatas) ?>&nbsp;
<?php endforeach ?>
</td>
</tr>
<?php $cnt++ ?>
<?php $cnt++ ?>
<?php endforeach ?>
</tbody>
</table>

View File

@ -1,7 +1,8 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div id="container" class="content">
<div id="container" clas
s="content">
<div class="form_top"><?= $this->include("{$viewDatas['layout']['template']}/form_content_top"); ?></div>
<?= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
<table class="table table-bordered">
@ -9,15 +10,15 @@
<th class="bg-light" colspan="2"><?= $viewDatas['title'] ?></th>
</tr>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?>
<tr>
<th nowrap class="text-end bg-light" width="20%">
<?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?>
</th>
<td nowrap class="text-start">
<?= $viewDatas['helper']->getFieldForm($field, old($field) ?? ($viewDatas['entity']->$field ?? null), $viewDatas) ?>
<div><?= validation_show_error($field); ?></div>
</td>
</tr>
<tr>
<th nowrap class="text-end bg-light" width="20%">
<?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?>
</th>
<td nowrap class="text-start">
<?= $viewDatas['helper']->getFieldForm($field, old($field) ?? ($viewDatas['entity']->$field ?? null), $viewDatas) ?>
<div><?= validation_show_error($field); ?></div>
</td>
</tr>
<?php endforeach; ?>
</table>
<div class="text-center"><?= form_submit('', '수정', array("class" => "btn btn-outline btn-primary")); ?></div>

View File

@ -1,6 +1,5 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div id="container" class="content">
<div class="form_top"><?= $this->include("{$viewDatas['layout']['template']}/form_content_top"); ?></div>
<?= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>

View File

@ -1,6 +1,5 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<table class="layout_middle">
<tr>
<td class="layout_right">

View File

@ -1,7 +1,8 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div id="container" class="content">
<div id="container" clas
s="content">
<div class="form_top"><?= $this->include("{$viewDatas['layout']['template']}/form_content_top"); ?></div>
<?= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
<table class="table table-bordered">
@ -9,13 +10,13 @@
<th class="bg-light" colspan="2"><?= $viewDatas['title'] ?></th>
</tr>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?>
<tr>
<th nowrap class="text-end bg-light" width="20%"><?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?></th>
<td nowrap class="text-start">
<?= $viewDatas['helper']->getFieldForm($field, old($field) ?? ($viewDatas['entity']->$field ?? null), $viewDatas) ?>
<div><?= validation_show_error($field); ?></div>
</td>
</tr>
<tr>
<th nowrap class="text-end bg-light" width="20%"><?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?></th>
<td nowrap class="text-start">
<?= $viewDatas['helper']->getFieldForm($field, old($field) ?? ($viewDatas['entity']->$field ?? null), $viewDatas) ?>
<div><?= validation_show_error($field); ?></div>
</td>
</tr>
<?php endforeach; ?>
</table>
<div class="text-center"><?= form_submit('', '수정', array("class" => "btn btn-outline btn-primary")); ?></div>

View File

@ -1,6 +1,5 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div id="container" class="content">
<div class="form_top"><?= $this->include("{$viewDatas['layout']['template']}/form_content_top"); ?></div>
<table class="table table-bordered">

View File

@ -1,17 +1,18 @@
<?= $this->extend($viewDatas['layout']['layout']) ?>
<?= $this->section('content') ?>
<?= session('message') ? $viewDatas['helper']->alertTrait(session('message')) : ""; ?>
<div id="container" class="content">
<div id="container" clas
s="content">
<div class="form_top"><?= $this->include("{$viewDatas['layout']['template']}/form_content_top"); ?></div>
<table class="table table-bordered">
<tr>
<th class="bg-light" colspan="2"><?= $viewDatas['title'] ?></th>
</tr>
<?php foreach ($viewDatas['formFields'] as $field => $label): ?>
<tr>
<th nowrap class="text-end bg-light" width="20%"><?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?></th>
<td nowrap class="text-start"><?= $viewDatas['helper']->getFieldView($field, old($field) ?? ($viewDatas['entity']->$field ?? null), $viewDatas) ?></td>
</tr>
<tr>
<th nowrap class="text-end bg-light" width="20%"><?= $viewDatas['helper']->getFieldLabel($field, $label, $viewDatas) ?></th>
<td nowrap class="text-start"><?= $viewDatas['helper']->getFieldView($field, old($field) ?? ($viewDatas['entity']->$field ?? null), $viewDatas) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php if (session('message')): ?><div class="alert alert-danger text-start"><?= nl2br(session('message')) ?></div><?php endif; ?>

View File

@ -2,7 +2,7 @@
<div class="login-page">
<div class="login-content">
<div class="login-container">
<h2 class="login-title">DBMS 로그인</h2>
<h2 class="login-title">DaemonIDC 로그인</h2>
<?= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
<div class="input-group">
<label for="userId">아이디</label>
@ -15,9 +15,10 @@
<div><?= validation_show_error('passwd'); ?></div>
</div>
<button type="submit" class="btn-login">로그인</button>
<div class="login-options"><?= $viewDatas['sns_button'] ?? "" ?></div>
<div class="login-options"><?= $viewDatas['sns_button'] ?? "" ?></div>
<?= form_close(); ?>
<?php if (session('message')): ?><div class="alert alert-info text-start"><?= session('message') ?></div><?php endif; ?>
<?php if (session('message')): ?>
<div class="alert alert-info text-start"><?= session('message') ?></div><?php endif; ?>
</div>
</div>
</div>

View File

@ -0,0 +1,26 @@
<section id="datacenter" class="py-5 bg-light">
<div class="container py-5">
<div class="row g-4 align-items-center">
<div class="col-lg-5 order-2 order-lg-1">
<img src="https://images.unsplash.com/photo-1596272875729-ed2ff7d6d9c5?auto=format&fit=crop&q=80&w=800"
class="img-fluid rounded-4 shadow" alt="IDC Center">
</div>
<div class="col-lg-7 ps-lg-5 order-1 order-lg-2">
<h2 class="fw-bold mb-4">치바/도쿄 프리미엄 IDC</h2>
<p class="text-muted mb-5">아시아 비즈니스의 전략적 허브인 치바/도쿄에 위치하여 한국, 일본, 글로벌 트래픽을 가장 낮은 지연시간(Low Latency)으로
처리합니다.
</p>
<div class="row g-4">
<div class="col-sm-6">
<h5 class="fw-bold"><i class="fa-solid fa-bolt-lightning text-warning me-2"></i> 전력 보장</h5>
<p class="small text-muted">N+1 이중화 시스템 설비로 최대 가용성 보장.</p>
</div>
<div class="col-sm-6">
<h5 class="fw-bold"><i class="fa-solid fa-snowflake text-info me-2"></i> 공조 시설</h5>
<p class="small text-muted">고밀도 서버의 열기를 완벽히 제어하는 기업용 냉방 시스템 완비.</p>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,18 @@
<section id="home" class="hero-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<h1 class="display-3 fw-bold mb-4">Enterprise-grade <span class="text-primary">Infrastructure</span>
</h1>
<p class="lead mb-5 text-light opacity-75">
High Performance Secure Low Latency<br>
도쿄 가야바초의 최첨단 IDC에서 제공하는 압도적 성능의 전용 서버 인프라.
</p>
<div class="d-flex justify-content-center gap-3">
<a href="#servers" class="btn btn-primary btn-lg px-5 py-3 fw-bold">서버 사양 보기</a>
<a href="#support" class="btn btn-outline-light btn-lg px-5 py-3 fw-bold">상담 문의</a>
</div>
</div>
</div>
</div>
</section>

View File

@ -4,302 +4,15 @@
<div class="layout_top"><?= $this->include($viewDatas['layout']['layout'] . '/top'); ?></div>
<!-- Layout Middle Start -->
<!-- Hero Section -->
<section id="home" class="hero-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<h1 class="display-3 fw-bold mb-4">Enterprise-grade <span class="text-primary">Infrastructure</span>
</h1>
<p class="lead mb-5 text-light opacity-75">
High Performance Secure Low Latency<br>
도쿄 가야바초의 최첨단 IDC에서 제공하는 압도적 성능의 전용 서버 인프라.
</p>
<div class="d-flex justify-content-center gap-3">
<a href="#servers" class="btn btn-primary btn-lg px-5 py-3 fw-bold">서버 사양 보기</a>
<a href="#support" class="btn btn-outline-light btn-lg px-5 py-3 fw-bold">상담 문의</a>
</div>
</div>
</div>
</div>
</section>
<?= $this->include($viewDatas['layout']['path'] . '/welcome/hero'); ?>
<!-- Stats Bar -->
<section class="py-5 bg-white border-bottom">
<div class="container text-center">
<div class="row g-4">
<div class="col-6 col-lg-3">
<i class="fa-solid fa-microchip text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">NVMe Gen4</h6>
<p class="small text-muted mb-0">엔터프라이즈 전용 SSD</p>
</div>
<div class="col-6 col-lg-3">
<i class="fa-solid fa-shield-halved text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">DDoS Protected</h6>
<p class="small text-muted mb-0">실시간 L3/L4/L7 방어</p>
</div>
<div class="col-6 col-lg-3">
<i class="fa-solid fa-bolt text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">100G Backbone</h6>
<p class="small text-muted mb-0">글로벌 고속 네트워크</p>
</div>
<div class="col-6 col-lg-3">
<i class="fa-solid fa-headset text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">24/7 Monitoring</h6>
<p class="small text-muted mb-0">상주 엔지니어 지원</p>
</div>
</div>
</div>
</section>
<?= $this->include($viewDatas['layout']['path'] . '/welcome/statsbar'); ?>
<!-- Server Hosting Section -->
<section id="servers" class="py-5 bg-light">
<div class="container py-5">
<div class="text-center mb-5">
<h2 class="fw-bold mb-3">전용 서버 호스팅</h2>
<p class="text-muted">도쿄 IDC 기반의 최신 하드웨어 라인업을 확인하세요.</p>
</div>
<div class="row g-4">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<!-- Proxmox 가상 서버 (추가 요청) -->
<div class="col">
<div class="server-card border-info" style="border-width: 2px;">
<div class="card-header-custom">
<div class="type-title text-info">Proxmox VPS</div>
<div class="type-subtitle text-info">고성능 가상 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">Hypervisor:</span> <span class="spec-value">Proxmox VE
(KVM)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">vCPU:</span> <span class="spec-value">Intel Xeon Gold
(Dedicated)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">8GB ~ 64GB (No
Overselling)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">Storage:</span> <span class="spec-value">Enterprise NVMe
SSD</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">Feature:</span> <span class="spec-value">Snapshot, Backup,
콘솔</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text text-info">가격 문의</div>
<button class="btn btn-info btn-inquiry text-white shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- B타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">B타입</div>
<div class="type-subtitle text-blue">범용 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (4 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">16GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 45만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- C타입 (가장 인기) -->
<div class="col">
<div class="server-card card-featured shadow">
<div class="popular-badge">가장 인기</div>
<div class="card-header-custom">
<div class="type-title">C타입</div>
<div class="type-subtitle text-purple">고성능 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (8 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">16GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text price-purple"> 55만원</div>
<button class="btn btn-primary btn-inquiry bg-purple shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- D타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">D타입</div>
<div class="type-subtitle text-blue">프리미엄 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (12 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">16GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 65만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- E타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">E타입</div>
<div class="type-subtitle text-blue">엔터프라이즈 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (20 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">32GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 75만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- E2타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">E2타입</div>
<div class="type-subtitle text-blue">최상위 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (28 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">32GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 85만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
</div>
</div>
</section>
<?= $this->include($viewDatas['layout']['path'] . '/welcome/server'); ?>
<!-- Network & DDoS Section -->
<section id="network" class="py-5 bg-white">
<div class="container py-5">
<div class="row align-items-center g-5">
<div class="col-lg-6">
<h6 class="text-primary fw-bold text-uppercase mb-3">Network & Security</h6>
<h2 class="fw-bold mb-4">실시간 지능형 DDoS 방어 시스템</h2>
<p class="text-muted mb-4">단순한 트래픽 차단이 아닌, 정교한 데이터 정화 엔진(Scrubbing Center) 통해 정상적인 비즈니스 트래픽만을 서버로
전달합니다.</p>
<ul class="list-unstyled">
<li class="mb-3 d-flex align-items-start"><i
class="fa-solid fa-check-circle text-success me-3 mt-1"></i> <span><strong>L3/L4
Mitigation:</strong> UDP/SYN Flood 완벽 대응</span></li>
<li class="mb-3 d-flex align-items-start"><i
class="fa-solid fa-check-circle text-success me-3 mt-1"></i> <span><strong>BGP +
Anycast:</strong> 세계 주요 거점에서 공격 분산 처리</span></li>
<li class="mb-3 d-flex align-items-start"><i
class="fa-solid fa-check-circle text-success me-3 mt-1"></i> <span><strong>Always-on
Protection:</strong> 1 미만의 탐지 즉각 대응</span></li>
</ul>
</div>
<div class="col-lg-6">
<div class="scrubbing-box">
<div class="text-center text-white fw-bold mb-4 uppercase tracking-widest">트래픽 흐름도(Traffic Flow)
</div>
<div class="d-flex flex-column align-items-center gap-4">
<div class="flow-item w-100">전체 트래픽 (All Internet Traffic)</div>
<i class="fa-solid fa-arrow-down opacity-25"></i>
<div class="flow-item w-100 bg-primary border-primary text-white shadow-lg">DAEMON Clean Center
</div>
<i class="fa-solid fa-arrow-down opacity-25"></i>
<div class="flow-item w-100 border-success text-success fw-bold">정상 트래픽(Cleaned Data) -> 고객
서버(Client Server)
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<?= $this->include($viewDatas['layout']['path'] . '/welcome/network'); ?>
<!-- Data Center Section -->
<section id="datacenter" class="py-5 bg-light">
<div class="container py-5">
<div class="row g-4 align-items-center">
<div class="col-lg-5 order-2 order-lg-1">
<img src="https://images.unsplash.com/photo-1596272875729-ed2ff7d6d9c5?auto=format&fit=crop&q=80&w=800"
class="img-fluid rounded-4 shadow" alt="IDC Center">
</div>
<div class="col-lg-7 ps-lg-5 order-1 order-lg-2">
<h2 class="fw-bold mb-4">치바/도쿄 프리미엄 IDC</h2>
<p class="text-muted mb-5">아시아 비즈니스의 전략적 허브인 치바/도쿄에 위치하여 한국, 일본, 글로벌 트래픽을 가장 낮은 지연시간(Low Latency)으로
처리합니다.
</p>
<div class="row g-4">
<div class="col-sm-6">
<h5 class="fw-bold"><i class="fa-solid fa-bolt-lightning text-warning me-2"></i> 전력 보장</h5>
<p class="small text-muted">N+1 이중화 시스템 설비로 최대 가용성 보장.</p>
</div>
<div class="col-sm-6">
<h5 class="fw-bold"><i class="fa-solid fa-snowflake text-info me-2"></i> 공조 시설</h5>
<p class="small text-muted">고밀도 서버의 열기를 완벽히 제어하는 기업용 냉방 시스템 완비.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<?= $this->include($viewDatas['layout']['path'] . '/welcome/datacenter'); ?>
<!-- Support Section -->
<section id="support" class="py-5 bg-white">
<div class="container py-5">
@ -323,7 +36,7 @@
<i class="fa-brands fa-discord fa-2x text-indigo me-3" style="color: #5865F2;"></i>
<div>
<div class="fw-bold">Discord 커뮤니티</div>
<div class="small text-muted">k6nQg84N</div>
<div class="small text-muted">데몬아이디씨</div>
</div>
</div>
<a href="https://discord.com/invite/k6nQg84N" target="blank"
@ -331,27 +44,7 @@
</div>
</div>
<div class="col-lg-7">
<div class="card border-0 shadow-sm p-4 p-md-5 rounded-4 bg-light">
<form>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-bold">성함 / 업체명</label>
<input type="text" class="form-control" placeholder="성함을 입력하세요">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">이메일</label>
<input type="email" class="form-control" placeholder="contact@domain.com">
</div>
<div class="col-12">
<label class="form-label small fw-bold">문의 내용</label>
<textarea class="form-control" rows="5" placeholder="문의하실 내용을 상세히 적어주세요."></textarea>
</div>
<div class="col-12 mt-4">
<button type="button" class="btn btn-dark w-100 py-3 fw-bold">문의 접수하기</button>
</div>
</div>
</form>
</div>
<?= $this->include($viewDatas['layout']['path'] . '/welcome/inquiry'); ?>
</div>
</div>
</div>

View File

@ -0,0 +1,72 @@
<div class="card border-0 shadow-sm p-4 p-md-5 rounded-4 bg-light">
<form id="inquiryForm">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-bold">성함 / 업체명</label>
<input type="text" id="title" name="title" class="form-control" placeholder="성함을 입력하세요">
<div class="error" data-error-for="title"></div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">이메일</label>
<input type="email" id="email" name="email" class="form-control" placeholder="예) contact@domain.com">
<div class="error" data-error-for="email"></div>
</div>
<div class="col-12">
<label class="form-label small fw-bold">문의 내용</label>
<textarea id="content" name="content" class="form-control" rows="5"
placeholder="문의하실 내용을 상세히 적어주세요."></textarea>
<div class="error" data-error-for="content"></div>
</div>
<div class="col-12 mt-4">
<button type="submit" id="submit" class="btn btn-dark w-100 py-3 fw-bold">문의 접수하기</button>
</div>
</div>
</form>
<div class="error text-danger mb-2" data-error-for="_global"></div>
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('inquiryForm');
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
// 에러 초기화
document.querySelectorAll('.error').forEach(el => el.textContent = '');
const formData = new FormData(form);
const res = await fetch('/inquiry/create', {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
// 응답이 JSON이 아닐 수도 있으니 안전하게
let data = {};
try { data = await res.json(); } catch (e) { }
// ✅ 422: 필드별 에러
if (res.status === 422 && data.errors) {
Object.entries(data.errors).forEach(([field, msg]) => {
const box = document.querySelector(`[data-error-for="${field}"]`);
if (box) box.textContent = msg;
});
return;
}
// ✅ 그 외 에러: message를 전역 에러로 표시
if (!res.ok || data.ok === false) {
const globalBox = document.querySelector(`[data-error-for="_global"]`);
if (globalBox) globalBox.textContent = data.message || '처리 중 오류가 발생했습니다.';
else alert(data.message || '처리 중 오류가 발생했습니다.');
return;
}
// ✅ 성공
alert('문의가 접수되었습니다.');
form.reset();
});
});
</script>

View File

@ -0,0 +1,39 @@
<section id="network" class="py-5 bg-white">
<div class="container py-5">
<div class="row align-items-center g-5">
<div class="col-lg-6">
<h6 class="text-primary fw-bold text-uppercase mb-3">Network & Security</h6>
<h2 class="fw-bold mb-4">실시간 지능형 DDoS 방어 시스템</h2>
<p class="text-muted mb-4">단순한 트래픽 차단이 아닌, 정교한 데이터 정화 엔진(Scrubbing Center) 통해 정상적인 비즈니스 트래픽만을 서버로
전달합니다.</p>
<ul class="list-unstyled">
<li class="mb-3 d-flex align-items-start"><i
class="fa-solid fa-check-circle text-success me-3 mt-1"></i> <span><strong>L3/L4
Mitigation:</strong> UDP/SYN Flood 완벽 대응</span></li>
<li class="mb-3 d-flex align-items-start"><i
class="fa-solid fa-check-circle text-success me-3 mt-1"></i> <span><strong>BGP +
Anycast:</strong> 세계 주요 거점에서 공격 분산 처리</span></li>
<li class="mb-3 d-flex align-items-start"><i
class="fa-solid fa-check-circle text-success me-3 mt-1"></i> <span><strong>Always-on
Protection:</strong> 1 미만의 탐지 즉각 대응</span></li>
</ul>
</div>
<div class="col-lg-6">
<div class="scrubbing-box">
<div class="text-center text-white fw-bold mb-4 uppercase tracking-widest">트래픽 흐름도(Traffic Flow)
</div>
<div class="d-flex flex-column align-items-center gap-4">
<div class="flow-item w-100">전체 트래픽 (All Internet Traffic)</div>
<i class="fa-solid fa-arrow-down opacity-25"></i>
<div class="flow-item w-100 bg-primary border-primary text-white shadow-lg">DAEMON Clean Center
</div>
<i class="fa-solid fa-arrow-down opacity-25"></i>
<div class="flow-item w-100 border-success text-success fw-bold">정상 트래픽(Cleaned Data) -> 고객
서버(Client Server)
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,191 @@
<section id="servers" class="py-5 bg-light">
<div class="container py-5">
<div class="text-center mb-5">
<h2 class="fw-bold mb-3">전용 서버 호스팅</h2>
<p class="text-muted">도쿄 IDC 기반의 최신 하드웨어 라인업을 확인하세요.</p>
</div>
<div class="row g-4">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<!-- Proxmox 가상 서버 (추가 요청) -->
<div class="col">
<div class="server-card border-info" style="border-width: 2px;">
<div class="card-header-custom">
<div class="type-title text-info">Proxmox VPS</div>
<div class="type-subtitle text-info">고성능 가상 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">Hypervisor:</span> <span class="spec-value">Proxmox VE
(KVM)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">vCPU:</span> <span class="spec-value">Intel Xeon Gold
(Dedicated)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">8GB ~ 64GB (No
Overselling)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">Storage:</span> <span class="spec-value">Enterprise NVMe
SSD</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">Feature:</span> <span class="spec-value">Snapshot, Backup,
콘솔</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-info"></i> <span
class="spec-label">결제:</span> <span class="spec-value">계좌이체 , 비트코인 가능</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text text-info">가격 문의</div>
<button class="btn btn-info btn-inquiry text-white shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- B타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">B타입</div>
<div class="type-subtitle text-blue">범용 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (4 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">16GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">결제:</span> <span class="spec-value">계좌이체 , 비트코인 가능</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 45만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- C타입 (가장 인기) -->
<div class="col">
<div class="server-card card-featured shadow">
<div class="popular-badge">가장 인기</div>
<div class="card-header-custom">
<div class="type-title">C타입</div>
<div class="type-subtitle text-purple">고성능 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (8 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">16GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-purple"></i> <span
class="spec-label">결제:</span> <span class="spec-value">계좌이체 , 비트코인 가능</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text price-purple"> 55만원</div>
<button class="btn btn-primary btn-inquiry bg-purple shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- D타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">D타입</div>
<div class="type-subtitle text-blue">프리미엄 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (12 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">16GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">결제:</span> <span class="spec-value">계좌이체 , 비트코인 가능</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 65만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- E타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">E타입</div>
<div class="type-subtitle text-blue">엔터프라이즈 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (20 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">32GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">결제:</span> <span class="spec-value">계좌이체 , 비트코인 가능</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 75만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
<!-- E2타입 -->
<div class="col">
<div class="server-card">
<div class="card-header-custom">
<div class="type-title">E2타입</div>
<div class="type-subtitle text-blue">최상위 서버</div>
</div>
<div class="spec-list">
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">CPU:</span> <span class="spec-value">Xeon (28 Core)</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAM:</span> <span class="spec-value">32GB 이상</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">HDD:</span> <span class="spec-value">SSD 256GB x2 + SSD 500GB
x2</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">RAID 구성:</span> <span class="spec-value">RAID 1/1</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">주요 용도:</span> <span class="spec-value">모든 서비스 지원</span></div>
<div class="spec-item"><i class="fa-solid fa-check text-success"></i> <span
class="spec-label">결제:</span> <span class="spec-value">계좌이체 , 비트코인 가능</span></div>
</div>
<div class="card-footer-custom">
<div class="price-text"> 85만원</div>
<button class="btn btn-primary btn-inquiry bg-blue shadow-sm">문의하기</button>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,26 @@
<section class="py-5 bg-white border-bottom">
<div class="container text-center">
<div class="row g-4">
<div class="col-6 col-lg-3">
<i class="fa-solid fa-microchip text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">NVMe Gen4</h6>
<p class="small text-muted mb-0">엔터프라이즈 전용 SSD</p>
</div>
<div class="col-6 col-lg-3">
<i class="fa-solid fa-shield-halved text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">DDoS Protected</h6>
<p class="small text-muted mb-0">실시간 L3/L4/L7 방어</p>
</div>
<div class="col-6 col-lg-3">
<i class="fa-solid fa-bolt text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">100G Backbone</h6>
<p class="small text-muted mb-0">글로벌 고속 네트워크</p>
</div>
<div class="col-6 col-lg-3">
<i class="fa-solid fa-headset text-primary fa-2x mb-3"></i>
<h6 class="fw-bold">24/7 Monitoring</h6>
<p class="small text-muted mb-0">상주 엔지니어 지원</p>
</div>
</div>
</div>
</section>

View File

@ -3,9 +3,9 @@
<head>
<title><?= $viewDatas['layout']['title'] ?></title>
<?php foreach ($viewDatas['layout']['metas'] as $meta): ?><?= $meta ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['stylesheets'] as $stylesheet): ?><?= $stylesheet ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['javascripts'] as $javascript): ?><?= $javascript ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['metas'] as $meta): ?> <?= $meta ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['stylesheets'] as $stylesheet): ?> <?= $stylesheet ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['javascripts'] as $javascript): ?> <?= $javascript ?><?php endforeach; ?>
<link href="/css/<?= $viewDatas['layout']['path'] ?>.css" media="screen" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="/js/<?= $viewDatas['layout']['path'] ?>.js"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
@ -20,6 +20,6 @@
<body>
<?= $this->renderSection('content') ?>
</body>
<?php foreach ($viewDatas['layout']['footerScripts'] as $javascript): ?><?= $javascript ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['footerScripts'] as $javascript): ?> <?= $javascript ?><?php endforeach; ?>
</html>

View File

@ -1,3 +1,6 @@
<div class="accordion-item">
<a href="/admin/user"><?= ICONS['MEMBER'] ?> 계정 관리</a>
</div>
<div class="accordion-item">
<a href="/admin/inquiry"><?= ICONS['PHONE'] ?> 문의 관리</a>
</div>

View File

@ -3,9 +3,9 @@
<head>
<title><?= $viewDatas['layout']['title'] ?></title>
<?php foreach ($viewDatas['layout']['metas'] as $meta): ?><?= $meta ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['stylesheets'] as $stylesheet): ?><?= $stylesheet ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['javascripts'] as $javascript): ?><?= $javascript ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['metas'] as $meta): ?> <?= $meta ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['stylesheets'] as $stylesheet): ?> <?= $stylesheet ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['javascripts'] as $javascript): ?> <?= $javascript ?><?php endforeach; ?>
<link href="/css/<?= $viewDatas['layout']['path'] ?>.css" media="screen" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="/js/<?= $viewDatas['layout']['path'] ?>.js"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
@ -17,9 +17,9 @@
</head>
<body>
<?php if ($error = session('message')): ?><?= $viewDatas['service']->getHelper()->alertTrait($error) ?><?php endif ?>
<div class="middle"><?= $this->renderSection('content') ?></div>
<?php if ($error = session('message')): ?> <?= alertTrait($error) ?><?php endif ?>
<?= $this->renderSection('content') ?>
</body>
<?php foreach ($viewDatas['layout']['footerScripts'] as $javascript): ?><?= $javascript ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['footerScripts'] as $javascript): ?> <?= $javascript ?><?php endforeach; ?>
</html>

View File

@ -3,9 +3,9 @@
<head>
<title><?= $viewDatas['layout']['title'] ?></title>
<?php foreach ($viewDatas['layout']['metas'] as $meta): ?><?= $meta ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['stylesheets'] as $stylesheet): ?><?= $stylesheet ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['javascripts'] as $javascript): ?><?= $javascript ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['metas'] as $meta): ?> <?= $meta ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['stylesheets'] as $stylesheet): ?> <?= $stylesheet ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['javascripts'] as $javascript): ?> <?= $javascript ?><?php endforeach; ?>
<link href="/css/<?= $viewDatas['layout']['path'] ?>.css" media="screen" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="/js/<?= $viewDatas['layout']['path'] ?>.js"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
@ -17,9 +17,9 @@
</head>
<body>
<?php if ($error = session('message')): ?><?= $viewDatas['service']->getHelper()->alertTrait($error) ?><?php endif ?>
<?= $this->renderSection('content') ?></div>
<?php if ($error = session('message')): ?> <?= alertTrait($error) ?><?php endif ?>
<?= $this->renderSection('content') ?>
</body>
<?php foreach ($viewDatas['layout']['footerScripts'] as $javascript): ?><?= $javascript ?><?php endforeach; ?>
<?php foreach ($viewDatas['layout']['footerScripts'] as $javascript): ?> <?= $javascript ?><?php endforeach; ?>
</html>

View File

@ -6,8 +6,7 @@
<div class="d-flex align-items-center mb-4">
<img src="https://daemon-idc.com/wp-content/uploads/2024/08/cropped-logo-small-30x32.png"
height="32" class="me-2 filter-white" style="filter: brightness(0) invert(1);">
<span class="h4 mb-0 fw-bold text-white">DAEMON</span>
<span class="h4 mb-0 fw-light text-muted ms-1">IDC</span>
<span class="h4 mb-0 fw-bold text-white">DAEMON IDC</span>
</div>
<p class="small lh-lg mb-0">데몬 IDC는 고성능 전용 서버와 엔터프라이즈급 보안 솔루션을 제공하는 글로벌 인프라 전문 브랜드입니다. 도쿄 가야바초의 최첨단 시설에서
귀사의 비즈니스를 보호합니다.</p>
@ -15,30 +14,33 @@
<div class="col-6 col-lg-2">
<h6 class="text-white fw-bold mb-4">서비스</h6>
<ul class="list-unstyled">
<li class="mb-2"><a href="#">데디케이티드 서버</a></li>
<li class="mb-2"><a href="#">전용선 서버</a></li>
<li class="mb-2"><a href="#">베어메탈 호스팅</a></li>
<li class="mb-2"><a href="#">DDoS 방어</a></li>
<li class="mb-2"><a href="#">게임 서버 인프라</a></li>
<li class="mb-2"><a href="#">가상 서버 인프라</a></li>
<li class="mb-2"><a href="#">VPN 업무용 서버</a></li>
</ul>
</div>
<div class="col-6 col-lg-2">
<h6 class="text-white fw-bold mb-4">고객지원</h6>
<ul class="list-unstyled">
<li class="mb-2"><a href="#">기술 지원</a></li>
<li class="mb-2"><a href="#">서비스 이용약관</a></li>
<li class="mb-2"><a href="#">개인정보처리방침</a></li>
<li class="mb-2"><a href="#">SLA 보장</a></li>
<li class="mb-2"><a href="#">전화 상담</a></li>
<!-- <li class="mb-2"><a href="#">개인정보처리방침</a></li> -->
</ul>
</div>
<div class="col-lg-4">
<h6 class="text-white fw-bold mb-4">Contact Info</h6>
<h6 class="text-white fw-bold mb-4">연락처</h6>
<ul class="list-unstyled small">
<li class="mb-3 d-flex align-items-center"><i class="fa-solid fa-envelope me-3"></i>
sales@daemon-idc.com</li>
<li class="mb-3 d-flex align-items-center"><i class="fa-brands fa-telegram me-3"></i> @daemonidc
(Telegram)</li>
<li class="mb-3 d-flex align-items-center"><i class="fa-brands fa-discord me-3"></i> k6nQg84N
(Discord)</li>
<li class="mb-3 d-flex align-items-center"><i class="fa-solid fa-phone me-3"></i> 070-8672-0021
</li>
<li class="mb-3 d-flex align-items-center"><i
class="fa-solid fa-envelope me-3"></i>webmaster@daemon-idc.com</li>
<li class="mb-3 d-flex align-items-center"><i class="fa-brands fa-telegram me-3"></i>
@daemonidc(Telegram)</li>
<li class="mb-3 d-flex align-items-center"><i class="fa-brands fa-discord me-3"></i> 데몬아이디씨(Discord)
</li>
<li class="mt-4"><span class="badge bg-success text-white px-3 py-2 uppercase fw-bold">System
Status: Normal</span></li>
</ul>
@ -48,7 +50,7 @@
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center small text-muted">
<p class="mb-0">&copy; 2024 Daemon IDC. All rights reserved.</p>
<div class="d-flex gap-4 mt-3 mt-md-0 fw-bold text-uppercase" style="font-size: 10px; letter-spacing: 2px;">
<span>Tokyo, Japan Facility</span>
<span>Chiba/Tokyo, Japan Facility</span>
<span>Engineering for Excellence</span>
</div>
</div>

View File

@ -26,7 +26,7 @@
<a href="https://discord.com/invite/k6nQg84N" class="btn btn-discord px-3 py-2 rounded-3">
<i class="fa-brands fa-discord me-2"></i>Discord
</a>
<a href="#" class="btn btn-dark btn-sm fw-bold px-3 py-2 rounded-3 ms-lg-2">로그인</a>
<!-- <a href="#" class="btn btn-dark btn-sm fw-bold px-3 py-2 rounded-3 ms-lg-2">로그인</a> -->
</div>
</div>
</div>

View File

@ -12,7 +12,12 @@
</span>
</li>
<li class="nav-item">
<span class="nav-link active" aria-current="page" style="cursor:pointer;"><a
href="/admin/customer/client">고객정보</a></span>
<span class="nav-link active" aria-current="page" style="cursor:pointer;"><a href="/admin/user">사용자정보</a></span>
</li>
<li class="nav-item"></li>
<span class="nav-link active" aria-current="page" style="cursor:pointer;"><a href="/admin/board">게시판정보</a></span>
</li>
<li class="nav-item"></li>
<span class="nav-link active" aria-current="page" style="cursor:pointer;"><a href="/admin/inquiry">문의정보</a></span>
</li>
</ul>

View File

@ -65,13 +65,20 @@ body {
/* Hero Section */
.hero-section {
padding: 160px 0 100px;
background: linear-gradient(rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.85)),
url('https://images.unsplash.com/photo-1558494949-ef010cbdcc48?auto=format&fit=crop&q=80&w=1920');
position: relative;
min-height: 600px;
display: flex;
align-items: center;
justify-content: center;
/* 고품질 데이터센터 서버룸 배경 이미지 */
background: linear-gradient(rgba(15, 23, 42, 0.8), rgba(15, 23, 42, 0.85)),
url('/images/main_background.png');
background-size: cover;
background-position: center;
background-position: center center;
background-attachment: scroll;
color: white;
text-align: center;
padding: 100px 0;
}
/* Product Cards */

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB