diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..17c9d95 --- /dev/null +++ b/.htaccess @@ -0,0 +1,23 @@ +RewriteEngine On + +# Skip existing files and directories. +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] + +# Admin UI +RewriteRule ^admin/?$ /kngil/skin/adm.php [L] +RewriteRule ^admin/company/?$ /kngil/skin/adm_comp.php [L] + +# Admin APIs +RewriteRule ^admin/api/super/?$ /kngil/bbs/adm.php [QSA,L] +RewriteRule ^admin/api/company/?$ /kngil/bbs/adm_comp.php [QSA,L] +RewriteRule ^admin/api/service/?$ /kngil/bbs/adm_service.php [QSA,L] +RewriteRule ^admin/api/purchase-history/?$ /kngil/bbs/adm_purch_popup.php [QSA,L] +RewriteRule ^admin/api/use-history/?$ /kngil/bbs/adm_use_history.php [QSA,L] +RewriteRule ^admin/api/product/?$ /kngil/bbs/adm_product_popup.php [QSA,L] +RewriteRule ^admin/api/product/save/?$ /kngil/bbs/adm_product_popup_save.php [QSA,L] +RewriteRule ^admin/api/product/delete/?$ /kngil/bbs/adm_product_popup_delete.php [QSA,L] +RewriteRule ^admin/api/faq/?$ /kngil/bbs/adm_faq_popup.php [QSA,L] +RewriteRule ^admin/api/faq/save/?$ /kngil/bbs/adm_faq_popup_save.php [QSA,L] +RewriteRule ^admin/api/faq/delete/?$ /kngil/bbs/adm_faq_popup_delete.php [QSA,L] diff --git a/README.md b/README.md index e626864..6db302e 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,19 @@ ## 빠른 시작 ```bash -docker compose up --build +docker compose up -d --build ``` - 접속: `http://localhost:8080` +```bash +docker compose down +``` ## 환경변수 `docker-compose.yml`에서 기본값을 사용하며, 필요 시 `.env`로 덮어쓸 수 있습니다. - `DB_HOST` (기본값: `db`) -- `DB_PORT` (기본값: `5432`) +- `DB_PORT` (기본값: `5432`) - 웹 컨테이너가 DB에 접속할 때 사용하는 포트 +- `DB_HOST_PORT` (기본값: `5432`) - 외부에서 포트포워딩으로 접속할 때 사용하는 호스트 포트 - `DB_NAME` (기본값: `kngil`) - `DB_USER` (기본값: `postgres`) - `DB_PASS` (기본값: `postgres`) @@ -43,5 +47,5 @@ docker compose down -v - `kngil/bbs/sales_results.php`는 410 응답으로 비활성 처리되어 있습니다. ## PostgreSQL 이미지 버전 -- 기본값은 `postgres:18`입니다. +- 기본값은 `postgres:16`입니다. - 이미지 풀 실패 시 `docker-compose.yml`의 태그를 사용 가능한 버전으로 변경하세요. diff --git a/docker-compose.yml b/docker-compose.yml index 60cbc39..f4d9872 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,8 +21,14 @@ services: db: image: postgres:16 - # ports: - # - "5432:5432" + ports: + - "0.0.0.0:${DB_HOST_PORT:-5432}:5432" + command: + - "postgres" + - "-c" + - "listen_addresses=*" + - "-c" + - "hba_file=/etc/postgresql/pg_hba.conf" environment: POSTGRES_DB: ${DB_NAME:-kngil} POSTGRES_USER: ${DB_USER:-postgres} @@ -30,6 +36,7 @@ services: volumes: - db_data:/var/lib/postgresql/data - ./docker/initdb/01_kngil_DB.sql:/docker-entrypoint-initdb.d/01_kngil_DB.sql:ro + - ./docker/postgres/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro volumes: db_data: diff --git a/docker/postgres/pg_hba.conf b/docker/postgres/pg_hba.conf new file mode 100644 index 0000000..fa5505e --- /dev/null +++ b/docker/postgres/pg_hba.conf @@ -0,0 +1,5 @@ +# +# Allow TCP connections. Narrow the address range in production. +# +host all all 0.0.0.0/0 scram-sha-256 +host all all ::/0 scram-sha-256 diff --git a/index.php b/index.php index 3fe7bac..4df456f 100644 --- a/index.php +++ b/index.php @@ -7,7 +7,7 @@ declare(strict_types=1); // 1. 기본 상수 // --------------------------------- define('ROOT', __DIR__); -define('SKIN_PATH', ROOT.'/skin'); +define('SKIN_PATH', ROOT.'/kngil/skin'); // --------------------------------- // 2. 페이지 결정 diff --git a/kngil/auth/oidc-callback.php b/kngil/auth/oidc-callback.php index 60934e6..eacdee0 100644 --- a/kngil/auth/oidc-callback.php +++ b/kngil/auth/oidc-callback.php @@ -1,12 +1,17 @@ setRedirectURL($config['redirect_url']); try { + $stmt = $pdo->query("SELECT to_regclass('kngil.users') AS reg"); + $reg = $stmt ? $stmt->fetchColumn() : null; + if (!$reg) { + $stmt = $pdo->query("SELECT to_regclass('public.users') AS reg"); + $reg = $stmt ? $stmt->fetchColumn() : null; + if ($reg) { + $usersTable = 'public.users'; + $membersTable = 'public.members'; + } else { + throw new Exception( + "사용자 테이블을 찾을 수 없습니다. DB 초기화가 필요합니다. " + . "docker compose down -v 후 다시 실행하거나, " + . "DB_NAME/DB_USER/DB_PASS 설정을 확인하세요." + ); + } + } + + $memberReg = $pdo->query("SELECT to_regclass('{$membersTable}') AS reg"); + $memberReg = $memberReg ? $memberReg->fetchColumn() : null; + if (!$memberReg) { + $altMembersTable = $membersTable === 'kngil.members' ? 'public.members' : 'kngil.members'; + $memberReg = $pdo->query("SELECT to_regclass('{$altMembersTable}') AS reg"); + $memberReg = $memberReg ? $memberReg->fetchColumn() : null; + if ($memberReg) { + $membersTable = $altMembersTable; + } else { + throw new Exception("회원 테이블을 찾을 수 없습니다. DB 초기화가 필요합니다."); + } + } + + $pdo->exec("ALTER TABLE {$usersTable} ADD COLUMN IF NOT EXISTS oidc_sub VARCHAR(255) UNIQUE"); + if (!$oidc->authenticate()) { throw new Exception("Authentication failed"); } $userInfo = $oidc->requestUserInfo(); + $idToken = $oidc->getIdToken(); + $accessToken = $oidc->getAccessToken(); + $jwtClaims = []; + if (!empty($idToken)) { + $parts = explode('.', $idToken); + if (count($parts) >= 2) { + $payload = strtr($parts[1], '-_', '+/'); + $padding = 4 - (strlen($payload) % 4); + if ($padding < 4) { + $payload .= str_repeat('=', $padding); + } + $decoded = json_decode(base64_decode($payload), true); + if (is_array($decoded)) { + $jwtClaims = $decoded; + } + } + } + + // 디버그용: ID 토큰 확보 여부 로그 출력 (파일) + $logDir = dirname(__DIR__) . '/log'; + if (!is_dir($logDir)) { + @mkdir($logDir, 0775, true); + } + $logPath = $logDir . '/oidc_debug.log'; + if (!is_writable($logDir)) { + $logPath = '/tmp/oidc_debug.log'; + error_log('[OIDC_DEBUG] log_dir_not_writable, fallback=/tmp/oidc_debug.log'); + } + $tokenInfo = empty($idToken) ? 'MISSING' : ('PRESENT len=' . strlen($idToken)); + $claimKeys = empty($jwtClaims) ? 'none' : implode(',', array_keys($jwtClaims)); + $logLine = sprintf( + "[%s] host=%s uri=%s sid=%s id_token=%s claims=%s\n", + date('c'), + $_SERVER['HTTP_HOST'] ?? '-', + $_SERVER['REQUEST_URI'] ?? '-', + session_id(), + $tokenInfo, + $claimKeys + ); + $writeOk = @file_put_contents($logPath, $logLine, FILE_APPEND); + if ($writeOk === false) { + error_log('[OIDC_DEBUG] log_write_failed path=' . $logPath); + } + // 디버그용: userInfo/claims 전체 덤프 (토큰 제외) + $dump = [ + 'userInfo' => $userInfo, + 'jwtClaims' => $jwtClaims + ]; + $dumpLine = sprintf( + "[%s] oidc_dump=%s\n", + date('c'), + json_encode($dump, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + $dumpOk = @file_put_contents($logPath, $dumpLine, FILE_APPEND); + if ($dumpOk === false) { + error_log('[OIDC_DEBUG] dump_write_failed path=' . $logPath); + } + // 도커 로그로도 출력 + error_log('[OIDC_DEBUG] ' . $dumpLine); // $userInfo 에 포함된 데이터 예시: sub, email, name, preferred_username 등 $email = $userInfo->email ?? null; $sub = $userInfo->sub ?? null; // IDP 고유 식별자 - $name = $userInfo->name ?? ($userInfo->preferred_username ?? 'Unknown'); + $preferred = $userInfo->preferred_username ?? null; + $name = $userInfo->name ?? null; + if (!$email && isset($jwtClaims['email'])) { + $email = $jwtClaims['email']; + } + if (!$name && isset($jwtClaims['name'])) { + $name = $jwtClaims['name']; + } + if (!$name && $preferred) { + $name = $preferred; + } + if (!$name && $email) { + $name = $email; + } + if (!$name && $sub) { + $seed = strtolower(preg_replace('/[^a-z0-9]/', '', (string)$sub)); + $name = 'oidc_' . substr($seed, 0, 10); + } if (!$email && !$sub) { throw new Exception("IDP provided insufficient user information."); @@ -33,7 +146,7 @@ try { // 1. 사용자 매핑 (sub 또는 email 기준) $stmt = $pdo->prepare(" - SELECT * FROM kngil.users + SELECT * FROM {$usersTable} WHERE (oidc_sub = :sub OR LOWER(email) = LOWER(:email)) AND use_yn = 'Y' LIMIT 1 @@ -42,14 +155,99 @@ try { $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { - // [정책 선택] 새 사용자 자동 생성 또는 로그인 거부 - // 여기서는 예시로 로그인 거부 처리 - throw new Exception("등록되지 않은 사용자입니다. 관리자에게 문의하세요. (IDP: $email)"); + $defaultMemberId = getenv('OIDC_DEFAULT_MEMBER_ID') ?: ''; + if ($defaultMemberId !== '') { + $checkMember = $pdo->prepare("SELECT 1 FROM {$membersTable} WHERE member_id = :member_id LIMIT 1"); + $checkMember->execute([':member_id' => $defaultMemberId]); + if (!$checkMember->fetchColumn()) { + throw new Exception("OIDC_DEFAULT_MEMBER_ID가 members에 존재하지 않습니다: {$defaultMemberId}"); + } + } else { + $memberStmt = $pdo->query("SELECT member_id FROM {$membersTable} ORDER BY member_id ASC LIMIT 1"); + $defaultMemberId = $memberStmt ? $memberStmt->fetchColumn() : ''; + if (!$defaultMemberId) { + throw new Exception("기본 member_id를 찾을 수 없습니다. OIDC_DEFAULT_MEMBER_ID를 설정하세요."); + } + } + + $defaultAuth = getenv('OIDC_DEFAULT_AUTH_BC') ?: 'BS100500'; + + $baseId = $userInfo->preferred_username ?? ($email ? explode('@', $email)[0] : ''); + $baseId = strtolower(preg_replace('/[^a-z0-9]/', '', $baseId)); + if ($baseId === '') { + $seed = strtolower(preg_replace('/[^a-z0-9]/', '', (string)($sub ?? 'oidc'))); + $baseId = 'oidc' . substr($seed, 0, 10); + } + $baseId = substr($baseId, 0, 16); + $userId = $baseId; + + $existsStmt = $pdo->prepare("SELECT 1 FROM {$usersTable} WHERE LOWER(user_id) = LOWER(:user_id) LIMIT 1"); + $suffix = 1; + while (true) { + $existsStmt->execute([':user_id' => $userId]); + if (!$existsStmt->fetchColumn()) { + break; + } + $tail = sprintf('%02d', $suffix); + $userId = substr($baseId, 0, 20 - strlen($tail)) . $tail; + $suffix++; + if ($suffix > 99) { + $userId = 'oidc' . bin2hex(random_bytes(4)); + $userId = substr($userId, 0, 20); + } + } + + $userNm = $name ?: ($email ?: $userId); + $rawPhone = $userInfo->phone_number ?? ''; + $digits = preg_replace('/\D/', '', $rawPhone); + if (strlen($digits) === 11) { + $telNo = substr($digits, 0, 3) . '-' . substr($digits, 3, 4) . '-' . substr($digits, 7, 4); + } elseif (strlen($digits) === 10) { + $telNo = substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4); + } else { + $telNo = '000-0000-0000'; + } + + $insert = $pdo->prepare(" + INSERT INTO {$usersTable} ( + member_id, user_id, user_pw, user_nm, + dept_nm, posit_nm, tel_no, email, + auth_bc, use_yn, rmks, + cid, cdt, mid, mdt, oidc_sub + ) VALUES ( + :member_id, :user_id, NULL, :user_nm, + :dept_nm, :posit_nm, :tel_no, :email, + :auth_bc, 'Y', :rmks, + :cid, CURRENT_TIMESTAMP, :mid, CURRENT_TIMESTAMP, :oidc_sub + ) + "); + $insert->execute([ + ':member_id' => $defaultMemberId, + ':user_id' => $userId, + ':user_nm' => $userNm, + ':dept_nm' => $userInfo->department ?? null, + ':posit_nm' => $userInfo->title ?? null, + ':tel_no' => $telNo, + ':email' => $email, + ':auth_bc' => $defaultAuth, + ':rmks' => 'OIDC auto-registered', + ':cid' => $userId, + ':mid' => $userId, + ':oidc_sub' => $sub + ]); + + $stmt = $pdo->prepare(" + SELECT * FROM {$usersTable} + WHERE LOWER(user_id) = LOWER(:user_id) + LIMIT 1 + "); + $stmt->execute([':user_id' => $userId]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); } // 2. oidc_sub 업데이트 (최초 연동 시) if (empty($user['oidc_sub']) && $sub) { - $upd = $pdo->prepare("UPDATE kngil.users SET oidc_sub = :sub WHERE user_id = :id"); + $upd = $pdo->prepare("UPDATE {$usersTable} SET oidc_sub = :sub WHERE user_id = :id"); $upd->execute([':sub' => $sub, ':id' => $user['user_id']]); } @@ -63,20 +261,42 @@ try { 'dept_nm' => $user['dept_nm'] ?? null, 'tel_no' => $user['tel_no'] ?? null, 'email' => $user['email'] ?? null, + 'idp_name' => $name ?: null, + 'idp_email' => $email ?? null, + 'idp_id_token' => $idToken ?? null, + 'idp_access_token' => $accessToken ?? null, + 'idp_claims' => $jwtClaims ?? null, 'oidc_mode' => true // OIDC 로그인을 나타내는 플래그 ]; - // 로그인 완료 후 부모 창에 알리고 종료 + session_write_close(); + + // 로그인 완료 후 부모 창에 알리고 종료 (팝업이 아닐 경우 메인으로 이동) ?>
+ res.json()) // .then(d => { // if (d.status !== 'success') { @@ -243,7 +243,7 @@ function formatBizNo(value) { 상단 회사 목록 로드 ---------------------------------------- */ function loadCompanies() { - fetch('/kngil/bbs/adm.php') + fetch('/admin/api/super') .then(res => res.json()) .then(json => { if (!json.records) return @@ -340,7 +340,7 @@ export function bindSaveButton() { return } - fetch('/kngil/bbs/adm.php?action=save', { + fetch('/admin/api/super?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/kngil/js/adm_comp copy.js b/kngil/js/adm_comp copy.js index b4e9589..6b224a7 100644 --- a/kngil/js/adm_comp copy.js +++ b/kngil/js/adm_comp copy.js @@ -9,7 +9,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -144,7 +144,7 @@ export async function createUserGrid(boxId, options = {}) { } function loadUsers() { - fetch('/kngil/bbs/adm_comp.php') + fetch('/admin/api/company') .then(res => res.text()) // 🔥 먼저 text로 확인 .then(text => { try { @@ -168,7 +168,7 @@ export function loadUsersByMember(member_id) { return } - fetch('/kngil/bbs/adm_comp.php') + fetch('/admin/api/company') .then(res => res.json()) .then(json => { g.clear() @@ -198,7 +198,7 @@ export function setUserGridMode(mode = 'view') { export function loadData({ loadSummary = true } = {}) { - fetch('/kngil/bbs/adm_comp.php') + fetch('/admin/api/company') .then(res => res.json()) .then(async d => { @@ -323,7 +323,7 @@ document.getElementById('btnSave_comp')?.addEventListener('click', () => { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -408,7 +408,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`) .yes(() => { - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -436,7 +436,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { }) function loadTotalArea(memberId) { - return fetch(`/kngil/bbs/adm_comp.php?action=total_area&member_id=${memberId}`) + return fetch(`/admin/api/company?action=total_area&member_id=${memberId}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -467,7 +467,7 @@ function doSearch() { } // ⚠️ type === 'id' 는 DB로 안 보냄 - fetch(`/kngil/bbs/adm_comp.php?action=list` + fetch(`/admin/api/company?action=list` + `&user_nm=${encodeURIComponent(p_user_nm)}` + `&dept_nm=${encodeURIComponent(p_dept_nm)}` + `&use_yn=${useYn}` diff --git a/kngil/js/adm_comp.js b/kngil/js/adm_comp.js index c7eadd8..90991a7 100644 --- a/kngil/js/adm_comp.js +++ b/kngil/js/adm_comp.js @@ -21,7 +21,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -251,7 +251,7 @@ export function loadUsersByMember(memberId) { return } - fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${memberId}`) + fetch(`/admin/api/company?action=list&member_id=${memberId}`) .then(res => res.json()) .then(d => { @@ -285,7 +285,7 @@ export function setUserGridMode(mode = 'view') { } export function loadData({ loadSummary = true } = {}) { - fetch('/kngil/bbs/adm_comp.php?action=list') + fetch('/admin/api/company?action=list') .then(res => res.json()) .then(async d => { @@ -404,7 +404,7 @@ document.getElementById('btnSave_comp')?.addEventListener('click', () => { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -489,7 +489,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`) .yes(() => { - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -517,7 +517,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { }) function loadTotalArea(memberId) { - return fetch(`/kngil/bbs/adm_comp.php?action=total_area`) + return fetch(`/admin/api/company?action=total_area`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -548,7 +548,7 @@ function doSearch() { } // ⚠️ type === 'id' 는 DB로 안 보냄 - fetch(`/kngil/bbs/adm_comp.php?action=list` + fetch(`/admin/api/company?action=list` + `&user_nm=${encodeURIComponent(p_user_nm)}` + `&dept_nm=${encodeURIComponent(p_dept_nm)}` + `&use_yn=${useYn}` @@ -670,7 +670,7 @@ function openBulkCreatePopup(memberId) { function runBulkCreate(memberId, csvUrl) { - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -771,7 +771,7 @@ function loadDataByMemberId(memberId) { return; } - fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${encodeURIComponent(memberId)}`) + fetch(`/admin/api/company?action=list&member_id=${encodeURIComponent(memberId)}`) .then(res => res.json()) .then(async d => { diff --git a/kngil/js/adm_faq_popup.js b/kngil/js/adm_faq_popup.js index fc59dc2..a2b8a8f 100644 --- a/kngil/js/adm_faq_popup.js +++ b/kngil/js/adm_faq_popup.js @@ -11,7 +11,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -100,7 +100,7 @@ export function openfaqPopup() { // 3. 브라우저 기본 확인창 사용 (가장 확실함) if (confirm(`선택한 ${ids.length}개의 상품을 삭제하시겠습니까?`)) { - fetch('/kngil/bbs/adm_faq_popup_delete.php', { + fetch('/admin/api/faq/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', ids: ids }) @@ -169,7 +169,7 @@ export function openfaqPopup() { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_faq_popup_save.php', { + fetch('/admin/api/faq/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -270,7 +270,7 @@ async function loadfaqData() { try { w2ui.faqGrid.lock('조회 중...', true); - const response = await fetch('/kngil/bbs/adm_faq_popup.php'); // PHP 파일 호출 + const response = await fetch('/admin/api/faq'); // PHP 파일 호출 const data = await response.json(); w2ui.faqGrid.clear(); diff --git a/kngil/js/adm_product_popup.js b/kngil/js/adm_product_popup.js index 8e522fc..b91bec1 100644 --- a/kngil/js/adm_product_popup.js +++ b/kngil/js/adm_product_popup.js @@ -11,7 +11,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -101,7 +101,7 @@ export function openProductPopup() { // 3. 브라우저 기본 확인창 사용 (가장 확실함) if (confirm(`선택한 ${ids.length}개의 상품을 삭제하시겠습니까?`)) { - fetch('/kngil/bbs/adm_product_popup_delete.php', { + fetch('/admin/api/product/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', ids: ids }) @@ -171,7 +171,7 @@ export function openProductPopup() { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_product_popup_save.php', { + fetch('/admin/api/product/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -265,7 +265,7 @@ async function loadProductData() { try { w2ui.productGrid.lock('조회 중...', true); - const response = await fetch('/kngil/bbs/adm_product_popup.php'); // PHP 파일 호출 + const response = await fetch('/admin/api/product'); // PHP 파일 호출 const data = await response.json(); w2ui.productGrid.clear(); diff --git a/kngil/js/adm_purch_popup.js b/kngil/js/adm_purch_popup.js index 2a44fc3..b6d770d 100644 --- a/kngil/js/adm_purch_popup.js +++ b/kngil/js/adm_purch_popup.js @@ -117,7 +117,7 @@ async function loadPurchaseHistoryData(memberId) { searchParams.append('fbuy_dt', ''); searchParams.append('tbuy_dt', ''); - const response = await fetch('/kngil/bbs/adm_purch_popup.php', { + const response = await fetch('/admin/api/purchase-history', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: searchParams diff --git a/kngil/js/adm_service copy.js b/kngil/js/adm_service copy.js index 5b778a6..b9d10f0 100644 --- a/kngil/js/adm_service copy.js +++ b/kngil/js/adm_service copy.js @@ -173,7 +173,7 @@ function addServiceFromProduct(p) { ------------------------------------------------- */ function loadExistingPurchase(memberId, buyDate) { - fetch(`/kngil/bbs/adm_service.php?member_id=${memberId}&buy_date=${buyDate}`) + fetch(`/admin/api/service?member_id=${memberId}&buy_date=${buyDate}`) .then(res => res.json()) .then(json => { diff --git a/kngil/js/adm_service.js b/kngil/js/adm_service.js index 71a4c05..f041d47 100644 --- a/kngil/js/adm_service.js +++ b/kngil/js/adm_service.js @@ -288,7 +288,7 @@ function createProductList() { new w2grid({ name: 'productList', box: '#productList', - url: '/kngil/bbs/adm_product_popup.php', + url: '/admin/api/product', columns: [ { field: 'itm_nm', text: '상품명', size: '120px' }, { @@ -413,7 +413,7 @@ function deleteServiceImmediately(row) { sq_no: row.sq_no }) - fetch('/kngil/bbs/adm_service.php', { + fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -446,7 +446,7 @@ function isServiceItem(r) { ------------------------------------------------- */ function loadExistingPurchase(memberId, buyDate) { - fetch('/kngil/bbs/adm_service.php', { + fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -606,7 +606,7 @@ function saveService(ctx) { _deleted: r._deleted || false })) - fetch('/kngil/bbs/adm_service.php', { + fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/kngil/js/adm_use_history.js b/kngil/js/adm_use_history.js index 206ae04..c15d64c 100644 --- a/kngil/js/adm_use_history.js +++ b/kngil/js/adm_use_history.js @@ -131,7 +131,7 @@ async function loadUseHistoryData(memberId = ''){ searchParams.append('user_nm', sUnm); searchParams.append('dept_nm', sDnm); - const response = await fetch('/kngil/bbs/adm_use_history.php', { + const response = await fetch('/admin/api/use-history', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: searchParams diff --git a/kngil/js/common.js b/kngil/js/common.js index 846129c..ddd10a6 100644 --- a/kngil/js/common.js +++ b/kngil/js/common.js @@ -4286,4 +4286,44 @@ return lottie; */ var Swiper=function(){"use strict";function e(e){return null!==e&&"object"==typeof e&&"constructor"in e&&e.constructor===Object}function t(s,a){void 0===s&&(s={}),void 0===a&&(a={});const i=["__proto__","constructor","prototype"];Object.keys(a).filter((e=>i.indexOf(e)<0)).forEach((i=>{void 0===s[i]?s[i]=a[i]:e(a[i])&&e(s[i])&&Object.keys(a[i]).length>0&&t(s[i],a[i])}))}const s={body:{},addEventListener(){},removeEventListener(){},activeElement:{blur(){},nodeName:""},querySelector:()=>null,querySelectorAll:()=>[],getElementById:()=>null,createEvent:()=>({initEvent(){}}),createElement:()=>({children:[],childNodes:[],style:{},setAttribute(){},getElementsByTagName:()=>[]}),createElementNS:()=>({}),importNode:()=>null,location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""}};function a(){const e="undefined"!=typeof document?document:{};return t(e,s),e}const i={document:s,navigator:{userAgent:""},location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""},history:{replaceState(){},pushState(){},go(){},back(){}},CustomEvent:function(){return this},addEventListener(){},removeEventListener(){},getComputedStyle:()=>({getPropertyValue:()=>""}),Image(){},Date(){},screen:{},setTimeout(){},clearTimeout(){},matchMedia:()=>({}),requestAnimationFrame:e=>"undefined"==typeof setTimeout?(e(),null):setTimeout(e,0),cancelAnimationFrame(e){"undefined"!=typeof setTimeout&&clearTimeout(e)}};function r(){const e="undefined"!=typeof window?window:{};return t(e,i),e}function n(e){return void 0===e&&(e=""),e.trim().split(" ").filter((e=>!!e.trim()))}function l(e,t){return void 0===t&&(t=0),setTimeout(e,t)}function o(){return Date.now()}function d(e,t){void 0===t&&(t="x");const s=r();let a,i,n;const l=function(e){const t=r();let s;return t.getComputedStyle&&(s=t.getComputedStyle(e,null)),!s&&e.currentStyle&&(s=e.currentStyle),s||(s=e.style),s}(e);return s.WebKitCSSMatrix?(i=l.transform||l.webkitTransform,i.split(",").length>6&&(i=i.split(", ").map((e=>e.replace(",","."))).join(", ")),n=new s.WebKitCSSMatrix("none"===i?"":i)):(n=l.MozTransform||l.OTransform||l.MsTransform||l.msTransform||l.transform||l.getPropertyValue("transform").replace("translate(","matrix(1, 0, 0, 1,"),a=n.toString().split(",")),"x"===t&&(i=s.WebKitCSSMatrix?n.m41:16===a.length?parseFloat(a[12]):parseFloat(a[4])),"y"===t&&(i=s.WebKitCSSMatrix?n.m42:16===a.length?parseFloat(a[13]):parseFloat(a[5])),i||0}function c(e){return"object"==typeof e&&null!==e&&e.constructor&&"Object"===Object.prototype.toString.call(e).slice(8,-1)}function p(){const e=Object(arguments.length<=0?void 0:arguments[0]),t=["__proto__","constructor","prototype"];for(let a=1;a1&&m.push(e.virtualSize-r)}if(o&&s.loop){const t=g[0]+x;if(s.slidesPerGroup>1){const a=Math.ceil((e.virtual.slidesBefore+e.virtual.slidesAfter)/s.slidesPerGroup),i=t*s.slidesPerGroup;for(let e=0;e!(s.cssMode&&!s.loop)||t!==c.length-1)).forEach((e=>{e.style[t]=`${x}px`}))}if(s.centeredSlides&&s.centeredSlidesBounds){let e=0;g.forEach((t=>{e+=t+(x||0)})),e-=x;const t=e>r?e-r:0;m=m.map((e=>e<=0?-v:e>t?t+w:e))}if(s.centerInsufficientSlides){let e=0;g.forEach((t=>{e+=t+(x||0)})),e-=x;const t=(s.slidesOffsetBefore||0)+(s.slidesOffsetAfter||0);if(e+t 0?(r._cssModeVirtualInitialSet=!0,requestAnimationFrame((()=>{h[e?"scrollLeft":"scrollTop"]=s}))):h[e?"scrollLeft":"scrollTop"]=s,y&&requestAnimationFrame((()=>{r.wrapperEl.style.scrollSnapType="",r._immediateVirtual=!1}));else{if(!r.support.smoothScroll)return m({swiper:r,targetPosition:s,side:e?"left":"top"}),!0;h.scrollTo({[e?"left":"top"]:s,behavior:"smooth"})}return!0}const E=A().isSafari;return y&&!i&&E&&r.isElement&&r.virtual.update(!1,!1,n),r.setTransition(t),r.setTranslate(w),r.updateActiveIndex(n),r.updateSlidesClasses(),r.emit("beforeTransitionStart",t,a),r.transitionStart(s,b),0===t?r.transitionEnd(s,b):r.animating||(r.animating=!0,r.onSlideToWrapperTransitionEnd||(r.onSlideToWrapperTransitionEnd=function(e){r&&!r.destroyed&&e.target===this&&(r.wrapperEl.removeEventListener("transitionend",r.onSlideToWrapperTransitionEnd),r.onSlideToWrapperTransitionEnd=null,delete r.onSlideToWrapperTransitionEnd,r.transitionEnd(s,b))}),r.wrapperEl.addEventListener("transitionend",r.onSlideToWrapperTransitionEnd)),!0},slideToLoop:function(e,t,s,a){if(void 0===e&&(e=0),void 0===s&&(s=!0),"string"==typeof e){e=parseInt(e,10)}const i=this;if(i.destroyed)return;void 0===t&&(t=i.params.speed);const r=i.grid&&i.params.grid&&i.params.grid.rows>1;let n=e;if(i.params.loop)if(i.virtual&&i.params.virtual.enabled)n+=i.virtual.slidesBefore;else{let e;if(r){const t=n*i.params.grid.rows;e=i.slides.find((e=>1*e.getAttribute("data-swiper-slide-index")===t)).column}else e=i.getSlideIndexByData(n);const t=r?Math.ceil(i.slides.length/i.params.grid.rows):i.slides.length,{centeredSlides:s}=i.params;let l=i.params.slidesPerView;"auto"===l?l=i.slidesPerViewDynamic():(l=Math.ceil(parseFloat(i.params.slidesPerView,10)),s&&l%2==0&&(l+=1));let o=t-e1){const e=[];return n.querySelectorAll(t.el).forEach((s=>{const a=p({},t,{el:s});e.push(new ie(a))})),e}const l=this;l.__swiper__=!0,l.support=I(),l.device=z({userAgent:t.userAgent}),l.browser=A(),l.eventsListeners={},l.eventsAnyListeners=[],l.modules=[...l.__modules__],t.modules&&Array.isArray(t.modules)&&l.modules.push(...t.modules);const o={};l.modules.forEach((e=>{e({params:t,swiper:l,extendParams:te(t,o),on:l.on.bind(l),once:l.once.bind(l),off:l.off.bind(l),emit:l.emit.bind(l)})}));const d=p({},ee,o);return l.params=p({},d,ae,t),l.originalParams=p({},l.params),l.passedParams=p({},t),l.params&&l.params.on&&Object.keys(l.params.on).forEach((e=>{l.on(e,l.params.on[e])})),l.params&&l.params.onAny&&l.onAny(l.params.onAny),Object.assign(l,{enabled:l.params.enabled,el:e,classNames:[],slides:[],slidesGrid:[],snapGrid:[],slidesSizesGrid:[],isHorizontal:()=>"horizontal"===l.params.direction,isVertical:()=>"vertical"===l.params.direction,activeIndex:0,realIndex:0,isBeginning:!0,isEnd:!1,translate:0,previousTranslate:0,progress:0,velocity:0,animating:!1,cssOverflowAdjustment(){return Math.trunc(this.translate/2**23)*2**23},allowSlideNext:l.params.allowSlideNext,allowSlidePrev:l.params.allowSlidePrev,touchEventsData:{isTouched:void 0,isMoved:void 0,allowTouchCallbacks:void 0,touchStartTime:void 0,isScrolling:void 0,currentTranslate:void 0,startTranslate:void 0,allowThresholdMove:void 0,focusableElements:l.params.focusableElements,lastClickTime:0,clickTimeout:void 0,velocities:[],allowMomentumBounce:void 0,startMoving:void 0,pointerId:null,touchId:null},allowClick:!0,allowTouchMove:l.params.allowTouchMove,touches:{startX:0,startY:0,currentX:0,currentY:0,diff:0},imagesToLoad:[],imagesLoaded:0}),l.emit("_swiper"),l.params.init&&l.init(),l}getDirectionLabel(e){return this.isHorizontal()?e:{width:"height","margin-top":"margin-left","margin-bottom ":"margin-right","margin-left":"margin-top","margin-right":"margin-bottom","padding-left":"padding-top","padding-right":"padding-bottom",marginRight:"marginBottom"}[e]}getSlideIndex(e){const{slidesEl:t,params:s}=this,a=y(f(t,`.${s.slideClass}, swiper-slide`)[0]);return y(e)-a}getSlideIndexByData(e){return this.getSlideIndex(this.slides.find((t=>1*t.getAttribute("data-swiper-slide-index")===e)))}recalcSlides(){const{slidesEl:e,params:t}=this;this.slides=f(e,`.${t.slideClass}, swiper-slide`)}enable(){const e=this;e.enabled||(e.enabled=!0,e.params.grabCursor&&e.setGrabCursor(),e.emit("enable"))}disable(){const e=this;e.enabled&&(e.enabled=!1,e.params.grabCursor&&e.unsetGrabCursor(),e.emit("disable"))}setProgress(e,t){const s=this;e=Math.min(Math.max(e,0),1);const a=s.minTranslate(),i=(s.maxTranslate()-a)*e+a;s.translateTo(i,void 0===t?0:t),s.updateActiveIndex(),s.updateSlidesClasses()}emitContainerClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=e.el.className.split(" ").filter((t=>0===t.indexOf("swiper")||0===t.indexOf(e.params.containerModifierClass)));e.emit("_containerClasses",t.join(" "))}getSlideClasses(e){const t=this;return t.destroyed?"":e.className.split(" ").filter((e=>0===e.indexOf("swiper-slide")||0===e.indexOf(t.params.slideClass))).join(" ")}emitSlidesClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=[];e.slides.forEach((s=>{const a=e.getSlideClasses(s);t.push({slideEl:s,classNames:a}),e.emit("_slideClass",s,a)})),e.emit("_slideClasses",t)}slidesPerViewDynamic(e,t){void 0===e&&(e="current"),void 0===t&&(t=!1);const{params:s,slides:a,slidesGrid:i,slidesSizesGrid:r,size:n,activeIndex:l}=this;let o=1;if("number"==typeof s.slidesPerView)return s.slidesPerView;if(s.centeredSlides){let e,t=a[l]?Math.ceil(a[l].swiperSlideSize):0;for(let s=l+1;sT){const t=I(e);s.slides.filter((e=>e.matches(`.${s.params.slideClass}[data-swiper-slide-index="${t}"], swiper-slide[data-swiper-slide-index="${t}"]`))).forEach((e=>{e.remove()}))}const z=o?-g.length:0,A=o?2*g.length:g.length;for(let t=z;t=S&&t<=T){const s=I(t);void 0===h||e?L.push(s):(t>h&&L.push(s),tC&&(u=C),m
- KNGIL
+ KNGIL
통합 회원관리
@@ -103,7 +179,7 @@ $isCompanyAdmin = in_array($auth, ['BS100100', 'BS100200', 'BS100300', 'BS100400
-
회사 관리자
@@ -112,30 +188,23 @@ $isCompanyAdmin = in_array($auth, ['BS100100', 'BS100200', 'BS100300', 'BS100400
-
-
+