first commit 2
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# JavaScript 파일의 MIME 타입을 명시적으로 설정하여, 브라우저의 엄격한 MIME 타입 검사 오류를 해결합니다.
|
||||
AddType text/javascript .js
|
||||
AddType text/javascript .mjs
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
소프트웨어 제품은 저작권법 및 국제저작권 협약을 비롯하여, 기타 지적재산권법 및 협약의 보호를 받습니다.
|
||||
|
||||
프로그램 명칭 : 그누보드5 ( GNU Board 5 )
|
||||
|
||||
저작자 : (주)에스아이알소프트 http://sir.kr
|
||||
|
||||
라이센스 (License)
|
||||
|
||||
번역문 아래에 원문이 있습니다.
|
||||
|
||||
주의 )
|
||||
1. 번역문과 원문의 내용상 차이가 있는 경우 원문의 내용을 우선으로 따릅니다.
|
||||
2. 법적인 분쟁이 발생한 경우 저작자의 회사 소재지를 관할하는 관할법원에서 분쟁을 해결합니다.
|
||||
3. 이 라이센스 파일 및 내용은 저작자를 제외한 어느 누구도 추가, 수정, 삭제할 수 없습니다.
|
||||
|
||||
----- LGPL 번역문 --------------------------------------------------------
|
||||
|
||||
GNU 약소 일반 공중 사용 허가서
|
||||
|
||||
2.1판, 1999년 2월
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
|
||||
누구든지 본 사용 허가서를 있는 그대로 복제하고 배포할 수 있습니다.
|
||||
그러나 본문에 대한 수정은 허용되지 않습니다.
|
||||
|
||||
[이 문서는 GNU 약소 일반 공중 사용 허가서의 이름으로 공표된 최초의 판본입니다.
|
||||
본 사용 허가서는 GNU 라이브러리 일반 공중 사용 허가서 2판의 후속판으로
|
||||
간주되기 때문에 2.1의 판번호를 갖고 있습니다.]
|
||||
전 문
|
||||
|
||||
소프트웨어에 적용되는 대부분의 사용 허가서들은 소프트웨어에 대한 수정과 공유의 자유를 제한하려는 것을 그 목적으로 합니다. 그러나 GNU 일반 공중 사용 허가서들은 자유 소프트웨어에 대한 수정과 공유의 자유를 모든 사용자들에게 보장하기 위해서 성립된 것입니다.
|
||||
본 사용 허가서인 GNU 약소 일반 공중 사용 허가서(이하, ``LGPL''이라고 칭합니다.)는 자유 소프트웨어 재단과 그밖의 저작자들이 이를 채택하기로 결정한, 주로 라이브러리와 같은 일부 특정한 소프트웨어 꾸러미에 적용됩니다. 누구든지 자신의 프로그램에 LGPL을 적용할 수 있지만, 어떤 상황에서 어떤 사용 허가서를 선택하는 것이 보다 나은 전략인지에 대해서 다음의 설명을 기준으로 먼저 신중히 고려해 보시기 바랍니다.
|
||||
자유 소프트웨어라는 말에서 사용된 ``자유''라는 단어는 무료를 의미하는 금전적인 측면의 자유가 아니라 구속되지 않는다는 관점에서의 자유를 의미하며, GNU 일반 공중 사용 허가서들은 자유 소프트웨어를 이용한 복제와 개작 및 배포와 수익 사업 등의 가능한 모든 형태의 자유를 실질적으로 보장하고 있습니다. 여기에는 원시 코드의 일부 또는 전부를 원용해서 보다 개선된 프로그램을 만들거나 새로운 프로그램을 창작할 수 있는 자유가 보장되어 있으며, 자신에게 양도된 이러한 자유와 권리를 보다 명확하게 인식할 수 있도록 하기 위한 규정도 포함되어 있습니다.
|
||||
소프트웨어를 양도받은 사람의 권리를 보호하기 위해서 우리는 배포자들이 이러한 권리를 부정하거나 포기하도록 피양도자에게 요구하는 행위를 금지시킬 필요가 있습니다. 이러한 금지 사항은 라이브러리를 개작하거나 배포하는 모든 사람들이 예외없이 지켜야 할 의무와 같습니다.
|
||||
예를 들어, LGPL 라이브러리를 배포할 경우에는 이를 유료로 판매하거나 무료로 배포하는 것에 관계없이 자신이 해당 라이브러리에 대해서 가질 수 있었던 모든 권리를 피양도자에게 그대로 양도해 주어야 하고, 라이브러리의 원시 코드를 함께 제공하거나 원시 코드를 구할 수 있는 방법을 확실히 알려주어야 합니다. 또한 라이브러리에 다른 코드를 링크시켰다면, 라이브러리를 수정한 뒤에도 정상적으로 컴파일을 진행할 수 있도록 링크 되었던 코드에 해당하는 완전한 목적 파일 전체를 함께 제공해야 합니다. 또한 피양도자에게 이러한 모든 사항들을 분명히 알 수 있도록 해 주어야 합니다.
|
||||
자유 소프트웨어 재단은 다음과 같은 두 가지 단계를 통해서 사용자들의 권리를 보호합니다. (1) 라이브러리에 저작권을 설정합니다. (2) 저작권의 양도에 관한 실정법에 의해서 유효한 법률적 효력을 갖는 LGPL을 통해서 소프트웨어를 복제하거나 개작 및 배포할 수 있는 권리를 사용자에게 부여합니다.
|
||||
모든 배포자들을 보호하기 위해서 우리는 자유 라이브러리에 대한 어떠한 보증도 제공하지 않는다는 점을 명확히 밝혀둡니다. 라이브러리를 사용하는 사람들은 반복적인 재배포 과정을 통해 라이브러리 자체에 수정과 변형이 일어날 수도 있으며, 이는 최초의 저작자가 만든 라이브러리가 갖고 있는 문제가 아닐 수 있다는 개연성을 인식하고 있어야 합니다. 우리는 개작과 재배포 과정에서 다른 사람에 의해 발생된 문제로 인해 라이브러리의 원저작자의 신망이 실추되는 것을 원하지 않습니다.
|
||||
특허 제도는 자유 소프트웨어의 존재를 위협하는 요소일 수밖에 없습니다. 우리는 특허권자로부터 기업이 제한적인 사용 허가를 얻은 뒤에 이를 통해 자유 프로그램의 사용자들을 규제할 수 없게 되기를 희망합니다. 따라서 우리는 특정한 버전의 라이브러리에 대한 어떠한 특허 사용 허가의 취득도 LGPL에 규정된 자유를 완전히 만족시키는 범위 내에서 이루어 져야 할 것을 요구합니다.
|
||||
몇몇 라이브러리를 포함한 대부분의 GNU 소프트웨어에는 GPL이 적용됩니다. 본 사용 허가서인 LGPL은 특정한 라이브러리에만 적용되며 GPL과는 상당히 다른 면을 갖고 있습니다. LGPL은 특정한 라이브러리가 자유 소프트웨어가 아닌 프로그램과 함께 링크되는 것을 허용하려는 목적으로 사용됩니다.
|
||||
어떤 프로그램이 라이브러리와 함께 링크된다면, 라이브러리가 정적으로 링크되든지 공유 라이브러리로 사용되든지 간에 이 두개의 조합은 법적으로 말할 때 결합 저작물, 즉 최초의 라이브러리로부터 파생된 2차적 저작물로 간주됩니다. GPL은 이러한 형태의 링크가 일어날 경우에 결합된 전체 저작물이 GPL을 만족할 때에 한해서만 링크를 허용합니다. 그러나 LGPL은 보다 유연한 링크 조건을 허용하고 있습니다.
|
||||
우리가 본 사용 허가서를 ``약소'' 일반 공중 사용 허가서라고 부르는 이유는 사용자들의 자유를 보호하는 강도를 GPL보다 경감시켰기 때문입니다. 또한 LGPL은 자유 소프트웨어가 아닌 프로그램과 경쟁하는데 있어서 자유 소프트웨어 개발자들에게 GPL보다 이점을 덜 제공합니다. 우리가 많은 종류의 라이브러리에 LGPL이 아닌 일반적인 GPL을 사용하는 것은 이러한 이유 때문입니다. 그러나 특수한 상황에서는 오히려 LGPL을 사용하는 것이 유리할 수 있습니다.
|
||||
극히 드문 예이긴 하지만, 어떤 라이브러리의 사용 폭을 가능한 넓게 유도해서 그것을 사실상의 표준으로 만들어야 할 특별한 필요가 있다고 생각해 봅시다. 이것을 가능하게 만들기 위해서는 자유 소프트웨어가 아닌 프로그램도 이러한 라이브러리를 사용할 수 있도록 허용해야 합니다. 이보다 흔한 또 한가지 예로 자유 소프트웨어가 아닌 라이브러리가 폭넓게 사용되고 있을 때 이와 동일한 기능을 제공하는 자유 라이브러리가 만들어진 경우를 생각해 볼 수 있습니다. 이러한 상황에서는 자유 라이브러리의 사용을 자유 소프트웨어에만 한정함으로써 얻을 수 있는 이익이 거의 없습니다. 이런 경우에 우리는 LGPL을 사용합니다.
|
||||
또 하나의 예는 자유 소프트웨어가 아닌 프로그램에 특정한 라이브러리의 사용을 허용함으로써 보다 많은 사람들이 자유 소프트웨어를 사용할 수 있게 만드는 경우입니다. 예를 들면, 자유 소프트웨어가 아닌 프로그램들이 GNU C 라이브러리를 사용할 수 있도록 허용해서 사람들이 GNU 운영체제와 GNU/리눅스 운영체제를 사용하도록 유도할 수 있습니다.
|
||||
LGPL이 사용자의 자유를 보다 약소적으로 보호하고 있음에도 불구하고, LGPL로 설정된 라이브러리와 링크된 프로그램을 사용하는 사용자는 라이브러리가 개작되더라도 개작된 버전을 사용해서 프로그램을 실행할 수 있는 확실한 자유와 이에 필요한 수단을 갖고 있습니다.
|
||||
복제와 개작 및 배포에 관련된 구체적인 조건과 규정은 다음과 같습니다. ``라이브러리에 기반한 저작물''과 ``라이브러리를 사용하는 저작물''의 차이에 특별한 주의를 기울이기 바랍니다. 전자는 라이브러리로부터 파생된 코드를 담고 있는 저작물을 의미하는데 반해서 후자는 실행되기 위해서 라이브러리와 결합되어야 하는 저작물을 말합니다.
|
||||
복제와 개작 및 배포에 관한 조건과 규정
|
||||
|
||||
제 0 조. 본 사용 허가 계약은 GNU 약소 일반 공중 사용 허가서(이하, ``LGPL''이라고 칭합니다.)의 규정에 따라 배포될 수 있다는 사항이 저작권자 또는 그에 준하는 정당한 권리을 갖고 있는 자에 의해서 명시된 모든 종류의 소프트웨어 라이브러리와 컴퓨터 프로그램 저작물(이하, ``프로그램''이라고 칭합니다.)에 대해서 동일하게 적용됩니다. ``피양도자''란 LGPL의 규정에 따라 프로그램을 양도받은 사람을 의미합니다.
|
||||
``라이브러리''란 소프트웨어 함수와 데이터를 함께 또는 개별적으로 수집해 놓은 것으로 이들 중 일부를 사용하는 응용 프로그램과 링크되어 실행물을 생성하는데 편리하도록 미리 준비된 것을 의미합니다.
|
||||
이하로 언급되는 ``라이브러리''는 본 사용 허가서에 의해서 배포되고 있는 모든 소프트웨어 라이브러리와 저작물을 의미합니다. ``라이브러리에 기반한 저작물''은 라이브러리 또는 저작권법에 따른 라이브러리의 2차적 저작물을 모두 의미합니다. 다시 말하면, 전술한 라이브러리 자신 또는 저작권법의 규정에 따라 라이브러리의 전부 또는 상당 부분을 원용하거나 다른 언어로의 번역을 포함할 수 있는 개작 과정을 통해서 창작된 새로운 라이브러리와 이와 관련된 저작물입니다. (이후로 다른 언어로의 번역은 별다른 제한없이 개작의 범위에 포함되는 것으로 간주합니다.)
|
||||
저작물에 대한 ``원시 코드''란 해당 저작물을 개작하기에 적절한 형식을 의미합니다. 라이브러리에 대한 완전한 원시 코드란 라이브러리에 포함된 모든 모듈들의 원시 코드와 이와 관련된 인터페이스 정의 파일 모두, 그리고 라이브러리의 컴파일과 설치를 제어하는데 사용된 스크립트 전부를 의미합니다.
|
||||
본 허가서는 복제와 개작 및 배포 행위에 대해서만 적용됩니다. 따라서 라이브러리를 사용하는 프로그램을 실행시키는 행위에 대한 제한은 없습니다. 이러한 프로그램의 결과물에는, 결과물을 생성하기 위한 도구로 라이브러리가 사용되었는지 아닌지의 여부에 관계없이 결과물이 라이브러리에 기반한 2차적 저작물을 구성했을 때에 한해서 본 허가서의 규정들이 적용됩니다. 2차적 저작물의 구성 여부는 2차적 저작물 안에서의 라이브러리의 역할과 라이브러리를 사용한 프로그램의 역할을 토대로 판단합니다.
|
||||
제 1 조. 적절한 저작권 표시와 라이브러리에 대한 보증이 제공되지 않는다는 사실을 각각의 복제물에 명시하는 한, 피양도자는 라이브러리의 원시 코드를 자신이 양도받은 상태 그대로 어떠한 매체를 통해서도 복제하고 배포할 수 있습니다. 복제와 배포가 이루어 질 때는 본 허가서와 라이브러리에 대한 보증이 제공되지 않는다는 사실에 대해서 언급되었던 모든 내용들을 그대로 유지시켜야 하며, 영문판 LGPL을 함께 제공해야 합니다.
|
||||
배포자는 복제물을 물리적으로 인도하는데 소요된 비용을 청구할 수 있으며, 선택 사항으로 독자적인 유료 보증을 설정할 수 있습니다.
|
||||
제 2 조. 피양도자는 자신이 양도받은 라이브러리의 전부나 일부를 개작할 수 있으며, 이를 통해 라이브러리에 기반한 2차적 저작물을 창작할 수 있습니다. 개작된 라이브러리나 창작된 2차적 저작물은 다음의 사항들을 모두 만족시키는 조건에 한해서, 제1조의 규정에 따라 또다시 복제되고 배포될 수 있습니다.
|
||||
제 1 항. 개작된 저작물은 반드시 소프트웨어 라이브러리여야 합니다.
|
||||
제 2 항. 파일을 개작할 때는 파일을 개작한 사실과 그 날짜를 파일 안에 명시해야 합니다.
|
||||
제 3 항. 저작물 전체에 대한 사용 권리를 본 허가서의 규정에 따라 공중에게 무상으로 허용해야 합니다.
|
||||
제 4 항. 개작된 라이브러리에 포함된 기능이 그 기능을 사용하는 응용 프로그램으로부터 제공되는 함수나 데이터 테이블을 참조하는 경우에는, 이러한 기능이 호출되었을 때 매개 인수를 전달하는 경우를 제외하고는 응용 프로그램이 그러한 함수나 테이블을 제공하지 않는 경우에도 기능이 독립적으로 수행되고 목적하는 모든 부분이 확실하게 유효할 수 있도록 최대의 노력을 기울여야만 합니다
|
||||
|
||||
(예를 들면, 라이브러리에 포함된 제곱근 연산 함수는 응용 프로그램으로부터 명확하고 완전하게 독립적인 형태로 만들어져야 합니다. 따라서 제2조 4항은 제곱근 연산 함수가 어떠한 응용 프로그램이 제공하는 함수나 테이블도 필수적으로 사용되는 않는 형태로 만들어져야 한다는 것을 규정합니다. 즉, 응용 프로그램이 제공하는 기능 없이도 제곱근 연산 함수가 제곱근을 구할 수 있어야 합니다.)
|
||||
위의 조항들은 개작된 라이브러리 전체에 적용됩니다. 만약, 개작된 라이브러리에 포함된 특정 부분이 라이브러리부터 파생된 것이 아닌 별도의 독립 저작물로 인정될 만한 상당한 이유가 있을 경우에는 해당 저작물의 개별적인 배포에는 본 허가서의 규정들이 적용되지 않습니다. 그러나 이러한 저작물이 라이브러리에 기반한 2차적 저작물의 일부로서 함께 배포된다면 개별적인 저작권과 배포 기준에 상관없이 저작물 모두가 본 허가서에 의해서 관리되어야 하며, 전체 저작물에 대한 사용 권리는 공중에게 무상으로 양도됩니다.
|
||||
이러한 규정은 개별적인 저작물에 대한 저작자들의 권리를 침해하거나 인정하지 않으려는 것이 아니라, 라이브러리로부터 파생된 2차적 저작물이나 수집 저작물의 배포를 일관적으로 규제할 수 있는 권리를 행사하기 위한 것입니다.
|
||||
라이브러리나 라이브러리로부터 파생된 2차적 저작물을 이들로부터 파생되지 않은 다른 저작물과 함께 단순히 저장하거나 배포하기 위한 목적으로 동일한 매체에 모아 놓은 집합물의 경우에는, 라이브러리로부터 파생되지 않은 다른 저작물에는 본 허가서의 규정들이 적용되지 않습니다.
|
||||
제 3 조. 피양도자는 자신이 양도받은 라이브러리의 복제물에 LGPL 대신 GPL의 규정을 적용할 수 있습니다. 이것이 가능하기 위해서는 LGPL에 대해서 언급되었던 모든 사항을 GPL 2판으로 대체시켜야 합니다. (GPL 2판보다 신판이 공표되었을 경우에는 원한다면 신판의 판번호를 사용할 수 있습니다.) 그 이외에 다른 사항들은 변경할 수 없습니다.
|
||||
|
||||
복제물에 대해서 이러한 수정이 이루어 졌을 경우에는 GPL로 변경된 사용권 허가를 다시 변경할 수 없으며, 이에 따라서 해당 복제물을 기반으로 만들어진 모든 저작물과 복제물에는 GPL이 적용되어야만 합니다.
|
||||
|
||||
이러한 선택 사항은 라이브러리의 코드 일부분을 라이브러리가 아닌 일반 프로그램에 포함시키고자 할 경우에 유용합니다.
|
||||
제 4 조. 피양도자는 제1조와 제2조의 규정에 따라 라이브러리(또는 제2조에 의한 라이브러리의 일부나 라이브러리에 기반한 2차적 저작물)를 목적 코드나 실행물의 형태로 복제하고 배포할 수 있습니다. 이 때 목적 코드나 실행물에 상응하는 컴퓨터가 인식할 수 있는 완전한 원시 코드를 제1조와 제2조의 규정에 따라 소프트웨어의 교환을 위해서 일반적으로 사용되는 매체를 통해 함께 제공해야 합니다.
|
||||
|
||||
목적 코드를 지정한 장소로부터 복제해 갈 수 있게 하는 방식으로 배포할 경우, 동일한 장소로부터 원시 코드를 복제할 수 있는 동등한 접근 방법을 제공한다면 이는 원시 코드가 목적 코드와 함께 복제되도록 설정되지 않았다 하더라도 원시 코드를 배포하는 것으로 간주됩니다.
|
||||
제 5 조. 라이브러리의 어떠한 부분으로부터의 파생물도 포함하지 않지만, 컴파일 또는 링크를 통해서 라이브러리와 함께 작동하도록 설계된 프로그램은 ``라이브러리를 사용하는 저작물''이 됩니다. 이러한 저작물이 별도로 분리되어 있을 때는 라이브러리에 대한 파생물이 아니므로 본 사용 허가서가 적용되지 않습니다.
|
||||
그러나 ``라이브러리를 사용하는 저작물''이 라이브러리와 링크된 결과로 생성된 실행물은, 실행물 안에 라이브러리의 일부를 포함하고 있기 때문에 라이브러리에 기반한 2차적 저작물을 구성하게 됩니다. 따라서 이러한 방식으로 생성된 실행물은 본 사용 허가서의 적용을 받습니다. 제6조는 이러한 종류의 실행물의 배포를 위한 규정을 담고 있습니다.
|
||||
``라이브러리를 사용하는 저작물''이 라이브러리의 일부인 헤더 파일의 자료를 사용한 경우에는 그러한 저작물의 원시 코드가 라이브러리에 기반한 2차적 저작물이 아니었다 하더라도 목적 코드는 라이브러리에 기반한 2차적 저작물이 될 수 있습니다. 이러한 구분이 성립될 수 있는지의 여부는 그러한 저작물 자체가 라이브러리이거나 저작물에 사용된 라이브러리 없이도 링크될 수 있는 경우에 있어서 매우 중요한 차이를 갖습니다. 그러나 이러한 구분이 성립될 수 있는 명확한 판단 기준은 법률적으로 정의되어 있지 않습니다.
|
||||
만약 이와같은 형태의 목적 파일이 단지 숫자 매개 변수와 자료 구조의 설계 형태 및 이에 대한 접근 도구 그리고 10행 미만으로 이루어진 작은 인라인 함수와 매크로만을 사용하는 것이라면 법적 기준에 의한 2차적 저작물의 성립 여부에 관계없이 그 사용이 제한되지 않습니다. (그러나 이러한 목적 코드와 라이브러리의 일부가 함께 포함된 실행물은 여전히 제6조의 적용을 받습니다.)
|
||||
저작물이 라이브러리에 기반한 2차적 저작물이라면 해당 저작물에 대한 목적 코드는 제6조에 따라 배포될 수 있습니다. 또한 그러한 저작물을 포함한 실행물들은 기반이 된 라이브러리에 직접 링크되는지 아닌지의 여부에 관계없이 모두 제6조의 적용을 받습니다.
|
||||
제 6 조. 위의 조항들에 대한 예외의 하나로, 라이브러리와 ``라이브러리를 사용하는 저작물''을 함께 결합하거나 링크시켜서 라이브러리의 일부분이 포함된 저작물을 만들었다면, 이를 자신이 선택한 규정에 따라 배포할 수 있습니다. 이 경우 배포 규정에는 피양도자들이 자신의 필요에 따라 저작물을 개작할 수 있으며 개작에 따른 디버깅을 위해 코드역분석(reverse regineering)을 허용한다는 사항이 포함되어야 합니다.
|
||||
라이브러리와 라이브러리의 사용에는 본 사용 허가서가 적용된다는 것과 저작물 안에 이러한 라이브러리가 사용되고 있다는 사실을 담고 있는 안내 문구를 모든 복제물에 분명하게 명시해야 합니다. 또한 영문판 LGLP 사본을 함께 제공해야 합니다. 저작물이 실행될 때 저작권 사항이 표시되는 형태를 취하고 있다면 라이브러리에 대한 저작권 사항도 함께 포함시켜야 하며 LGPL 사본을 참고할 수 있는 방법을 명시해야 합니다. 또한 다음 중 하나의 사항을 반드시 만족시켜야 합니다.
|
||||
제 1 항. 저작물에 포함된 라이브러리에 어떠한 수정이 가해졌다 하더라도 해당 라이브러리에 대한 컴퓨터가 인식할 수 있는 완전한 형태의 원시 코드를 저작물과 함께 제공해야 합니다. 이 원시 코드는 제1조와 제2조의 규정에 따라 배포될 수 있어야 합니다. 만약 저작물이 라이브러리와 링크되는 실행물이었을 경우에는 피양도자가 실행물과 링크되는 라이브러리를 개작한 뒤에도 링크를 통해 새로운 실행물을 만들 수 있도록 하기 위해서 ``라이브러리를 사용한 저작물''로서 배포된 저작물에 해당하는 컴퓨터가 인식할 수 있는 완전한 형태의 원시 코드와 목적 코드 중 하나 또는 둘 모두를 제공해야 합니다. (라이브러리에 포함된 정의 파일의 내용을 수정한 경우에는 변경된 정의 부분을 사용하기 위해서 응용 프로그램을 반드시 다시 컴파일할 필요는 없다는 점은 인정됩니다.)
|
||||
제 2 항. 라이브러리는 적절한 공유 라이브러리 방식을 사용해서 링크되어야 합니다. 적절한 방식이란, (1) 라이브러리의 함수를 실행물 속으로 직접 복제하는 것이 아니라 실행 시점에서 볼 때 이미 사용자의 컴퓨터 시스템 상에 존재하고 있는 라이브러리의 복제물이 사용되는 것입니다. 또한 (2) 사용자가 개작된 라이브러리를 설치한 경우에도 개작된 라이브러리가 저작물을 만들 때 사용된 라이브러리의 버전과 인터페이스상으로 호환되는 한, 적절하게 동작할 수 있어야 합니다.
|
||||
제 3 항. 배포에 필요한 최소한의 비용만을 받고 피양도자에게 제6조 1항에 규정된 자료를 배포하겠다는, 최소한 3년간 유효한 약정서를 저작물과 함께 제공해야 합니다.
|
||||
제 4 항. 저작물을 지정한 장소로부터 복제해 갈 수 있게 하는 방식으로 배포하는 경우, 동일한 장소로부터 제6조 1항에 규정된 자료를 복제할 수 있는 동등한 접근 방법을 제공하는 것은 저작물에 대한 배포 조건을 충족하는 것으로 간주됩니다.
|
||||
제 5 항. 피양도자가 제6조 1항에 규정된 자료의 복제물을 이미 수령했는지를 확인하거나 자신이 피양도자에게 그러한 자료를 이미 송부했는지를 확인해야 합니다.
|
||||
실행물이 ``라이브러리를 사용하는 저작물''의 형태로 배포된다면 여기에는 실행물을 재생산하기 위해서 필요한 유틸리티 프로그램과 데이터들이 모두 포함되어야 합니다. 그러나 특별한 예외의 하나로서, 실행물이 실행될 운영체제의 주요 부분(컴파일러나 커널 등)과 함께 (원시 코드나 바이너리의 형태로) 일반적으로 배포되는 구성 요소들은 이러한 구성 요소 자체가 실행물에 수반되지 않는한 배포 대상에서 제외되어도 무방합니다.
|
||||
이러한 규정이 일반적으로 운영체제에 함께 수반되지 않는 독점 라이브러리들의 사용 허가서와 충돌하게 될 경우에는 배포하고자 하는 실행물 안에 본 사용 허가서가 적용되는 라이브러리와 독점 라이브러리를 함께 사용할 수 없습니다.
|
||||
제 7 조. 라이브러리에 기반한 저작물로서의 라이브러리의 일부를 본 사용 허가서가 적용되지 않는 다른 라이브러리의 일부와 하나의 라이브러리 안에 병존시킬 수 있습니다. 이러한 결합 라이브러리를 배포할 경우에는 라이브러리에 기반한 저작물과 그렇지 않은 라이브러리가 별도로 배포될 수 있음을 명시해야 하며 다음의 두가지 사항을 준수해야 합니다.
|
||||
제 1 항. 결합 라이브러리를 구성하고 있는 ``라이브러리에 기반한 저작물''의 복제물을 결합되지 않은 독립된 상태로 함께 제공해야 합니다. 이 복제물의 배포에는 위의 조항들이 적용됩니다.
|
||||
제 2 항. 라이브러리에 기반한 저작물의 일부가 결합 라이브러리 안에 포함하고 있다는 사실을 명시해야 하며, 제7조 1항에 의해서 제공된 결합되지 않은 상태의 ``라이브러리에 기반한 저작물''의 위치 정보를 명기해야 합니다.
|
||||
제 8 조. 본 허가서에 의해서 명시적으로 이루어 지지 않는 한 라이브러리에 대한 복제와 개작, 하위 허가권 설정과 링크 및 배포가 이루어 질 수 없습니다. 이와 관련된 어떠한 행위도 무효이며 본 허가서가 보장한 권리는 자동으로 소멸됩니다. 그러나 본 허가서의 규정에 따라 라이브러리의 복제물이나 권리를 양도받았던 제3자는 본 허가서의 규정들을 준수하는 한, 배포자의 권리 소멸에 관계없이 사용상의 권리를 계속해서 유지할 수 있습니다.
|
||||
제 9 조. 본 허가서는 서명이나 날인이 수반되는 형식을 갖고 있지 않기 때문에 피양도자가 본 허가서의 내용을 반드시 받아들여야 할 필요는 없습니다. 그러나 라이브러리나 라이브러리에 기반한 2차적 저작물에 대한 개작 및 배포를 허용하는 것은 본 허가서에 의해서만 가능합니다. 만약 본 허가서에 동의하지 않을 경우에는 이러한 행위들이 법률적으로 금지됩니다. 따라서 라이브러리(또는 라이브러리에 기반한 2차적 저작물)을 개작하거나 배포하는 행위는 이에 따른 본 허가서의 내용에 동의한다는 것을 의미하며, 복제와 개작 및 배포에 관한 본 허가서의 조건과 규정들을 모두 받아들이겠다는 의미로 간주됩니다.
|
||||
제 10 조. 피양도자에 의해서 라이브러리(또는 라이브러리에 기반한 2차적 저작물)이 반복적으로 재배포될 경우, 각 단계에서의 피양도자는 본 허가서의 규정에 따른 라이브러리의 복제와 개작, 링크, 배포에 대한 권리를 최초의 양도자로부터 양도받은 것으로 자동적으로 간주됩니다. 라이브러리(또는 라이브러리에 기반한 2차적 저작물)을 배포할 때는 피양도자의 권리의 행사를 제한할 수 있는 어떠한 사항도 추가할 수 없습니다. 그러나 피양도자에게 재배포가 일어날 시점에서의 제3의 피양도자에게 본 허가서를 준수하도록 강제할 책임은 부과되지 않습니다.
|
||||
제 11 조. 법원의 판결이나 특허권 침해에 대한 주장 또는 특허 문제에 국한되지 않은 그밖의 이유들로 인해서 본 허가서의 규정에 배치되는 사항이 발생한다 하더라도 그러한 사항이 선행하거나 본 허가서의 조건과 규정들이 면제되는 것은 아닙니다. 따라서 법원의 명령이나 합의 등에 의해서 본 허가서에 위배되는 사항들이 발생한 상황이라도 양측 모두를 만족시킬 수 없다면 라이브러리는 배포될 수 없습니다. 예를 들면, 특정한 특허 관련 허가가 라이브러리의 복제물을 직접 또는 간접적인 방법으로 양도받은 임의의 제3자에게 해당 라이브러리를 무상으로 재배포할 수 있게 허용하지 않는다면, 그러한 허가와 본 사용 허가를 동시에 만족시키면서 라이브러리를 배포할 수 있는 방법은 없습니다.
|
||||
본 조항은 특정한 상황에서 본 조항의 일부가 유효하지 않거나 적용될 수 없을 경우에도 본 조항의 나머지 부분들을 적용하기 위한 의도로 만들어 졌습니다. 따라서 그 이외의 상황에서는 본 조항을 전체적으로 적용하면 됩니다.
|
||||
본 조항의 목적은 특허나 저작권 침해 등의 행위를 조장하거나 해당 권리를 인정하지 않으려는 것이 아니라, 공중 사용 허가서들을 통해서 구현되어 있는 자유 소프트웨어의 배포 체계를 통합적으로 보호하기 위한 것입니다. 많은 사람들이 배포 체계에 대한 신뢰있는 지원을 계속해 줌으로써 소프트웨어의 다양한 분야에 많은 공헌을 해 주었습니다. 소프트웨어를 어떠한 배포 체계를 통해 배포할 것인가를 결정하는 것은 전적으로 저작자와 기증자들의 의지에 달려있는 것이지, 일반 사용자들이 강요할 수 있는 문제는 아닙니다.
|
||||
본 조항은 본 허가서의 다른 조항들에서 무엇이 중요하게 고려되어야 하는 지를 명확하게 설명하기 위한 목적으로 만들어진 것입니다
|
||||
제 12 조. 특허나 저작권이 설정된 인터페이스로 인해서 특정 국가에서 라이브러리의 배포와 사용이 함께 또는 개별적으로 제한되어 있는 경우, 본 사용 허가서를 라이브러리에 적용한 최초의 저작권자는 문제가 발생하지 않는 국가에 한해서 라이브러리를 배포한다는 배포상의 지역적 제한 조건을 명시적으로 설정할 수 있으며, 이러한 사항은 본 허가서의 일부로 간주됩니다.
|
||||
제 13 조. 자유 소프트웨어 재단은 때때로 본 사용 허가서의 개정판이나 신판을 공표할 수 있습니다. 새롭게 공표될 판은 당면한 문제나 현안을 처리하기 위해서 세부적인 내용에 차이가 발생할 수 있지만, 그 근본 정신에는 변함이 없을 것입니다.
|
||||
각각의 판들은 판번호를 사용해서 구별됩니다. 특정한 판번호와 그 이후 판을 따른다는 사항이 명시된 라이브러리에는 해당 판이나 그 이후에 발행된 어떠한 판을 선택해서 적용해도 무방하고, 판번호를 명시하고 있지 않은 경우에는 자유 소프트웨어 재단이 공표한 어떠한 판번호의 판을 적용해도 무방합니다.
|
||||
제 14 조. 라이브러리의 일부를 본 허가서와 배포 기준이 다른 자유 프로그램과 함께 결합하고자 할 경우에는 해당 프로그램의 저작자로부터 서면 승인을 받아야 합니다. 자유 소프트웨어 재단이 저작권을 갖고 있는 소프트웨어의 경우에는 자유 소프트웨어 재단의 승인을 얻어야 합니다. 우리는 이러한 요청을 수락하기 위해서 때때로 예외 기준을 만들기도 합니다. 자유 소프트웨어 재단은 일반적으로 자유 소프트웨어의 2차적 저작물들을 모두 자유로운 상태로 유지시키려는 목적과 소프트웨어의 공유와 재활용을 증진시키려는 두가지 목적을 기준으로 승인 여부를 결정할 것입니다.
|
||||
보증의 결여 (제15조, 제16조)
|
||||
|
||||
제 15 조. 본 허가서를 따르는 라이브러리는 무상으로 양도되기 때문에 관련 법률이 허용하는 한도 내에서 어떠한 형태의 보증도 제공되지 않습니다. 라이브러리의 저작권자와 배포자가 공동 또는 개별적으로 별도의 보증을 서면으로 제공할 때를 제외하면, 특정한 목적에 대한 라이브러리의 적합성이나 상업성 여부에 대한 보증을 포함한 어떠한 형태의 보증도 명시적이나 묵시적으로 설정되지 않은 ``있는 그대로의'' 상태로 이 라이브러리를 배포합니다. 라이브러리와 라이브러리의 실행에 따라 발생할 수 있는 모든 위험은 피양도자에게 인수되며 이에 따른 보수 및 복구를 위한 제반 경비 또한 피양도자가 모두 부담해야 합니다.
|
||||
제 16 조. 저작권자나 배포자가 라이브러리의 손상 가능성을 사전에 알고 있었다 하더라도 발생된 손실이 관련 법규에 의해 보호되고 있거나 이에 대한 별도의 서면 보증이 설정된 경우가 아니라면, 저작권자나 라이브러리를 원래의 상태 또는 개작한 상태로 제공한 배포자는 라이브러리의 사용이나 비작동으로 인해 발생된 손실이나 라이브러리 자체의 손실에 대해 책임지지 않습니다. 이러한 면책 조건은 사용자나 제3자가 라이브러리를 조작함으로써 발생된 손실이나 다른 소프트웨어와 라이브러리를 함께 동작시키는 것으로 인해서 발생된 데이터의 상실 및 부정확한 산출 결과에만 국한되는 것이 아닙니다. 발생된 손실의 일반성이나 특수성 뿐 아니라 원인의 우발성 및 필연성도 전혀 고려되지 않습니다.
|
||||
복제와 개작 및 배포에 관한 조건과 규정의 끝.
|
||||
새로운 라이브러리에 LGPL을 적용하는 방법
|
||||
|
||||
새로운 라이브러리를 개발하고 그 라이브러리가 보다 많은 사람들에게 최대한 유용하게 사용되기를 원한다면, 본 허가서나 GNU 일반 공중 사용 허가서를 선택적으로 적용해서 누구나 자유롭게 개작하고 재배포할 수 있는 자유 소프트웨어로 만드는 것이 최선의 방법입니다.
|
||||
라이브러리를 자유 소프트웨어로 만들기 위해서는 다음과 같은 사항을 라이브러리에 추가하면 됩니다. 라이브러리에 대한 보증이 제공되지 않는다는 사실을 가장 효과적으로 전달할 수 있는 방법은 원시 코드 파일의 시작 부분에 이러한 사항을 추가하는 것입니다. 각각의 파일에는 최소한 저작권을 명시한 행과 본 사용 허가서의 전체 내용을 참고할 수 있는 위치 정보를 명시해야 합니다.
|
||||
라이브러리의 이름과 용도를 한 줄 정도로 설명합니다.
|
||||
Copyright (C) 20yy년 <프로그램 저작자의 이름>
|
||||
이 라이브러리는 자유 소프트웨어입니다. 소프트웨어의 피양도자는 자유 소프트웨어 재단이 공표한 GNU 약소 일반 공중 사용 허가서 2.1판 또는 그 이후 판을 임의로 선택해서, 그 규정에 따라 라이브러리를 개작하거나 재배포할 수 있습니다.
|
||||
이 라이브러리는 유용하게 사용될 수 있으리라는 희망에서 배포되고 있지만, 특정한 목적에 맞는 적합성 여부나 판매용으로 사용할 수 있으리라는 묵시적인 보증을 포함한 어떠한 형태의 보증도 제공하지 않습니다. 보다 자세한 사항에 대해서는 GNU 약소 일반 공중 사용 허가서를 참고하시기 바랍니다.
|
||||
GNU 약소 일반 공중 사용 허가서는 이 라이브러리와 함께 제공됩니다. 만약, 이 문서가 누락되어 있다면 자유 소프트웨어 재단으로 문의하시기 바랍니다. (자유 소프트웨어 재단: Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA)
|
||||
또한, 사용자들이 라이브러리를 배포한 사람에게 전자 메일과 서면으로 연락할 수 있는 정보를 추가해야 합니다.
|
||||
만약, 라이브러리의 저작자가 학교나 기업과 같은 단체나 기관에 프로그래머로 고용되어 있다면 라이브러리의 자유로운 배포를 위해서 고용주나 해당 기관장으로부터 라이브러리에 대한 저작권 포기 각서를 받아야 합니다. 예를 들면 다음과 같은 형식이 될 수 있다. (아래의 문구를 실제로 사용할 경우에는 예로 사용된 이름들을 실제 이름으로 대체하면 됩니다.)
|
||||
본사는 제임스 해커가 만든 (설정 옵션을 조정하기 위한) `Frob' 라이브러리에 관련된 모든 저작권을 포기합니다.
|
||||
1990년 4월 1일
|
||||
Yoyodye, Inc., 부사장: Ty Coon
|
||||
서명: Ty Coon의 서명
|
||||
|
||||
|
||||
|
||||
----- LGPL 원문 --------------------------------------------------------
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 2.1, February 1999
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
|
||||
This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
|
||||
When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
|
||||
To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
|
||||
For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
|
||||
We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
|
||||
To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
|
||||
Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
|
||||
Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
|
||||
When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
|
||||
We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
|
||||
For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
|
||||
In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
|
||||
Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
|
||||
The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
|
||||
A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
|
||||
The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
|
||||
"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
|
||||
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
|
||||
1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
|
||||
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||
2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||
a) The modified work must itself be a software library.
|
||||
b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
|
||||
c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
|
||||
d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
|
||||
(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
|
||||
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
|
||||
In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
|
||||
Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
|
||||
This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
|
||||
4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
|
||||
If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||
5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
|
||||
However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
|
||||
When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
|
||||
If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
|
||||
Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
|
||||
6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
|
||||
You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
|
||||
a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
|
||||
b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
|
||||
c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
|
||||
d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
|
||||
e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
|
||||
For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||
It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
|
||||
7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
|
||||
a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
|
||||
b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
|
||||
8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||
9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
|
||||
10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
|
||||
11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
|
||||
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
|
||||
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||
12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||
13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||
Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
|
||||
14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||
NO WARRANTY
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
|
||||
To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
one line to give the library's name and an idea of what it does.
|
||||
Copyright (C) year name of author
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in
|
||||
the library `Frob' (a library for tweaking knobs) written
|
||||
by James Random Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
That's all there is to it!
|
||||
|
||||
----- 끝 --------------------------------------------------------
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
include_once('./common.php');
|
||||
|
||||
// 커뮤니티 사용여부
|
||||
if(defined('G5_COMMUNITY_USE') && G5_COMMUNITY_USE === false) {
|
||||
if (!defined('G5_USE_SHOP') || !G5_USE_SHOP)
|
||||
die('<p>쇼핑몰 설치 후 이용해 주십시오.</p>');
|
||||
|
||||
define('_SHOP_', true);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
||||
|
||||
include_once(G5_PATH.'/head.php');
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
||||
|
||||
include_once(G5_PATH.'/tail.php');
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
define('G5_IS_ADMIN', true);
|
||||
require_once '../common.php';
|
||||
require_once G5_ADMIN_PATH . '/admin.lib.php';
|
||||
|
||||
if (isset($token)) {
|
||||
$token = @htmlspecialchars(strip_tags($token), ENT_QUOTES);
|
||||
}
|
||||
|
||||
run_event('admin_common');
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_use_apache = (stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== false);
|
||||
|
||||
$is_use_nginx = (stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false);
|
||||
|
||||
$is_use_iis = !$is_use_apache && (stripos($_SERVER['SERVER_SOFTWARE'], 'microsoft-iis') !== false);
|
||||
|
||||
$is_write_file = false;
|
||||
$is_apache_need_rules = false;
|
||||
$is_apache_rewrite = false;
|
||||
|
||||
if (!($is_use_apache || $is_use_nginx || $is_use_iis)) { // 셋다 아니면 다 출력시킨다.
|
||||
$is_use_apache = true;
|
||||
$is_use_nginx = true;
|
||||
}
|
||||
|
||||
if ($is_use_nginx) {
|
||||
$is_write_file = false;
|
||||
}
|
||||
|
||||
if ($is_use_apache) {
|
||||
$is_write_file = (is_writable(G5_PATH) || (file_exists(G5_PATH . '/.htaccess') && is_writable(G5_PATH . '/.htaccess'))) ? true : false;
|
||||
$is_apache_need_rules = check_need_rewrite_rules();
|
||||
$is_apache_rewrite = function_exists('apache_get_modules') && in_array('mod_rewrite', apache_get_modules());
|
||||
}
|
||||
|
||||
$get_path_url = parse_url(G5_URL);
|
||||
|
||||
$base_path = isset($get_path_url['path']) ? $get_path_url['path'] . '/' : '/';
|
||||
|
||||
// add_stylesheet('css 구문', 출력순서); 숫자가 작을 수록 먼저 출력됨
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_JS_URL . '/remodal/remodal.css">', 11);
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_JS_URL . '/remodal/remodal-default-theme.css">', 12);
|
||||
add_javascript('<script src="' . G5_JS_URL . '/remodal/remodal.js"></script>', 10);
|
||||
?>
|
||||
<section id="anc_cf_url">
|
||||
<h2 class="h2_frm">짧은 주소 설정</h2>
|
||||
<?php echo $pg_anchor ?>
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
게시판과 컨텐츠 페이지에 짧은 URL 을 사용합니다. <a href="https://sir.kr/manual/g5/286" class="btn btn_03" target="_blank" style="margin-left:10px">설정 관련 메뉴얼 보기</a>
|
||||
<?php if ($is_use_apache && !$is_use_nginx) { ?>
|
||||
<?php if (!$is_apache_rewrite) { ?>
|
||||
<br><strong>Apache 서버인 경우 rewrite_module 이 비활성화 되어 있으면 짧은 주소를 사용할수 없습니다.</strong>
|
||||
<?php } elseif (!$is_write_file && $is_apache_need_rules) { // apache인 경우 ?>
|
||||
<br><strong>짧은 주소 사용시 아래 Apache 설정 코드를 참고하여 설정해 주세요.</strong>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="server_config_views">
|
||||
<?php if ($is_use_apache) { ?>
|
||||
<button type="button" data-remodal-target="modal_apache" class="btn btn_03">Apache 설정 코드 보기</button>
|
||||
<?php } ?>
|
||||
<?php if ($is_use_nginx) { ?>
|
||||
<button type="button" data-remodal-target="modal_nginx" class="btn btn_03">Nginx 설정 코드 보기</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>짧은주소 설정</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<?php
|
||||
$short_url_arrs = array(
|
||||
'0' => array('label' => '사용안함', 'url' => G5_URL . '/board.php?bo_table=free&wr_id=123'),
|
||||
'1' => array('label' => '숫자', 'url' => G5_URL . '/free/123'),
|
||||
'2' => array('label' => '글 이름', 'url' => G5_URL . '/free/안녕하세요/'),
|
||||
);
|
||||
foreach ($short_url_arrs as $k => $v) {
|
||||
$checked = ((int) $config['cf_bbs_rewrite'] === (int) $k) ? 'checked' : '';
|
||||
?>
|
||||
<tr>
|
||||
<td><input name="cf_bbs_rewrite" id="cf_bbs_rewrite_<?php echo $k; ?>" type="radio" value="<?php echo $k; ?>" <?php echo $checked; ?>><label for="cf_bbs_rewrite_<?php echo $k; ?>" class="rules_label"><?php echo $v['label']; ?></label></td>
|
||||
<td><?php echo $v['url']; ?></td>
|
||||
</tr>
|
||||
<?php } //end foreach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="server_rewrite_info">
|
||||
<div class="is_rewrite remodal" data-remodal-id="modal_apache" role="dialog" aria-labelledby="modalApache" aria-describedby="modal1Desc">
|
||||
|
||||
<button type="button" class="connect-close" data-remodal-action="close">
|
||||
<i class="fa fa-close"></i>
|
||||
<span class="txt">닫기</span>
|
||||
</button>
|
||||
|
||||
<h4 class="copy_title">.htaccess 파일에 적용할 코드입니다.
|
||||
<?php if (!$is_apache_rewrite) { ?>
|
||||
<br><span class="info-warning">Apache 서버인 경우 rewrite_module 이 비활성화 되어 있으면 짧은 주소를 사용할수 없습니다.</span>
|
||||
<?php } elseif (!$is_write_file && $is_apache_need_rules) { ?>
|
||||
<br><span class="info-warning">자동으로 .htaccess 파일을 수정 할수 있는 권한이 없습니다.<br>.htaccess 파일이 없다면 생성 후에, 아래 코드가 없으면 코드를 복사하여 붙여넣기 해 주세요.</span>
|
||||
<?php } elseif (!$is_apache_need_rules) { ?>
|
||||
<br><span class="info-success">정상적으로 적용된 상태입니다.</span>
|
||||
<?php } ?>
|
||||
</h4>
|
||||
<textarea readonly="readonly" rows="10"><?php echo get_mod_rewrite_rules(true); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="is_rewrite remodal" data-remodal-id="modal_nginx" role="dialog" aria-labelledby="modalNginx" aria-describedby="modal2Desc">
|
||||
|
||||
<button type="button" class="connect-close" data-remodal-action="close">
|
||||
<i class="fa fa-close"></i>
|
||||
<span class="txt">닫기</span>
|
||||
</button>
|
||||
<h4 class="copy_title">아래 코드를 복사하여 nginx 설정 파일에 적용해 주세요.</h4>
|
||||
<textarea readonly="readonly" rows="10"><?php echo get_nginx_conf_rules(true); ?></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$g5_debug['php']['begin_time'] = $begin_time = get_microtime();
|
||||
|
||||
$files = glob(G5_ADMIN_PATH . '/css/admin_extend_*');
|
||||
if (is_array($files)) {
|
||||
foreach ((array) $files as $k => $css_file) {
|
||||
|
||||
$fileinfo = pathinfo($css_file);
|
||||
$ext = $fileinfo['extension'];
|
||||
|
||||
if ($ext !== 'css') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$css_file = str_replace(G5_ADMIN_PATH, G5_ADMIN_URL, $css_file);
|
||||
add_stylesheet('<link rel="stylesheet" href="' . $css_file . '">', $k);
|
||||
}
|
||||
}
|
||||
|
||||
require_once G5_PATH . '/head.sub.php';
|
||||
|
||||
function print_menu1($key, $no = '')
|
||||
{
|
||||
global $menu;
|
||||
|
||||
$str = print_menu2($key, $no);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
function print_menu2($key, $no = '')
|
||||
{
|
||||
global $menu, $auth_menu, $is_admin, $auth, $g5, $sub_menu;
|
||||
|
||||
$str = "<ul>";
|
||||
for ($i = 1; $i < count($menu[$key]); $i++) {
|
||||
if (!isset($menu[$key][$i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($is_admin != 'super' && (!array_key_exists($menu[$key][$i][0], $auth) || !strstr($auth[$menu[$key][$i][0]], 'r'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$gnb_grp_div = $gnb_grp_style = '';
|
||||
|
||||
if (isset($menu[$key][$i][4])) {
|
||||
if (($menu[$key][$i][4] == 1 && $gnb_grp_style == false) || ($menu[$key][$i][4] != 1 && $gnb_grp_style == true)) {
|
||||
$gnb_grp_div = 'gnb_grp_div';
|
||||
}
|
||||
|
||||
if ($menu[$key][$i][4] == 1) {
|
||||
$gnb_grp_style = 'gnb_grp_style';
|
||||
}
|
||||
}
|
||||
|
||||
$current_class = '';
|
||||
|
||||
if ($menu[$key][$i][0] == $sub_menu) {
|
||||
$current_class = ' on';
|
||||
}
|
||||
|
||||
$str .= '<li data-menu="' . $menu[$key][$i][0] . '"><a href="' . $menu[$key][$i][2] . '" class="gnb_2da ' . $gnb_grp_style . ' ' . $gnb_grp_div . $current_class . '">' . $menu[$key][$i][1] . '</a></li>';
|
||||
|
||||
$auth_menu[$menu[$key][$i][0]] = $menu[$key][$i][1];
|
||||
}
|
||||
$str .= "</ul>";
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
$adm_menu_cookie = array(
|
||||
'container' => '',
|
||||
'gnb' => '',
|
||||
'btn_gnb' => '',
|
||||
);
|
||||
|
||||
if (!empty($_COOKIE['g5_admin_btn_gnb'])) {
|
||||
$adm_menu_cookie['container'] = 'container-small';
|
||||
$adm_menu_cookie['gnb'] = 'gnb_small';
|
||||
$adm_menu_cookie['btn_gnb'] = 'btn_gnb_open';
|
||||
}
|
||||
?>
|
||||
|
||||
<script>
|
||||
var g5_admin_csrf_token_key = "<?php echo (function_exists('admin_csrf_token_key')) ? admin_csrf_token_key() : ''; ?>";
|
||||
var tempX = 0;
|
||||
var tempY = 0;
|
||||
|
||||
function imageview(id, w, h) {
|
||||
|
||||
menu(id);
|
||||
|
||||
var el_id = document.getElementById(id);
|
||||
|
||||
//submenu = eval(name+".style");
|
||||
submenu = el_id.style;
|
||||
submenu.left = tempX - (w + 11);
|
||||
submenu.top = tempY - (h / 2);
|
||||
|
||||
selectBoxVisible();
|
||||
|
||||
if (el_id.style.display != 'none')
|
||||
selectBoxHidden(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="to_content"><a href="#container">본문 바로가기</a></div>
|
||||
|
||||
<header id="hd">
|
||||
<h1><?php echo $config['cf_title'] ?></h1>
|
||||
<div id="hd_top">
|
||||
<button type="button" id="btn_gnb" class="btn_gnb_close <?php echo $adm_menu_cookie['btn_gnb']; ?>">메뉴</button>
|
||||
<div id="logo"><a href="<?php echo correct_goto_url(G5_ADMIN_URL); ?>"><img src="<?php echo G5_ADMIN_URL ?>/img/logo.png" alt="<?php echo get_text($config['cf_title']); ?> 관리자"></a></div>
|
||||
|
||||
<div id="tnb">
|
||||
<ul>
|
||||
<?php if (defined('G5_USE_SHOP') && G5_USE_SHOP) { ?>
|
||||
<li class="tnb_li"><a href="<?php echo G5_SHOP_URL ?>/" class="tnb_shop" target="_blank" title="쇼핑몰 바로가기">쇼핑몰 바로가기</a></li>
|
||||
<?php } ?>
|
||||
<li class="tnb_li"><a href="<?php echo G5_URL ?>/" class="tnb_community" target="_blank" title="커뮤니티 바로가기">커뮤니티 바로가기</a></li>
|
||||
<li class="tnb_li"><a href="<?php echo G5_ADMIN_URL ?>/service.php" class="tnb_service">부가서비스</a></li>
|
||||
<li class="tnb_li"><button type="button" class="tnb_mb_btn">관리자<span class="./img/btn_gnb.png">메뉴열기</span></button>
|
||||
<ul class="tnb_mb_area">
|
||||
<li><a href="<?php echo G5_ADMIN_URL ?>/member_form.php?w=u&mb_id=<?php echo $member['mb_id'] ?>">관리자정보</a></li>
|
||||
<li id="tnb_logout"><a href="<?php echo G5_BBS_URL ?>/logout.php">로그아웃</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav id="gnb" class="gnb_large <?php echo $adm_menu_cookie['gnb']; ?>">
|
||||
<h2>관리자 주메뉴</h2>
|
||||
<ul class="gnb_ul">
|
||||
<?php
|
||||
$jj = 1;
|
||||
foreach ($amenu as $key => $value) {
|
||||
$href1 = $href2 = '';
|
||||
|
||||
if (isset($menu['menu' . $key][0][2]) && $menu['menu' . $key][0][2]) {
|
||||
$href1 = '<a href="' . $menu['menu' . $key][0][2] . '" class="gnb_1da">';
|
||||
$href2 = '</a>';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$current_class = "";
|
||||
if (isset($sub_menu) && (substr($sub_menu, 0, 3) == substr($menu['menu' . $key][0][0], 0, 3))) {
|
||||
$current_class = " on";
|
||||
}
|
||||
|
||||
$button_title = $menu['menu' . $key][0][1];
|
||||
?>
|
||||
<li class="gnb_li<?php echo $current_class; ?>">
|
||||
<button type="button" class="btn_op menu-<?php echo $key; ?> menu-order-<?php echo $jj; ?>" title="<?php echo $button_title; ?>"><?php echo $button_title; ?></button>
|
||||
<div class="gnb_oparea_wr">
|
||||
<div class="gnb_oparea">
|
||||
<h3><?php echo $menu['menu' . $key][0][1]; ?></h3>
|
||||
<?php echo print_menu1('menu' . $key, 1); ?>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<?php
|
||||
$jj++;
|
||||
} //end foreach
|
||||
?>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
<script>
|
||||
jQuery(function($) {
|
||||
|
||||
var menu_cookie_key = 'g5_admin_btn_gnb';
|
||||
|
||||
$(".tnb_mb_btn").click(function() {
|
||||
$(".tnb_mb_area").toggle();
|
||||
});
|
||||
|
||||
$("#btn_gnb").click(function() {
|
||||
|
||||
var $this = $(this);
|
||||
|
||||
try {
|
||||
if (!$this.hasClass("btn_gnb_open")) {
|
||||
set_cookie(menu_cookie_key, 1, 60 * 60 * 24 * 365);
|
||||
} else {
|
||||
delete_cookie(menu_cookie_key);
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
$("#container").toggleClass("container-small");
|
||||
$("#gnb").toggleClass("gnb_small");
|
||||
$this.toggleClass("btn_gnb_open");
|
||||
|
||||
});
|
||||
|
||||
$(".gnb_ul li .btn_op").click(function() {
|
||||
$(this).parent().addClass("on").siblings().removeClass("on");
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div id="wrapper">
|
||||
|
||||
<div id="container" class="<?php echo $adm_menu_cookie['container']; ?>">
|
||||
|
||||
<h1 id="container_title"><?php echo $g5['title'] ?></h1>
|
||||
<div class="container_wr">
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
function check_all(f)
|
||||
{
|
||||
var chk = document.getElementsByName("chk[]");
|
||||
|
||||
for (i=0; i<chk.length; i++)
|
||||
chk[i].checked = f.chkall.checked;
|
||||
}
|
||||
|
||||
function btn_check(f, act)
|
||||
{
|
||||
if (act == "update") // 선택수정
|
||||
{
|
||||
f.action = list_update_php;
|
||||
str = "수정";
|
||||
}
|
||||
else if (act == "delete") // 선택삭제
|
||||
{
|
||||
f.action = list_delete_php;
|
||||
str = "삭제";
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
var chk = document.getElementsByName("chk[]");
|
||||
var bchk = false;
|
||||
|
||||
for (i=0; i<chk.length; i++)
|
||||
{
|
||||
if (chk[i].checked)
|
||||
bchk = true;
|
||||
}
|
||||
|
||||
if (!bchk)
|
||||
{
|
||||
alert(str + "할 자료를 하나 이상 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "delete")
|
||||
{
|
||||
if (!confirm("선택한 자료를 정말 삭제 하시겠습니까?"))
|
||||
return;
|
||||
}
|
||||
|
||||
f.submit();
|
||||
}
|
||||
|
||||
function is_checked(elements_name)
|
||||
{
|
||||
var checked = false;
|
||||
var chk = document.getElementsByName(elements_name);
|
||||
for (var i=0; i<chk.length; i++) {
|
||||
if (chk[i].checked) {
|
||||
checked = true;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
|
||||
function delete_confirm(el)
|
||||
{
|
||||
if(confirm("한번 삭제한 자료는 복구할 방법이 없습니다.\n\n정말 삭제하시겠습니까?")) {
|
||||
var token = get_ajax_token();
|
||||
var href = el.href.replace(/&token=.+$/g, "");
|
||||
if(!token) {
|
||||
alert("토큰 정보가 올바르지 않습니다.");
|
||||
return false;
|
||||
}
|
||||
el.href = href+"&token="+token;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function delete_confirm2(msg)
|
||||
{
|
||||
if(confirm(msg))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_ajax_token()
|
||||
{
|
||||
var token = "",
|
||||
admin_csrf_token_key = (typeof g5_admin_csrf_token_key !== "undefined") ? g5_admin_csrf_token_key : "";
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: g5_admin_url+"/ajax.token.php",
|
||||
data : {admin_csrf_token_key:admin_csrf_token_key},
|
||||
cache: false,
|
||||
async: false,
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
if(data.error) {
|
||||
alert(data.error);
|
||||
if(data.url)
|
||||
document.location.href = data.url;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
token = data.token;
|
||||
}
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$(document).on("click", "form input:submit, form button:submit", function() {
|
||||
var f = this.form;
|
||||
var token = get_ajax_token();
|
||||
|
||||
if(!token) {
|
||||
alert("토큰 정보가 올바르지 않습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var $f = $(f);
|
||||
|
||||
if(typeof f.token === "undefined")
|
||||
$f.prepend('<input type="hidden" name="token" value="">');
|
||||
|
||||
$f.find("input[name=token]").val(token);
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,702 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
// 081022 : CSRF 방지를 위해 코드를 작성했으나 효과가 없어 주석처리 함
|
||||
if (!get_session('ss_admin')) {
|
||||
set_session('ss_admin', true);
|
||||
goto_url('.');
|
||||
}
|
||||
*/
|
||||
|
||||
// 스킨디렉토리를 SELECT 형식으로 얻음
|
||||
function get_skin_select($skin_gubun, $id, $name, $selected = '', $event = '')
|
||||
{
|
||||
global $config;
|
||||
|
||||
$skins = array();
|
||||
|
||||
if (defined('G5_THEME_PATH') && $config['cf_theme']) {
|
||||
$dirs = get_skin_dir($skin_gubun, G5_THEME_PATH . '/' . G5_SKIN_DIR);
|
||||
if (!empty($dirs)) {
|
||||
foreach ($dirs as $dir) {
|
||||
$skins[] = 'theme/' . $dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$skins = array_merge($skins, get_skin_dir($skin_gubun));
|
||||
|
||||
$str = "<select id=\"$id\" name=\"$name\" $event>\n";
|
||||
for ($i = 0; $i < count($skins); $i++) {
|
||||
if ($i == 0) {
|
||||
$str .= "<option value=\"\">선택</option>";
|
||||
}
|
||||
if (preg_match('#^theme/(.+)$#', $skins[$i], $match)) {
|
||||
$text = '(테마) ' . $match[1];
|
||||
} else {
|
||||
$text = $skins[$i];
|
||||
}
|
||||
|
||||
$str .= option_selected($skins[$i], $selected, $text);
|
||||
}
|
||||
$str .= "</select>";
|
||||
return $str;
|
||||
}
|
||||
|
||||
// 모바일 스킨디렉토리를 SELECT 형식으로 얻음
|
||||
function get_mobile_skin_select($skin_gubun, $id, $name, $selected = '', $event = '')
|
||||
{
|
||||
global $config;
|
||||
|
||||
$skins = array();
|
||||
|
||||
if (defined('G5_THEME_PATH') && $config['cf_theme']) {
|
||||
$dirs = get_skin_dir($skin_gubun, G5_THEME_MOBILE_PATH . '/' . G5_SKIN_DIR);
|
||||
if (!empty($dirs)) {
|
||||
foreach ($dirs as $dir) {
|
||||
$skins[] = 'theme/' . $dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$skins = array_merge($skins, get_skin_dir($skin_gubun, G5_MOBILE_PATH . '/' . G5_SKIN_DIR));
|
||||
|
||||
$str = "<select id=\"$id\" name=\"$name\" $event>\n";
|
||||
for ($i = 0; $i < count($skins); $i++) {
|
||||
if ($i == 0) {
|
||||
$str .= "<option value=\"\">선택</option>";
|
||||
}
|
||||
if (preg_match('#^theme/(.+)$#', $skins[$i], $match)) {
|
||||
$text = '(테마) ' . $match[1];
|
||||
} else {
|
||||
$text = $skins[$i];
|
||||
}
|
||||
|
||||
$str .= option_selected($skins[$i], $selected, $text);
|
||||
}
|
||||
$str .= "</select>";
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
||||
// 스킨경로를 얻는다
|
||||
function get_skin_dir($skin, $skin_path = G5_SKIN_PATH)
|
||||
{
|
||||
global $g5;
|
||||
|
||||
$result_array = array();
|
||||
|
||||
$dirname = $skin_path . '/' . $skin . '/';
|
||||
if (!is_dir($dirname)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$handle = opendir($dirname);
|
||||
while ($file = readdir($handle)) {
|
||||
if ($file == '.' || $file == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($dirname . $file)) {
|
||||
$result_array[] = $file;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
sort($result_array);
|
||||
|
||||
return $result_array;
|
||||
}
|
||||
|
||||
|
||||
// 테마
|
||||
function get_theme_dir()
|
||||
{
|
||||
$result_array = array();
|
||||
|
||||
$dirname = G5_PATH . '/' . G5_THEME_DIR . '/';
|
||||
$handle = opendir($dirname);
|
||||
while ($file = readdir($handle)) {
|
||||
if ($file == '.' || $file == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($dirname . $file)) {
|
||||
$theme_path = $dirname . $file;
|
||||
if (is_file($theme_path . '/index.php') && is_file($theme_path . '/head.php') && is_file($theme_path . '/tail.php')) {
|
||||
$result_array[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
natsort($result_array);
|
||||
|
||||
return $result_array;
|
||||
}
|
||||
|
||||
|
||||
// 테마정보
|
||||
function get_theme_info($dir)
|
||||
{
|
||||
$info = array();
|
||||
$path = G5_PATH . '/' . G5_THEME_DIR . '/' . $dir;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$screenshot = $path . '/screenshot.png';
|
||||
$screenshot_url = '';
|
||||
if (is_file($screenshot)) {
|
||||
$size = @getimagesize($screenshot);
|
||||
|
||||
if (isset($size[2]) && $size[2] == 3) { // PNG
|
||||
$screenshot_url = str_replace(G5_PATH, G5_URL, $screenshot);
|
||||
}
|
||||
}
|
||||
|
||||
$info['screenshot'] = $screenshot_url;
|
||||
|
||||
$text = $path . '/readme.txt';
|
||||
if (is_file($text)) {
|
||||
$content = file($text, false);
|
||||
$content = array_map('trim', $content);
|
||||
|
||||
// 💡 [핵심 수정] readme.txt 파일의 각 줄에 정보가 있는지 확인하고, 있을 경우에만 값을 할당하도록 변경
|
||||
preg_match('#^Theme Name:(.+)$#i', (isset($content[0]) ? $content[0] : ''), $m0);
|
||||
preg_match('#^Theme URI:(.+)$#i', (isset($content[1]) ? $content[1] : ''), $m1);
|
||||
preg_match('#^Maker:(.+)$#i', (isset($content[2]) ? $content[2] : ''), $m2);
|
||||
preg_match('#^Maker URI:(.+)$#i', (isset($content[3]) ? $content[3] : ''), $m3);
|
||||
preg_match('#^Version:(.+)$#i', (isset($content[4]) ? $content[4] : ''), $m4);
|
||||
preg_match('#^Detail:(.+)$#i', (isset($content[5]) ? $content[5] : ''), $m5);
|
||||
preg_match('#^License:(.+)$#i', (isset($content[6]) ? $content[6] : ''), $m6);
|
||||
preg_match('#^License URI:(.+)$#i', (isset($content[7]) ? $content[7] : ''), $m7);
|
||||
|
||||
$info['theme_name'] = isset($m0[1]) ? trim($m0[1]) : '';
|
||||
$info['theme_uri'] = isset($m1[1]) ? trim($m1[1]) : '';
|
||||
$info['maker'] = isset($m2[1]) ? trim($m2[1]) : '';
|
||||
$info['maker_uri'] = isset($m3[1]) ? trim($m3[1]) : '';
|
||||
$info['version'] = isset($m4[1]) ? trim($m4[1]) : '';
|
||||
$info['detail'] = isset($m5[1]) ? trim($m5[1]) : '';
|
||||
$info['license'] = isset($m6[1]) ? trim($m6[1]) : '';
|
||||
$info['license_uri'] = isset($m7[1]) ? trim($m7[1]) : '';
|
||||
}
|
||||
|
||||
if (empty($info['theme_name'])) {
|
||||
$info['theme_name'] = $dir;
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
|
||||
// 테마설정 정보
|
||||
function get_theme_config_value($dir, $key = '*')
|
||||
{
|
||||
$tconfig = array();
|
||||
|
||||
$theme_config_file = G5_PATH . '/' . G5_THEME_DIR . '/' . $dir . '/theme.config.php';
|
||||
if (is_file($theme_config_file)) {
|
||||
include $theme_config_file;
|
||||
// 22.05.26 Undefined Variable $theme_config;
|
||||
if ($key == '*') {
|
||||
$tconfig = $theme_config;
|
||||
} else {
|
||||
$keys = array_map('trim', explode(',', $key));
|
||||
foreach ($keys as $v) {
|
||||
$tconfig[$v] = isset($theme_config[$v]) ? trim($theme_config[$v]) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tconfig;
|
||||
}
|
||||
|
||||
|
||||
// 회원권한을 SELECT 형식으로 얻음
|
||||
function get_member_level_select($name, $start_id = 0, $end_id = 10, $selected = "", $event = "")
|
||||
{
|
||||
global $g5;
|
||||
|
||||
$str = "\n<select id=\"{$name}\" name=\"{$name}\"";
|
||||
if ($event) {
|
||||
$str .= " $event";
|
||||
}
|
||||
$str .= ">\n";
|
||||
for ($i = $start_id; $i <= $end_id; $i++) {
|
||||
$str .= '<option value="' . $i . '"';
|
||||
if ($i == $selected) {
|
||||
$str .= ' selected="selected"';
|
||||
}
|
||||
$str .= ">{$i}</option>\n";
|
||||
}
|
||||
$str .= "</select>\n";
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
||||
// 회원아이디를 SELECT 형식으로 얻음
|
||||
function get_member_id_select($name, $level, $selected = "", $event = "")
|
||||
{
|
||||
global $g5;
|
||||
|
||||
$sql = " select mb_id from {$g5['member_table']} where mb_level >= '{$level}' ";
|
||||
$result = sql_query($sql);
|
||||
$str = '<select id="' . $name . '" name="' . $name . '" ' . $event . '><option value="">선택안함</option>';
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$str .= '<option value="' . $row['mb_id'] . '"';
|
||||
if ($row['mb_id'] == $selected) {
|
||||
$str .= ' selected';
|
||||
}
|
||||
$str .= '>' . $row['mb_id'] . '</option>';
|
||||
}
|
||||
$str .= '</select>';
|
||||
return $str;
|
||||
}
|
||||
|
||||
// php8 버전 호환 권한 검사 함수
|
||||
function auth_check_menu($auth, $sub_menu, $attr, $return = false)
|
||||
{
|
||||
|
||||
$check_auth = isset($auth[$sub_menu]) ? $auth[$sub_menu] : '';
|
||||
return auth_check($check_auth, $attr, $return);
|
||||
}
|
||||
|
||||
// 권한 검사
|
||||
function auth_check($auth, $attr, $return = false)
|
||||
{
|
||||
global $is_admin;
|
||||
|
||||
if ($is_admin == 'super') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trim($auth)) {
|
||||
$msg = '이 메뉴에는 접근 권한이 없습니다.\\n\\n접근 권한은 최고관리자만 부여할 수 있습니다.';
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg);
|
||||
}
|
||||
}
|
||||
|
||||
$attr = strtolower($attr);
|
||||
|
||||
if (!strstr($auth, $attr)) {
|
||||
if ($attr == 'r') {
|
||||
$msg = '읽을 권한이 없습니다.';
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg);
|
||||
}
|
||||
} else if ($attr == 'w') {
|
||||
$msg = '입력, 추가, 생성, 수정 권한이 없습니다.';
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg);
|
||||
}
|
||||
} else if ($attr == 'd') {
|
||||
$msg = '삭제 권한이 없습니다.';
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg);
|
||||
}
|
||||
} else {
|
||||
$msg = '속성이 잘못 되었습니다.';
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 작업아이콘 출력
|
||||
function icon($act, $link = '', $target = '_parent')
|
||||
{
|
||||
global $g5;
|
||||
|
||||
$img = array('입력' => 'insert', '추가' => 'insert', '생성' => 'insert', '수정' => 'modify', '삭제' => 'delete', '이동' => 'move', '그룹' => 'move', '보기' => 'view', '미리보기' => 'view', '복사' => 'copy');
|
||||
$icon = '<img src="' . G5_ADMIN_PATH . '/img/icon_' . $img[$act] . '.gif" title="' . $act . '">';
|
||||
if ($link) {
|
||||
$s = '<a href="' . $link . '">' . $icon . '</a>';
|
||||
} else {
|
||||
$s = $icon;
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
// rm -rf 옵션 : exec(), system() 함수를 사용할 수 없는 서버 또는 win32용 대체
|
||||
// www.php.net 참고 : pal at degerstrom dot com
|
||||
function rm_rf($file)
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
if (is_dir($file)) {
|
||||
$handle = opendir($file);
|
||||
while ($filename = readdir($handle)) {
|
||||
if ($filename != '.' && $filename != '..') {
|
||||
rm_rf($file . '/' . $filename);
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
|
||||
@chmod($file, G5_DIR_PERMISSION);
|
||||
@rmdir($file);
|
||||
} else {
|
||||
@chmod($file, G5_FILE_PERMISSION);
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 입력 폼 안내문
|
||||
function help($help = "")
|
||||
{
|
||||
global $g5;
|
||||
|
||||
$str = '<span class="frm_info">' . str_replace("\n", "<br>", $help) . '</span>';
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
// 출력순서
|
||||
function order_select($fld, $sel = '')
|
||||
{
|
||||
$s = '<select name="' . $fld . '" id="' . $fld . '">';
|
||||
for ($i = 1; $i <= 100; $i++) {
|
||||
$s .= '<option value="' . $i . '" ';
|
||||
if ($sel) {
|
||||
if ($i == $sel) {
|
||||
$s .= 'selected';
|
||||
}
|
||||
} else {
|
||||
if ($i == 50) {
|
||||
$s .= 'selected';
|
||||
}
|
||||
}
|
||||
$s .= '>' . $i . '</option>';
|
||||
}
|
||||
$s .= '</select>';
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
// 불법접근을 막도록 토큰을 생성하면서 토큰값을 리턴
|
||||
function get_admin_token()
|
||||
{
|
||||
$token = md5(uniqid(rand(), true));
|
||||
set_session('ss_admin_token', $token);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
// 관리자가 자동등록방지를 사용해야 할 경우
|
||||
function get_admin_captcha_by($type = 'get')
|
||||
{
|
||||
|
||||
$captcha_name = 'ss_admin_use_captcha';
|
||||
|
||||
if ($type === 'remove') {
|
||||
set_session($captcha_name, '');
|
||||
}
|
||||
|
||||
return get_session($captcha_name);
|
||||
}
|
||||
|
||||
//input value 에서 xss 공격 filter 역할을 함 ( 반드시 input value='' 타입에만 사용할것 )
|
||||
function get_sanitize_input($s, $is_html = false)
|
||||
{
|
||||
|
||||
if (!$is_html) {
|
||||
$s = strip_tags($s);
|
||||
}
|
||||
|
||||
$s = htmlspecialchars($s, ENT_QUOTES, 'utf-8');
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
function domain_mail_host($is_at=true){
|
||||
list($domain_host,) = explode(':', $_SERVER['HTTP_HOST']);
|
||||
|
||||
if ('www.' === substr($domain_host, 0, 4)) {
|
||||
$domain_host = substr($domain_host, 4);
|
||||
}
|
||||
|
||||
return $is_at ? '@'.$domain_host : $domain_host;
|
||||
}
|
||||
|
||||
function check_log_folder($log_path, $is_delete = true)
|
||||
{
|
||||
|
||||
if (is_writable($log_path)) {
|
||||
|
||||
// 아파치 서버인 경우 웹에서 해당 폴더 접근 막기
|
||||
$htaccess_file = $log_path . '/.htaccess';
|
||||
if (!file_exists($htaccess_file)) {
|
||||
if ($handle = @fopen($htaccess_file, 'w')) {
|
||||
fwrite($handle, 'Order deny,allow' . "\n");
|
||||
fwrite($handle, 'Deny from all' . "\n");
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
|
||||
// 아파치 서버인 경우 해당 디렉토리 파일 목록 안보이게 하기
|
||||
$index_file = $log_path . '/index.php';
|
||||
if (!file_exists($index_file)) {
|
||||
if ($handle = @fopen($index_file, 'w')) {
|
||||
fwrite($handle, '');
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_delete) {
|
||||
try {
|
||||
// txt 파일과 log 파일을 조회하여 30일이 지난 파일은 삭제합니다.
|
||||
$txt_files = glob($log_path . '/*.txt');
|
||||
$log_files = glob($log_path . '/*.log');
|
||||
|
||||
$del_files = array_merge($txt_files, $log_files);
|
||||
|
||||
if ($del_files && is_array($del_files)) {
|
||||
foreach ($del_files as $del_file) {
|
||||
$filetime = filemtime($del_file);
|
||||
// 30일이 지난 파일을 삭제
|
||||
if ($filetime && $filetime < (G5_SERVER_TIME - 2592000)) {
|
||||
@unlink($del_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST로 넘어온 토큰과 세션에 저장된 토큰 비교
|
||||
function check_admin_token()
|
||||
{
|
||||
$token = get_session('ss_admin_token');
|
||||
set_session('ss_admin_token', '');
|
||||
|
||||
if (!$token || !$_REQUEST['token'] || $token != $_REQUEST['token']) {
|
||||
alert('올바른 방법으로 이용해 주십시오.', G5_URL);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function admin_csrf_token_key($is_must=0){
|
||||
global $member;
|
||||
|
||||
$key = '';
|
||||
|
||||
if($is_must || !((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'))){
|
||||
$key = md5((isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '').(defined('G5_TOKEN_ENCRYPTION_KEY') ? G5_TOKEN_ENCRYPTION_KEY : '').$member['mb_id'].$_SERVER['DOCUMENT_ROOT']);
|
||||
}
|
||||
|
||||
return run_replace('admin_csrf_token_key', $key, $is_must);
|
||||
}
|
||||
|
||||
// 관리자 페이지 referer 체크
|
||||
function admin_referer_check($return = false)
|
||||
{
|
||||
$referer = isset($_SERVER['HTTP_REFERER']) ? trim($_SERVER['HTTP_REFERER']) : '';
|
||||
if (!$referer) {
|
||||
$msg = '정보가 올바르지 않습니다.';
|
||||
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg, G5_URL);
|
||||
}
|
||||
}
|
||||
|
||||
$p = @parse_url($referer);
|
||||
|
||||
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
|
||||
$msg = '';
|
||||
|
||||
if ($host != $p['host']) {
|
||||
$msg = '올바른 방법으로 이용해 주십시오.';
|
||||
}
|
||||
|
||||
if ($p['path'] && !preg_match('/\/' . preg_quote(G5_ADMIN_DIR) . '\//i', $p['path'])) {
|
||||
$msg = '올바른 방법으로 이용해 주십시오';
|
||||
}
|
||||
|
||||
if ($msg) {
|
||||
if ($return) {
|
||||
return $msg;
|
||||
} else {
|
||||
alert($msg, G5_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function admin_check_xss_params($params)
|
||||
{
|
||||
|
||||
if (!$params) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
admin_check_xss_params($value);
|
||||
} else if (
|
||||
(preg_match('/<\s?[^\>]*\/?\s?>/i', $value) && (preg_match('/script.*?\/script/ius', $value) || preg_match('/on[a-z]+=*/ius', $value))) || preg_match('/^(?=.*token\()(?=.*xmlhttprequest\()(?=.*send\().*$/im', $value) ||
|
||||
(preg_match('/(on[a-z]+|focus)=.*/ius', $value) && preg_match('/(eval|atob|fetch|expression|exec|prompt)(\s*)\((.*)\)/ius', $value))) {
|
||||
alert('요청 쿼리에 잘못된 스크립트문장이 있습니다.\\nXSS 공격일수도 있습니다.', G5_URL);
|
||||
die();
|
||||
} else if (preg_match('/atob\s*\(\s*[\'"]?([a-zA-Z0-9+\/=]+)[\'"]?\s*\)/ius', $value, $matches)) {
|
||||
$decoded = base64_decode($matches[1], true);
|
||||
if ($decoded && preg_match('/(eval|fetch|script|alert|settimeout|setinterval)/ius', $decoded)) {
|
||||
// error_log("Base64 XSS 시도 감지: key=$key, decoded=$decoded, IP=" . $_SERVER['REMOTE_ADDR']);
|
||||
alert('Base64로 인코딩된 위험한 스크립트가 발견되었습니다.', G5_URL);
|
||||
die();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function admin_menu_find_by($call, $search_key)
|
||||
{
|
||||
global $menu;
|
||||
|
||||
static $cache_menu = array();
|
||||
|
||||
if (empty($cache_menu)) {
|
||||
foreach ($menu as $k1 => $arr1) {
|
||||
|
||||
if (empty($arr1)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($arr1 as $k2 => $arr2) {
|
||||
if (empty($arr2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$menu_key = isset($arr2[3]) ? $arr2[3] : '';
|
||||
if (empty($menu_key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cache_menu[$menu_key] = array(
|
||||
'sub_menu' => $arr2[0],
|
||||
'title' => $arr2[1],
|
||||
'link' => $arr2[2],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($cache_menu[$call]) && isset($cache_menu[$call][$search_key])) {
|
||||
return $cache_menu[$call][$search_key];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// 접근 권한 검사
|
||||
if (!$member['mb_id']) {
|
||||
alert('로그인 하십시오.', G5_BBS_URL . '/login.php?url=' . urlencode(correct_goto_url(G5_ADMIN_URL)));
|
||||
} else if ($is_admin != 'super') {
|
||||
$auth = array();
|
||||
$sql = " select au_menu, au_auth from {$g5['auth_table']} where mb_id = '{$member['mb_id']}' ";
|
||||
$result = sql_query($sql);
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$auth[$row['au_menu']] = $row['au_auth'];
|
||||
}
|
||||
|
||||
if (!$i) {
|
||||
alert('최고관리자 또는 관리권한이 있는 회원만 접근 가능합니다.', G5_URL);
|
||||
}
|
||||
}
|
||||
|
||||
// 관리자의 클라이언트를 검증하여 일치하지 않으면 세션을 끊고 관리자에게 메일을 보낸다.
|
||||
if (!verify_mb_key($member)) {
|
||||
session_destroy();
|
||||
|
||||
include_once G5_LIB_PATH . '/mailer.lib.php';
|
||||
|
||||
// 메일 알림
|
||||
mailer($member['mb_nick'], $member['mb_email'], $member['mb_email'], 'XSS 공격 알림', $_SERVER['REMOTE_ADDR'] . ' 아이피로 XSS 공격이 있었습니다.<br><br>관리자 권한을 탈취하려는 접근이므로 주의하시기 바랍니다.<br><br>해당 아이피는 차단하시고 의심되는 게시물이 있는지 확인하시기 바랍니다.' . G5_URL, 0);
|
||||
|
||||
alert_close('정상적으로 로그인하여 접근하시기 바랍니다.');
|
||||
}
|
||||
|
||||
if (isset($auth) && is_array($auth)) {
|
||||
@ksort($auth);
|
||||
} else {
|
||||
$auth = array();
|
||||
}
|
||||
|
||||
// 가변 메뉴
|
||||
unset($auth_menu);
|
||||
unset($menu);
|
||||
unset($amenu);
|
||||
$tmp = dir(G5_ADMIN_PATH);
|
||||
$menu_files = array();
|
||||
while ($entry = $tmp->read()) {
|
||||
if (!preg_match('/^admin.menu([0-9]{3}).*\.php$/', $entry, $m)) {
|
||||
continue; // 파일명이 menu 으로 시작하지 않으면 무시한다.
|
||||
}
|
||||
|
||||
$amenu[$m[1]] = $entry;
|
||||
$menu_files[] = G5_ADMIN_PATH . '/' . $entry;
|
||||
}
|
||||
@asort($menu_files);
|
||||
foreach ($menu_files as $file) {
|
||||
include_once $file;
|
||||
}
|
||||
@ksort($amenu);
|
||||
|
||||
$amenu = run_replace('admin_amenu', $amenu);
|
||||
if (isset($menu) && $menu) {
|
||||
$menu = run_replace('admin_menu', $menu);
|
||||
}
|
||||
|
||||
$arr_query = array();
|
||||
if (isset($sst)) {
|
||||
$arr_query[] = 'sst=' . $sst;
|
||||
}
|
||||
if (isset($sod)) {
|
||||
$arr_query[] = 'sod=' . $sod;
|
||||
}
|
||||
if (isset($sfl)) {
|
||||
$arr_query[] = 'sfl=' . $sfl;
|
||||
}
|
||||
if (isset($stx)) {
|
||||
$arr_query[] = 'stx=' . $stx;
|
||||
}
|
||||
if (isset($page)) {
|
||||
$arr_query[] = 'page=' . $page;
|
||||
}
|
||||
$qstr = implode("&", $arr_query);
|
||||
|
||||
if (isset($_REQUEST) && $_REQUEST) {
|
||||
if (admin_referer_check(true)) {
|
||||
admin_check_xss_params($_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 관리자에서는 추가 스크립트와 추가 매타태그, 방문자분석 스크립트가 실행되지 않게 빈값으로 합니다.
|
||||
if (run_replace('safe_admin_add_script_boolean', false) === false) {
|
||||
$config['cf_analytics'] = '';
|
||||
$config['cf_add_script'] = '';
|
||||
$config['cf_add_meta'] = '';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$menu['menu000'] = array(
|
||||
array('000000', '빌더설정', G5_ADMIN_URL . '/rb/rb_form.php', 'rb_config'),
|
||||
array('000000', '빌더설정', G5_ADMIN_URL . '/rb/rb_form.php', 'rb_config'),
|
||||
array('000100', '환경설정', G5_ADMIN_URL . '/rb/config_form.php', 'rb_config'),
|
||||
array('000200', '모듈관리', G5_ADMIN_URL . '/rb/module_list.php', 'rb_config'),
|
||||
array('000000', ' ', G5_ADMIN_URL . '', 'rb_config'),
|
||||
array('000300', '배너 관리', G5_ADMIN_URL . '/rb/banner_list.php', 'rb_config'),
|
||||
array('000400', '게시물 관리', G5_ADMIN_URL . '/rb/bbs_list.php', 'rb_config'),
|
||||
array('000500', 'SEO 관리 ', G5_ADMIN_URL . '/rb/seo_form.php', 'rb_config'),
|
||||
array('000000', ' ', G5_ADMIN_URL . '', 'rb_config'),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
$menu['menu100'] = array(
|
||||
array('100000', '환경설정', G5_ADMIN_URL . '/config_form.php', 'config'),
|
||||
array('100100', '기본환경설정', G5_ADMIN_URL . '/config_form.php', 'cf_basic'),
|
||||
array('100200', '관리권한설정', G5_ADMIN_URL . '/auth_list.php', 'cf_auth'),
|
||||
array('100280', '테마설정', G5_ADMIN_URL . '/theme.php', 'cf_theme', 1),
|
||||
array('100290', '메뉴설정', G5_ADMIN_URL . '/menu_list.php', 'cf_menu', 1),
|
||||
array('100300', '메일 테스트', G5_ADMIN_URL . '/sendmail_test.php', 'cf_mailtest'),
|
||||
array('100310', '팝업레이어관리', G5_ADMIN_URL . '/newwinlist.php', 'scf_poplayer'),
|
||||
array('100800', '세션파일 일괄삭제', G5_ADMIN_URL . '/session_file_delete.php', 'cf_session', 1),
|
||||
array('100900', '캐시파일 일괄삭제', G5_ADMIN_URL . '/cache_file_delete.php', 'cf_cache', 1),
|
||||
array('100910', '캡챠파일 일괄삭제', G5_ADMIN_URL . '/captcha_file_delete.php', 'cf_captcha', 1),
|
||||
array('100920', '썸네일파일 일괄삭제', G5_ADMIN_URL . '/thumbnail_file_delete.php', 'cf_thumbnail', 1),
|
||||
array('100500', 'phpinfo()', G5_ADMIN_URL . '/phpinfo.php', 'cf_phpinfo')
|
||||
);
|
||||
|
||||
if (version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE) {
|
||||
$menu['menu100'][] = array('100510', 'Browscap 업데이트', G5_ADMIN_URL . '/browscap.php', 'cf_browscap');
|
||||
$menu['menu100'][] = array('100520', '접속로그 변환', G5_ADMIN_URL . '/browscap_convert.php', 'cf_visit_cnvrt');
|
||||
}
|
||||
|
||||
$menu['menu100'][] = array('100410', 'DB업그레이드', G5_ADMIN_URL . '/dbupgrade.php', 'db_upgrade');
|
||||
$menu['menu100'][] = array('100400', '부가서비스', G5_ADMIN_URL . '/service.php', 'cf_service');
|
||||
$menu['menu100'][] = array('100420', '모듈 설치', G5_ADMIN_URL . '/reinstall_update/install.php', 'module_install');
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
$menu['menu200'] = array(
|
||||
array('200000', '회원관리', G5_ADMIN_URL . '/member_list.php', 'member'),
|
||||
array('200100', '회원관리', G5_ADMIN_URL . '/member_list.php', 'mb_list'),
|
||||
array('200300', '회원메일발송', G5_ADMIN_URL . '/mail_list.php', 'mb_mail'),
|
||||
array('200800', '접속자집계', G5_ADMIN_URL . '/visit_list.php', 'mb_visit', 1),
|
||||
array('200810', '접속자검색', G5_ADMIN_URL . '/visit_search.php', 'mb_search', 1),
|
||||
array('200820', '접속자로그삭제', G5_ADMIN_URL . '/visit_delete.php', 'mb_delete', 1),
|
||||
array('200200', '포인트관리', G5_ADMIN_URL . '/point_list.php', 'mb_point'),
|
||||
array('200900', '투표관리', G5_ADMIN_URL . '/poll_list.php', 'mb_poll'),
|
||||
|
||||
// 💡 [추가] 휴지통 메뉴
|
||||
array('200950', '휴지통', G5_ADMIN_URL . '/trash_list.php', 'mb_trash'),
|
||||
);
|
||||
?>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$menu['menu300'] = array(
|
||||
array('300000', '게시판관리', '' . G5_ADMIN_URL . '/board_list.php', 'board'),
|
||||
array('300100', '게시판관리', '' . G5_ADMIN_URL . '/board_list.php', 'bbs_board'),
|
||||
array('300200', '게시판그룹관리', '' . G5_ADMIN_URL . '/boardgroup_list.php', 'bbs_group'),
|
||||
array('300300', '인기검색어관리', '' . G5_ADMIN_URL . '/popular_list.php', 'bbs_poplist', 1),
|
||||
array('300400', '인기검색어순위', '' . G5_ADMIN_URL . '/popular_rank.php', 'bbs_poprank', 1),
|
||||
array('300500', '1:1문의설정', '' . G5_ADMIN_URL . '/qa_config.php', 'qa'),
|
||||
array('300600', '내용관리', G5_ADMIN_URL . '/contentlist.php', 'scf_contents', 1),
|
||||
array('300700', 'FAQ관리', G5_ADMIN_URL . '/faqmasterlist.php', 'scf_faq', 1),
|
||||
array('300820', '글,댓글 현황', G5_ADMIN_URL . '/write_count.php', 'scf_write_count'),
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
if (!defined('G5_USE_SHOP') || !G5_USE_SHOP) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu['menu400'] = array(
|
||||
array('400000', '쇼핑몰관리', G5_ADMIN_URL . '/shop_admin/', 'shop_config'),
|
||||
array('400010', '쇼핑몰현황', G5_ADMIN_URL . '/shop_admin/', 'shop_index'),
|
||||
array('400100', '쇼핑몰설정', G5_ADMIN_URL . '/shop_admin/configform.php', 'scf_config'),
|
||||
array('400400', '주문내역', G5_ADMIN_URL . '/shop_admin/orderlist.php', 'scf_order', 1),
|
||||
array('400440', '개인결제관리', G5_ADMIN_URL . '/shop_admin/personalpaylist.php', 'scf_personalpay', 1),
|
||||
array('400200', '분류관리', G5_ADMIN_URL . '/shop_admin/categorylist.php', 'scf_cate'),
|
||||
array('400300', '상품관리', G5_ADMIN_URL . '/shop_admin/itemlist.php', 'scf_item'),
|
||||
array('400660', '상품문의', G5_ADMIN_URL . '/shop_admin/itemqalist.php', 'scf_item_qna'),
|
||||
array('400650', '사용후기', G5_ADMIN_URL . '/shop_admin/itemuselist.php', 'scf_ps'),
|
||||
array('400620', '상품재고관리', G5_ADMIN_URL . '/shop_admin/itemstocklist.php', 'scf_item_stock'),
|
||||
array('400610', '상품유형관리', G5_ADMIN_URL . '/shop_admin/itemtypelist.php', 'scf_item_type'),
|
||||
array('400500', '상품옵션재고관리', G5_ADMIN_URL . '/shop_admin/optionstocklist.php', 'scf_item_option'),
|
||||
array('400800', '쿠폰관리', G5_ADMIN_URL . '/shop_admin/couponlist.php', 'scf_coupon'),
|
||||
array('400810', '쿠폰존관리', G5_ADMIN_URL . '/shop_admin/couponzonelist.php', 'scf_coupon_zone'),
|
||||
array('400750', '추가배송비관리', G5_ADMIN_URL . '/shop_admin/sendcostlist.php', 'scf_sendcost', 1),
|
||||
array('400410', '미완료주문', G5_ADMIN_URL . '/shop_admin/inorderlist.php', 'scf_inorder', 1),
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
if (!defined('G5_USE_SHOP') || !G5_USE_SHOP) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu['menu500'] = array(
|
||||
array('500000', '쇼핑몰현황/기타', G5_ADMIN_URL . '/shop_admin/itemsellrank.php', 'shop_stats'),
|
||||
array('500110', '매출현황', G5_ADMIN_URL . '/shop_admin/sale1.php', 'sst_order_stats'),
|
||||
array('500100', '상품판매순위', G5_ADMIN_URL . '/shop_admin/itemsellrank.php', 'sst_rank'),
|
||||
array('500120', '주문내역출력', G5_ADMIN_URL . '/shop_admin/orderprint.php', 'sst_print_order', 1),
|
||||
array('500400', '재입고SMS알림', G5_ADMIN_URL . '/shop_admin/itemstocksms.php', 'sst_stock_sms', 1),
|
||||
array('500300', '이벤트관리', G5_ADMIN_URL . '/shop_admin/itemevent.php', 'scf_event'),
|
||||
array('500310', '이벤트일괄처리', G5_ADMIN_URL . '/shop_admin/itemeventlist.php', 'scf_event_mng'),
|
||||
array('500500', '배너관리', G5_ADMIN_URL . '/shop_admin/bannerlist.php', 'scf_banner', 1),
|
||||
array('500140', '보관함현황', G5_ADMIN_URL . '/shop_admin/wishlist.php', 'sst_wish'),
|
||||
array('500210', '가격비교사이트', G5_ADMIN_URL . '/shop_admin/price.php', 'sst_compare', 1)
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 💡 [수정] 600번대 최상위 메뉴 배열에 아이콘 클래스('fa-envelope')를 추가합니다.
|
||||
$menu['menu600'][] = array('600000', '메일 관리', G5_ADMIN_URL.'/mail_manage/smtp_config.php', 'mail_manager', 'fa-envelope');
|
||||
|
||||
// '메일 관리'의 하위 메뉴들을 정의합니다.
|
||||
$menu['menu600'][] = array('600100', 'SMTP 설정', G5_ADMIN_URL.'/mail_manage/smtp_config.php', 'mail_smtp_config');
|
||||
$menu['menu600'][] = array('600200', '메일 템플릿 관리', G5_ADMIN_URL.'/mail_manage/template.php', 'mail_template');
|
||||
$menu['menu600'][] = array('600300', '메일 발송 이력', G5_ADMIN_URL.'/mail_manage/send_log.php', 'mail_send_log');
|
||||
$menu['menu600'][] = array('600900', '솔루션 설치', G5_ADMIN_URL.'/mail_manage/install.php', 'mail_solution_install');
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// 800번대 최상위 메뉴 '견적 관리'를 정의합니다.
|
||||
$menu['menu800'][] = array('800000', '견적 관리', G5_ADMIN_URL . '/order_manage/statistics.php', 'order_manage', 'fa-file-text-o');
|
||||
|
||||
// '견적 관리'의 하위 메뉴들을 정의합니다.
|
||||
$menu['menu800'][] = array('800100', '견적 목록', G5_BBS_URL . '/board.php?bo_table=order', 'order_list');
|
||||
$menu['menu800'][] = array('800200', '견적 통계', G5_ADMIN_URL . '/order_manage/statistics.php', 'order_statistics');
|
||||
$menu['menu800'][] = array('800300', '시스템 설정', G5_ADMIN_URL . '/order_manage/config_manager.php', 'order_config');
|
||||
$menu['menu800'][] = array('800400', '전문가 방문', G5_ADMIN_URL . '/order_manage/expert_visits.php', 'expert_visits');
|
||||
$menu['menu800'][] = array('800500', '메일 템플릿', G5_ADMIN_URL . '/order_manage/mail_templates.php', 'mail_templates');
|
||||
$menu['menu800'][] = array('800600', 'SMS 템플릿', G5_ADMIN_URL . '/order_manage/sms_templates.php', 'sms_templates');
|
||||
$menu['menu800'][] = array('800700', '알림 로그 관리', G5_ADMIN_URL.'/order_manage/notification_log.php', 'notification_log');
|
||||
$menu['menu800'][] = array('800800', '전문가 방문 예약', G5_ADMIN_URL.'/order_manage/expert_visit_reservations.php', 'expert_visit_reservations');
|
||||
$menu['menu800'][] = array('800850', '전문가 방문 스케줄', G5_ADMIN_URL.'/order_manage/expert_visit_schedule.php', 'expert_visit_schedule');
|
||||
$menu['menu800'][] = array('800860', '윈도우 창호 관리', G5_ADMIN_URL.'/order_manage/brand_manager.php', 'brand_manager');
|
||||
$menu['menu800'][] = array('800900', '솔루션 설치', G5_ADMIN_URL . '/order_manage/install.php', 'order_solution_install');
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// Contact Inquiry 메뉴
|
||||
if (file_exists(__DIR__ . '/contact_inquiry/admin.menu810.contact.php')) {
|
||||
include_once(__DIR__ . '/contact_inquiry/admin.menu810.contact.php');
|
||||
} else if (file_exists(__DIR__ . '/admin.menu810.contact.php')) {
|
||||
include_once(__DIR__ . '/admin.menu810.contact.php');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 메뉴
|
||||
*/
|
||||
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// 메뉴 구조: array('메뉴코드', '메뉴명', '링크', '메뉴 ID', '아이콘 클래스 (옵션)')
|
||||
// 💡 [개선] 다른 모듈과의 호환성을 위해 배열을 덮어쓰지 않고 추가/병합합니다.
|
||||
if (!isset($menu['menu850'])) $menu['menu850'] = array();
|
||||
|
||||
$menu['menu850'] = array_merge($menu['menu850'], array(
|
||||
// 💡 [개선] 메뉴 그룹 대표 아이콘 추가 (예: fa-comments)
|
||||
array('850000', '상담관리', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_manage', 'fa-comments'),
|
||||
array('850100', '대시보드', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_dashboard'),
|
||||
array('850200', '예약 현황', G5_ADMIN_URL . '/consultant_manage/reservations.php', 'consultant_reservations'),
|
||||
array('850300', '빠른 스케줄 관리', G5_ADMIN_URL . '/consultant_manage/schedule_generate.php', 'consultant_schedule_quick'),
|
||||
array('850400', '통계 분석', G5_ADMIN_URL . '/consultant_manage/statistics.php', 'consultant_statistics'),
|
||||
// 💡 [개선] 설정 관련 메뉴를 '환경설정' 그룹으로 통합
|
||||
array('850500', '팝업 샘플', G5_ADMIN_URL . '/consultant_manage/sample_page.php', 'consultant_sample'),
|
||||
array('850600', '환경설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings_group', 'fa-cogs'),
|
||||
array('850610', '기본/운영 설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings'),
|
||||
array('850615', '리소스(상담사) 관리', G5_ADMIN_URL . '/consultant_manage/resources.php', 'consultant_resources'), // 💡 [추가] 리소스 관리 메뉴
|
||||
array('850620', '알림 템플릿', G5_ADMIN_URL . '/consultant_manage/templates.php', 'consultant_templates'),
|
||||
array('850630', '시스템 로그', G5_ADMIN_URL . '/consultant_manage/log_view.php', 'consultant_log_view'),
|
||||
array('850640', '설치/업데이트', G5_ADMIN_URL . '/consultant_manage/install.php', 'consultant_install'),
|
||||
// array('850900', '시스템 테스트', G5_ADMIN_URL . '/consultant_manage/test_system.php', 'consultant_test') // 개발용 메뉴는 주석 처리
|
||||
));
|
||||
?>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// Consultant Manage 메뉴
|
||||
if (file_exists(__DIR__ . '/admin.menu850.consultant_manage.php')) {
|
||||
include_once(__DIR__ . '/admin.menu850.consultant_manage.php');
|
||||
}
|
||||
|
||||
// Contact Inquiry 메뉴
|
||||
if (file_exists(__DIR__ . '/admin.menu850.contact.php')) {
|
||||
include_once(__DIR__ . '/admin.menu850.contact.php');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
$menu["menu900"] = array(
|
||||
array('900000', 'SMS 관리', '' . G5_SMS5_ADMIN_URL . '/config.php', 'sms5'),
|
||||
array('900100', 'SMS 기본설정', '' . G5_SMS5_ADMIN_URL . '/config.php', 'sms5_config'),
|
||||
array('900200', '회원정보업데이트', '' . G5_SMS5_ADMIN_URL . '/member_update.php', 'sms5_mb_update'),
|
||||
array('900300', '문자 보내기', '' . G5_SMS5_ADMIN_URL . '/sms_write.php', 'sms_write'),
|
||||
array('900400', '전송내역-건별', '' . G5_SMS5_ADMIN_URL . '/history_list.php', 'sms_history', 1),
|
||||
array('900410', '전송내역-번호별', '' . G5_SMS5_ADMIN_URL . '/history_num.php', 'sms_history_num', 1),
|
||||
array('900500', '이모티콘 그룹', '' . G5_SMS5_ADMIN_URL . '/form_group.php', 'emoticon_group'),
|
||||
array('900600', '이모티콘 관리', '' . G5_SMS5_ADMIN_URL . '/form_list.php', 'emoticon_list'),
|
||||
array('900700', '휴대폰번호 그룹', '' . G5_SMS5_ADMIN_URL . '/num_group.php', 'hp_group', 1),
|
||||
array('900800', '휴대폰번호 관리', '' . G5_SMS5_ADMIN_URL . '/num_book.php', 'hp_manage', 1),
|
||||
array('900900', '휴대폰번호 파일', '' . G5_SMS5_ADMIN_URL . '/num_book_file.php', 'hp_file', 1)
|
||||
);
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 그누보드5.4.5.5 버전과 영카트5.4.5.5.1 버전이 통합됨에 따라 그누보드 버전만 표시
|
||||
// $print_version = defined('G5_YOUNGCART_VER') ? 'YoungCart Version '.G5_YOUNGCART_VER : 'Version '.G5_GNUBOARD_VER;
|
||||
$print_version = ($is_admin == 'super') ? 'Version ' . G5_GNUBOARD_VER : '';
|
||||
?>
|
||||
|
||||
<noscript>
|
||||
<p>
|
||||
귀하께서 사용하시는 브라우저는 현재 <strong>자바스크립트를 사용하지 않음</strong>으로 설정되어 있습니다.<br>
|
||||
<strong>자바스크립트를 사용하지 않음</strong>으로 설정하신 경우는 수정이나 삭제시 별도의 경고창이 나오지 않으므로 이점 주의하시기 바랍니다.
|
||||
</p>
|
||||
</noscript>
|
||||
|
||||
</div>
|
||||
<footer id="ft">
|
||||
<p>
|
||||
Copyright © <?php echo $_SERVER['HTTP_HOST']; ?>. All rights reserved. <?php echo $print_version; ?><br>
|
||||
<button type="button" class="scroll_top"><span class="top_img"></span><span class="top_txt">TOP</span></button>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(".scroll_top").click(function() {
|
||||
$("body,html").animate({
|
||||
scrollTop: 0
|
||||
}, 400);
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- <p>실행시간 : <?php echo get_microtime() - $begin_time; ?> -->
|
||||
|
||||
<script src="<?php echo G5_ADMIN_URL ?>/admin.js?ver=<?php echo G5_JS_VER; ?>"></script>
|
||||
<script src="<?php echo G5_JS_URL ?>/jquery.anchorScroll.js?ver=<?php echo G5_JS_VER; ?>"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
|
||||
var admin_head_height = $("#hd_top").height() + $("#container_title").height() + 5;
|
||||
|
||||
$("a[href^='#']").anchorScroll({
|
||||
scrollSpeed: 0, // scroll speed
|
||||
offsetTop: admin_head_height, // offset for fixed top bars (defaults to 0)
|
||||
onScroll: function() {
|
||||
// callback on scroll start
|
||||
},
|
||||
scrollEnd: function() {
|
||||
// callback on scroll end
|
||||
}
|
||||
});
|
||||
|
||||
var hide_menu = false;
|
||||
var mouse_event = false;
|
||||
var oldX = oldY = 0;
|
||||
|
||||
$(document).mousemove(function(e) {
|
||||
if (oldX == 0) {
|
||||
oldX = e.pageX;
|
||||
oldY = e.pageY;
|
||||
}
|
||||
|
||||
if (oldX != e.pageX || oldY != e.pageY) {
|
||||
mouse_event = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 주메뉴
|
||||
var $gnb = $(".gnb_1dli > a");
|
||||
$gnb.mouseover(function() {
|
||||
if (mouse_event) {
|
||||
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
|
||||
$(this).parent().addClass("gnb_1dli_over gnb_1dli_on");
|
||||
menu_rearrange($(this).parent());
|
||||
hide_menu = false;
|
||||
}
|
||||
});
|
||||
|
||||
$gnb.mouseout(function() {
|
||||
hide_menu = true;
|
||||
});
|
||||
|
||||
$(".gnb_2dli").mouseover(function() {
|
||||
hide_menu = false;
|
||||
});
|
||||
|
||||
$(".gnb_2dli").mouseout(function() {
|
||||
hide_menu = true;
|
||||
});
|
||||
|
||||
$gnb.focusin(function() {
|
||||
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
|
||||
$(this).parent().addClass("gnb_1dli_over gnb_1dli_on");
|
||||
menu_rearrange($(this).parent());
|
||||
hide_menu = false;
|
||||
});
|
||||
|
||||
$gnb.focusout(function() {
|
||||
hide_menu = true;
|
||||
});
|
||||
|
||||
$(".gnb_2da").focusin(function() {
|
||||
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
|
||||
var $gnb_li = $(this).closest(".gnb_1dli").addClass("gnb_1dli_over gnb_1dli_on");
|
||||
menu_rearrange($(this).closest(".gnb_1dli"));
|
||||
hide_menu = false;
|
||||
});
|
||||
|
||||
$(".gnb_2da").focusout(function() {
|
||||
hide_menu = true;
|
||||
});
|
||||
|
||||
$('#gnb_1dul>li').bind('mouseleave', function() {
|
||||
submenu_hide();
|
||||
});
|
||||
|
||||
$(document).bind('click focusin', function() {
|
||||
if (hide_menu) {
|
||||
submenu_hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 폰트 리사이즈 쿠키있으면 실행
|
||||
var font_resize_act = get_cookie("ck_font_resize_act");
|
||||
if (font_resize_act != "") {
|
||||
font_resize("container", font_resize_act);
|
||||
}
|
||||
});
|
||||
|
||||
function submenu_hide() {
|
||||
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
|
||||
}
|
||||
|
||||
function menu_rearrange(el) {
|
||||
var width = $("#gnb_1dul").width();
|
||||
var left = w1 = w2 = 0;
|
||||
var idx = $(".gnb_1dli").index(el);
|
||||
|
||||
for (i = 0; i <= idx; i++) {
|
||||
w1 = $(".gnb_1dli:eq(" + i + ")").outerWidth();
|
||||
w2 = $(".gnb_2dli > a:eq(" + i + ")").outerWidth(true);
|
||||
|
||||
if ((left + w2) > width) {
|
||||
el.removeClass("gnb_1dli_over").addClass("gnb_1dli_over2");
|
||||
}
|
||||
|
||||
left += w1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once G5_PATH . '/tail.sub.php';
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
require_once './_common.php';
|
||||
|
||||
set_session('ss_admin_token', '');
|
||||
|
||||
$admin_csrf_token_key = isset($_POST['admin_csrf_token_key']) ? $_POST['admin_csrf_token_key'] : '';
|
||||
|
||||
if(function_exists('admin_csrf_token_key') && $admin_csrf_token_key !== admin_csrf_token_key(1)){
|
||||
die(json_encode(array('error' => '토큰키 에러!', 'url' => G5_URL)));
|
||||
}
|
||||
|
||||
$error = admin_referer_check(true);
|
||||
if ($error) {
|
||||
die(json_encode(array('error' => $error, 'url' => G5_URL)));
|
||||
}
|
||||
|
||||
$token = get_admin_token();
|
||||
|
||||
die(json_encode(array('error' => '', 'token' => $token, 'url' => '')));
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once './_common.php';
|
||||
|
||||
if (isset($_POST['admin_use_captcha'])) {
|
||||
set_session('ss_admin_use_captcha', true);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
$sub_menu = "100200";
|
||||
require_once './_common.php';
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
$sql_common = " from {$g5['auth_table']} a left join {$g5['member_table']} b on (a.mb_id=b.mb_id) ";
|
||||
|
||||
$sql_search = " where (1) ";
|
||||
if ($stx) {
|
||||
$sql_search .= " and ( ";
|
||||
switch ($sfl) {
|
||||
default:
|
||||
$sql_search .= " ({$sfl} like '%{$stx}%') ";
|
||||
break;
|
||||
}
|
||||
$sql_search .= " ) ";
|
||||
}
|
||||
|
||||
if (!$sst) {
|
||||
$sst = "a.mb_id, au_menu";
|
||||
$sod = "";
|
||||
}
|
||||
$sql_order = " order by $sst $sod ";
|
||||
|
||||
$sql = " select count(*) as cnt
|
||||
{$sql_common}
|
||||
{$sql_search}
|
||||
{$sql_order} ";
|
||||
$row = sql_fetch($sql);
|
||||
$total_count = $row['cnt'];
|
||||
|
||||
$rows = $config['cf_page_rows'];
|
||||
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
|
||||
if ($page < 1) {
|
||||
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
|
||||
}
|
||||
$from_record = ($page - 1) * $rows; // 시작 열을 구함
|
||||
|
||||
$sql = " select *
|
||||
{$sql_common}
|
||||
{$sql_search}
|
||||
{$sql_order}
|
||||
limit {$from_record}, {$rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall btn_ov02">전체목록</a>';
|
||||
|
||||
$g5['title'] = "관리권한설정";
|
||||
require_once './admin.head.php';
|
||||
|
||||
$colspan = 5;
|
||||
?>
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
<?php echo $listall ?>
|
||||
<span class="btn_ov01"><span class="ov_txt">설정된 관리권한</span><span class="ov_num"><?php echo number_format($total_count) ?>건</span></span>
|
||||
</div>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<input type="hidden" name="sfl" value="a.mb_id" id="sfl">
|
||||
|
||||
<label for="stx" class="sound_only">회원아이디<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" required class="required frm_input">
|
||||
<input type="submit" value="검색" id="fsearch_submit" class="btn_submit">
|
||||
|
||||
</form>
|
||||
|
||||
<form name="fauthlist" id="fauthlist" method="post" action="./auth_list_delete.php" onsubmit="return fauthlist_submit(this);">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>">
|
||||
<input type="hidden" name="token" value="">
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">현재 페이지 회원 전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col"><?php echo subject_sort_link('a.mb_id') ?>회원아이디</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('mb_nick') ?>닉네임</a></th>
|
||||
<th scope="col">메뉴</th>
|
||||
<th scope="col">권한</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$count = 0;
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$is_continue = false;
|
||||
// 회원아이디가 없는 메뉴는 삭제함
|
||||
if ($row['mb_id'] == '' && $row['mb_nick'] == '') {
|
||||
sql_query(" delete from {$g5['auth_table']} where au_menu = '{$row['au_menu']}' ");
|
||||
$is_continue = true;
|
||||
}
|
||||
|
||||
// 메뉴번호가 바뀌는 경우에 현재 없는 저장된 메뉴는 삭제함
|
||||
if (!isset($auth_menu[$row['au_menu']])) {
|
||||
sql_query(" delete from {$g5['auth_table']} where au_menu = '{$row['au_menu']}' ");
|
||||
$is_continue = true;
|
||||
}
|
||||
|
||||
if ($is_continue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mb_nick = get_sideview($row['mb_id'], $row['mb_nick'], $row['mb_email'], $row['mb_homepage']);
|
||||
|
||||
$bg = 'bg' . ($i % 2);
|
||||
?>
|
||||
<tr class="<?php echo $bg; ?>">
|
||||
<td class="td_chk">
|
||||
<input type="hidden" name="au_menu[<?php echo $i ?>]" value="<?php echo $row['au_menu'] ?>">
|
||||
<input type="hidden" name="mb_id[<?php echo $i ?>]" value="<?php echo $row['mb_id'] ?>">
|
||||
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo $row['mb_nick'] ?>님 권한</label>
|
||||
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
|
||||
</td>
|
||||
<td class="td_mbid"><a href="?sfl=a.mb_id&stx=<?php echo $row['mb_id'] ?>"><?php echo $row['mb_id'] ?></a></td>
|
||||
<td class="td_auth_mbnick"><?php echo $mb_nick ?></td>
|
||||
<td class="td_menu">
|
||||
<?php echo $row['au_menu'] ?>
|
||||
<?php echo $auth_menu[$row['au_menu']] ?>
|
||||
</td>
|
||||
<td class="td_auth"><?php echo $row['au_auth'] ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
$count++;
|
||||
}
|
||||
|
||||
if ($count == 0) {
|
||||
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_list01 btn_list">
|
||||
<input type="submit" name="act_button" value="선택삭제" onclick="document.pressed=this.value" class="btn btn_02">
|
||||
</div>
|
||||
|
||||
<?php
|
||||
//if (isset($stx))
|
||||
// echo '<script>document.fsearch.sfl.value = "'.$sfl.'";</script>'."\n";
|
||||
|
||||
if (strstr($sfl, 'mb_id')) {
|
||||
$mb_id = $stx;
|
||||
} else {
|
||||
$mb_id = '';
|
||||
}
|
||||
?>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
$pagelist = get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'] . '?' . $qstr . '&page=');
|
||||
echo $pagelist;
|
||||
?>
|
||||
|
||||
<form name="fauthlist2" id="fauthlist2" action="./auth_update.php" method="post" autocomplete="off" onsubmit="return fauth_add_submit(this);">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>">
|
||||
<input type="hidden" name="token" value="">
|
||||
|
||||
<section id="add_admin">
|
||||
<h2 class="h2_frm">관리권한 추가</h2>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
다음 양식에서 회원에게 관리권한을 부여하실 수 있습니다.<br>
|
||||
권한 <strong>r</strong>은 읽기권한, <strong>w</strong>는 쓰기권한, <strong>d</strong>는 삭제권한입니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="mb_id">회원아이디<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<strong id="msg_mb_id" class="msg_sound_only"></strong>
|
||||
<input type="text" name="mb_id" value="<?php echo $mb_id ?>" id="mb_id" required class="required frm_input">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="au_menu">접근가능메뉴<strong class="sound_only">필수</strong></label></th>
|
||||
<td>
|
||||
<select id="au_menu" name="au_menu" required class="required">
|
||||
<option value=''>선택하세요</option>
|
||||
<?php
|
||||
foreach ($auth_menu as $key => $value) {
|
||||
if (!(substr($key, -3) == '000' || $key == '-' || !$key)) {
|
||||
echo '<option value="' . $key . '">' . $key . ' ' . $value . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">권한지정</th>
|
||||
<td>
|
||||
<input type="checkbox" name="r" value="r" id="r" checked>
|
||||
<label for="r">r (읽기)</label>
|
||||
<input type="checkbox" name="w" value="w" id="w">
|
||||
<label for="w">w (쓰기)</label>
|
||||
<input type="checkbox" name="d" value="d" id="d">
|
||||
<label for="d">d (삭제)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">자동등록방지</th>
|
||||
<td>
|
||||
<?php
|
||||
require_once G5_CAPTCHA_PATH . '/captcha.lib.php';
|
||||
$captcha_html = captcha_html();
|
||||
$captcha_js = chk_captcha_js();
|
||||
echo $captcha_html;
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<input type="submit" value="추가" class="btn_submit btn">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function fauth_add_submit(f) {
|
||||
|
||||
<?php echo $captcha_js; // 캡챠 사용시 자바스크립트에서 입력된 캡챠를 검사함 ?>
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function fauthlist_submit(f) {
|
||||
if (!is_checked("chk[]")) {
|
||||
alert(document.pressed + " 하실 항목을 하나 이상 선택하세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.pressed == "선택삭제") {
|
||||
if (!confirm("선택한 자료를 정말 삭제하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
$sub_menu = "100200";
|
||||
require_once './_common.php';
|
||||
|
||||
check_demo();
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$count = (isset($_POST['chk']) && is_array($_POST['chk'])) ? count($_POST['chk']) : 0;
|
||||
$post_act_button = isset($_POST['act_button']) ? clean_xss_tags($_POST['act_button'], 1, 1) : '';
|
||||
|
||||
if (!$count) {
|
||||
alert($_POST['act_button'] . " 하실 항목을 하나 이상 체크하세요.");
|
||||
}
|
||||
|
||||
if ((isset($_POST['mb_id']) && !is_array($_POST['mb_id'])) || (isset($_POST['au_menu']) && !is_array($_POST['au_menu']))) {
|
||||
alert("잘못된 요청입니다.");
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
// 실제 번호를 넘김
|
||||
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
|
||||
|
||||
$mb_id = isset($_POST['mb_id'][$k]) ? preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['mb_id'][$k]) : '';
|
||||
$au_menu = isset($_POST['au_menu'][$k]) ? preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['au_menu'][$k]) : '';
|
||||
|
||||
$sql = " delete from {$g5['auth_table']} where mb_id = '" . $mb_id . "' and au_menu = '" . $au_menu . "' ";
|
||||
sql_query($sql);
|
||||
|
||||
run_event('adm_auth_delete_member', $mb_id, $au_menu);
|
||||
}
|
||||
|
||||
goto_url('./auth_list.php?' . $qstr);
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
$sub_menu = "100200";
|
||||
require_once './_common.php';
|
||||
require_once G5_LIB_PATH . '/mailer.lib.php';
|
||||
|
||||
$au_menu = isset($_POST['au_menu']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['au_menu']) : '';
|
||||
$post_r = isset($_POST['r']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['r']) : '';
|
||||
$post_w = isset($_POST['w']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['w']) : '';
|
||||
$post_d = isset($_POST['d']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['d']) : '';
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
$mb = get_member($mb_id);
|
||||
if (!$mb['mb_id']) {
|
||||
alert('존재하는 회원아이디가 아닙니다.');
|
||||
}
|
||||
|
||||
check_admin_token();
|
||||
|
||||
require_once G5_CAPTCHA_PATH . '/captcha.lib.php';
|
||||
|
||||
if (!chk_captcha()) {
|
||||
alert('자동등록방지 숫자가 틀렸습니다.');
|
||||
}
|
||||
|
||||
$sql = " insert into {$g5['auth_table']}
|
||||
set mb_id = '$mb_id',
|
||||
au_menu = '$au_menu',
|
||||
au_auth = '{$post_r},{$post_w},{$post_d}' ";
|
||||
$result = sql_query($sql, false);
|
||||
if (!$result) {
|
||||
$sql = " update {$g5['auth_table']}
|
||||
set au_auth = '{$post_r},{$post_w},{$post_d}'
|
||||
where mb_id = '$mb_id'
|
||||
and au_menu = '$au_menu' ";
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
//sql_query(" OPTIMIZE TABLE `$g5['auth_table']` ");
|
||||
|
||||
// 세션을 체크하여 하루에 한번만 메일알림이 가게 합니다.
|
||||
if (str_replace('-', '', G5_TIME_YMD) !== get_session('adm_auth_update')) {
|
||||
$site_url = preg_replace('/^www\./', '', strtolower($_SERVER['SERVER_NAME']));
|
||||
$to_email = 'gnuboard@' . $site_url;
|
||||
|
||||
mailer($config['cf_admin_email_name'], $to_email, $config['cf_admin_email'], '[' . $config['cf_title'] . '] 관리권한설정 알림', '<p><b>[' . $config['cf_title'] . '] 관리권한설정 변경 안내</b></p><p style="padding-top:1em">회원 아이디 ' . $mb['mb_id'] . ' 에 관리권한이 추가 되었습니다.</p><p style="padding-top:1em">' . G5_TIME_YMDHIS . '</p><p style="padding-top:1em"><a href="' . G5_URL . '" target="_blank">' . $config['cf_title'] . '</a></p>', 1);
|
||||
|
||||
set_session('adm_auth_update', str_replace('-', '', G5_TIME_YMD));
|
||||
}
|
||||
|
||||
run_event('adm_auth_update', $mb);
|
||||
|
||||
goto_url('./auth_list.php?' . $qstr);
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
$sub_menu = '000300';
|
||||
include_once('./_common.php');
|
||||
|
||||
auth_check_menu($auth, $sub_menu, "w");
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$bn_id = isset($_POST['bn_id']) ? (int)$_POST['bn_id'] : 0;
|
||||
$w = isset($_POST['w']) ? $_POST['w'] : '';
|
||||
|
||||
$sql_common = "
|
||||
bn_alt = '{$_POST['bn_alt']}',
|
||||
bn_url = '{$_POST['bn_url']}',
|
||||
bn_device = '{$_POST['bn_device']}',
|
||||
bn_position = '{$_POST['bn_position']}',
|
||||
bn_group = '{$_POST['bn_group']}',
|
||||
bn_border = '{$_POST['bn_border']}',
|
||||
bn_radius = '{$_POST['bn_radius']}',
|
||||
bn_ad_ico = '{$_POST['bn_ad_ico']}',
|
||||
bn_new_win = '{$_POST['bn_new_win']}',
|
||||
bn_begin_time = '{$_POST['bn_begin_time']}',
|
||||
bn_end_time = '{$_POST['bn_end_time']}',
|
||||
bn_order = '{$_POST['bn_order']}'
|
||||
";
|
||||
|
||||
if ($w == "") {
|
||||
$sql_common .= ", bn_time = '" . G5_TIME_YMDHIS . "' ";
|
||||
$sql = " INSERT INTO `rb_banner` SET {$sql_common} ";
|
||||
sql_query($sql);
|
||||
$bn_id = sql_insert_id();
|
||||
} else if ($w == "u") {
|
||||
$sql = " UPDATE `rb_banner` SET {$sql_common} WHERE bn_id = '{$bn_id}' ";
|
||||
sql_query($sql);
|
||||
} else {
|
||||
alert('제대로 된 값이 넘어오지 않았습니다.');
|
||||
}
|
||||
|
||||
// 이미지 업로드
|
||||
if (isset($_FILES['bn_bimg']) && $_FILES['bn_bimg']['name'] != '') {
|
||||
$bimg_dir = G5_DATA_PATH . '/banners';
|
||||
@mkdir($bimg_dir, G5_DIR_PERMISSION);
|
||||
@chmod($bimg_dir, G5_DIR_PERMISSION);
|
||||
|
||||
$dest_path = $bimg_dir . '/' . $bn_id;
|
||||
move_uploaded_file($_FILES['bn_bimg']['tmp_name'], $dest_path);
|
||||
chmod($dest_path, G5_FILE_PERMISSION);
|
||||
}
|
||||
|
||||
// 이미지 삭제
|
||||
if (isset($_POST['bn_bimg_del']) && $_POST['bn_bimg_del']) {
|
||||
@unlink(G5_DATA_PATH . '/banners/' . $bn_id);
|
||||
}
|
||||
|
||||
goto_url('./banner_list.php');
|
||||
?>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
include_once('./_common.php');
|
||||
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
// curl을 사용하여 이미지를 다운로드하는 함수
|
||||
function download_image_with_curl($url, $save_path) {
|
||||
$ch = curl_init($url);
|
||||
$fp = fopen($save_path, 'wb');
|
||||
|
||||
curl_setopt($ch, CURLOPT_FILE, $fp);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
// Referer 설정 (필요시)
|
||||
curl_setopt($ch, CURLOPT_REFERER, 'https://www.laser.or.kr/');
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
if ($result && $http_code == 200 && filesize($save_path) > 0) {
|
||||
return true;
|
||||
} else {
|
||||
@unlink($save_path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$banners = [
|
||||
['url' => 'http://www.lg.co.kr/', 'img' => 'foot-logo1.png', 'alt' => 'LG'],
|
||||
['url' => '#', 'img' => 'foot-logo2.png', 'alt' => 'Partner'],
|
||||
['url' => 'http://www.swhitech.com/main/', 'img' => 'foot-logo3.png', 'alt' => 'SWHitech'],
|
||||
['url' => 'http://www.hblaser.co.kr', 'img' => 'foot-logo4.png', 'alt' => 'HBLaser'],
|
||||
['url' => 'http://www.lasersystem.co.kr/', 'img' => 'foot-logo5.png', 'alt' => 'LaserSystem'],
|
||||
['url' => 'http://www.eotechnics.com/page/main/main.php', 'img' => 'foot-logo6.png', 'alt' => 'EO Technics'],
|
||||
['url' => 'https://www.lpkf.com/de/', 'img' => 'foot-logo7.jpg', 'alt' => 'LPKF'],
|
||||
['url' => 'https://amplitude-laser.com/', 'img' => 'foot-logo8.png', 'alt' => 'Amplitude'],
|
||||
['url' => 'http://www.mutechkorea.co.kr/', 'img' => 'foot-logo9.png', 'alt' => 'Mutech'],
|
||||
['url' => 'http://www.ainnotech.com/', 'img' => 'foot-logo10.png', 'alt' => 'Ainnotech'],
|
||||
['url' => 'https://www.edmundoptics.co.kr/', 'img' => 'foot-logo11.png', 'alt' => 'Edmund Optics'],
|
||||
['url' => 'https://www.coherent.com/', 'img' => 'foot-logo12_new.png', 'alt' => 'Coherent'],
|
||||
['url' => 'https://www.mksinst.com/', 'img' => 'foot-logo13.png', 'alt' => 'MKS'],
|
||||
['url' => 'http://daekhon.co.kr/', 'img' => 'foot-logo14.png', 'alt' => 'Daekhon'],
|
||||
['url' => 'https://lightrun.co.kr/', 'img' => 'foot-logo15.png', 'alt' => 'Lightrun'],
|
||||
['url' => 'https://www.uniotech.kr/', 'img' => 'foot-logo16.png', 'alt' => 'Uniotech'],
|
||||
['url' => 'http://www.hls-scansonic.kr/main/index.html', 'img' => 'foot-logo17.png', 'alt' => 'HLS'],
|
||||
['url' => 'http://coslaser.co.kr/', 'img' => 'Wooyang_Logo.png', 'alt' => 'Wooyang'],
|
||||
['url' => 'https://www.excelitas.com/', 'img' => 'foot-logo18.png', 'alt' => 'Excelitas'],
|
||||
['url' => 'http://www.evlaser.co.kr/', 'img' => 'foot-logo19.jpg', 'alt' => 'EV Laser'],
|
||||
['url' => 'https://www.precitec.com/kr/', 'img' => 'foot-logo20.jpg', 'alt' => 'Precitec'],
|
||||
['url' => 'https://www.amadaweldtech.co.kr/', 'img' => 'foot-logo21.png', 'alt' => 'Amada']
|
||||
];
|
||||
|
||||
// 💡 [핵심 수정] 이미지 경로에 /KOR/ 추가
|
||||
$base_img_url = 'https://www.laser.or.kr/KOR/images/comm/';
|
||||
$save_path = G5_DATA_PATH . '/banners/';
|
||||
|
||||
if (!is_dir($save_path)) {
|
||||
@mkdir($save_path, G5_DIR_PERMISSION);
|
||||
@chmod($save_path, G5_DIR_PERMISSION);
|
||||
}
|
||||
|
||||
echo "<h1>배너 자동 등록 시작 (경로 수정됨)</h1>";
|
||||
echo "<ul>";
|
||||
|
||||
$success_count = 0;
|
||||
|
||||
foreach ($banners as $idx => $item) {
|
||||
$row = sql_fetch(" SELECT bn_id FROM rb_banner WHERE bn_alt = '{$item['alt']}' AND bn_position = 'rolling_footer' ");
|
||||
|
||||
if ($row['bn_id']) {
|
||||
$bn_id = $row['bn_id'];
|
||||
echo "<li>[중복] ID: {$bn_id} - {$item['alt']} 이미 등록됨. 이미지 다운로드만 재시도합니다.</li>";
|
||||
} else {
|
||||
$sql = " INSERT INTO rb_banner
|
||||
SET bn_alt = '{$item['alt']}',
|
||||
bn_url = '{$item['url']}',
|
||||
bn_device = 'both',
|
||||
bn_position = 'rolling_footer',
|
||||
bn_border = '0',
|
||||
bn_new_win = '1',
|
||||
bn_begin_time = '" . date('Y-m-d H:i:s') . "',
|
||||
bn_end_time = '" . date('Y-m-d H:i:s', strtotime('+10 years')) . "',
|
||||
bn_time = '" . date('Y-m-d H:i:s') . "',
|
||||
bn_hit = 0,
|
||||
bn_order = " . ($idx + 1);
|
||||
|
||||
sql_query($sql);
|
||||
$bn_id = sql_insert_id();
|
||||
}
|
||||
|
||||
if ($bn_id) {
|
||||
$remote_img = $base_img_url . $item['img'];
|
||||
$local_img = $save_path . $bn_id;
|
||||
|
||||
if (download_image_with_curl($remote_img, $local_img)) {
|
||||
echo "<li>[성공] ID: {$bn_id} - {$item['alt']} 이미지 저장 완료</li>";
|
||||
$success_count++;
|
||||
} else {
|
||||
echo "<li style='color:red;'>[실패] ID: {$bn_id} - 이미지 다운로드 실패 ({$remote_img})</li>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "</ul>";
|
||||
echo "<h2>총 {$success_count}개의 배너 이미지가 저장되었습니다.</h2>";
|
||||
echo "<p><a href='" . G5_ADMIN_URL . "/rb/banner_list.php'>[배너 관리 페이지로 이동]</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
$sub_menu = "300100";
|
||||
require_once "./_common.php";
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
$g5['title'] = '게시판 복사';
|
||||
require_once G5_PATH . '/head.sub.php';
|
||||
|
||||
if (empty($bo_table)) {
|
||||
alert_close("정상적인 방법으로 이용해주세요.");
|
||||
}
|
||||
?>
|
||||
<script>
|
||||
var g5_admin_csrf_token_key = "<?php echo (function_exists('admin_csrf_token_key')) ? admin_csrf_token_key() : ''; ?>";
|
||||
</script>
|
||||
<script src="<?php echo G5_ADMIN_URL ?>/admin.js?ver=<?php echo G5_JS_VER; ?>"></script>
|
||||
|
||||
<div class="new_win">
|
||||
<h1><?php echo $g5['title']; ?></h1>
|
||||
|
||||
<form name="fboardcopy" id="fboardcopy" action="./board_copy_update.php" onsubmit="return fboardcopy_check(this);" method="post">
|
||||
<input type="hidden" name="bo_table" value="<?php echo $bo_table ?>" id="bo_table">
|
||||
<input type="hidden" name="token" value="">
|
||||
<div class=" new_win_con">
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?></caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="col">원본 테이블명</th>
|
||||
<td><?php echo $bo_table ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col"><label for="target_table">복사 테이블명<strong class="sound_only">필수</strong></label></th>
|
||||
<td><input type="text" name="target_table" id="target_table" required class="required alnum_ frm_input" maxlength="20">영문자, 숫자, _ 만 가능 (공백없이)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col"><label for="target_subject">게시판 제목<strong class="sound_only">필수</strong></label></th>
|
||||
<td><input type="text" name="target_subject" value="[복사본] <?php echo get_sanitize_input($board['bo_subject']); ?>" id="target_subject" required class="required frm_input" maxlength="120"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">복사 유형</th>
|
||||
<td>
|
||||
<input type="radio" name="copy_case" value="schema_only" id="copy_case" checked>
|
||||
<label for="copy_case">구조만</label>
|
||||
<input type="radio" name="copy_case" value="schema_data_both" id="copy_case2">
|
||||
<label for="copy_case2">구조와 데이터</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="win_btn ">
|
||||
<input type="submit" class="btn_submit btn" value="복사">
|
||||
<input type="button" class="btn_close btn" value="창닫기" onclick="window.close();">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fboardcopy_check(f) {
|
||||
<?php
|
||||
|
||||
if (!$w) {
|
||||
$js_array = get_bo_table_banned_word();
|
||||
echo "var banned_array = " . json_encode($js_array) . ";\n";
|
||||
}
|
||||
?>
|
||||
|
||||
// 게시판명이 금지된 단어로 되어 있으면
|
||||
if ((typeof banned_array != 'undefined') && jQuery.inArray(f.target_table.value, banned_array) !== -1) {
|
||||
alert("입력한 게시판 TABLE명을 사용할수 없습니다. 다른 이름으로 입력해 주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (f.bo_table.value == f.target_table.value) {
|
||||
alert("원본 테이블명과 복사할 테이블명이 달라야 합니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<?php
|
||||
require_once G5_PATH . '/tail.sub.php';
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
$sub_menu = '300100';
|
||||
require_once './_common.php';
|
||||
|
||||
check_demo();
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$bo_table = isset($_POST['bo_table']) ? substr(preg_replace('/[^a-z0-9_]/i', '', $_POST['bo_table']), 0, 20) : null;
|
||||
$target_table = isset($_POST['target_table']) ? trim($_POST['target_table']) : '';
|
||||
$target_subject = isset($_POST['target_subject']) ? trim($_POST['target_subject']) : '';
|
||||
|
||||
$target_subject = strip_tags(clean_xss_attributes($target_subject));
|
||||
|
||||
$file_copy = array();
|
||||
|
||||
if (empty($bo_table)) {
|
||||
alert("원본 테이블 정보가 없습니다.");
|
||||
}
|
||||
|
||||
if (!preg_match('/[A-Za-z0-9_]{1,20}/', $target_table)) {
|
||||
alert('게시판 TABLE명은 공백없이 영문자, 숫자, _ 만 사용 가능합니다. (20자 이내)');
|
||||
}
|
||||
|
||||
$target_table = substr(preg_replace('/[^a-z0-9_]/i', '', $target_table), 0, 20);
|
||||
|
||||
// 게시판명이 금지된 단어로 되어 있으면
|
||||
if ($w == '' && in_array($target_table, get_bo_table_banned_word())) {
|
||||
alert('입력한 게시판 TABLE명을 사용할수 없습니다. 다른 이름으로 입력해 주세요.');
|
||||
}
|
||||
|
||||
$row = sql_fetch(" select count(*) as cnt from {$g5['board_table']} where bo_table = '$target_table' ");
|
||||
if ($row['cnt']) {
|
||||
alert($target_table . '은(는) 이미 존재하는 게시판 테이블명 입니다.\\n복사할 테이블명으로 사용할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 게시판 테이블 생성
|
||||
$sql = get_table_define($g5['write_prefix'] . $bo_table);
|
||||
$sql = str_replace($g5['write_prefix'] . $bo_table, $g5['write_prefix'] . $target_table, $sql);
|
||||
sql_query($sql, false);
|
||||
|
||||
// 구조만 복사시에는 공지사항 번호는 복사하지 않는다.
|
||||
if ($copy_case == 'schema_only') {
|
||||
$board['bo_notice'] = '';
|
||||
}
|
||||
|
||||
// 게시판 정보
|
||||
$sql = " insert into {$g5['board_table']}
|
||||
set bo_table = '$target_table',
|
||||
gr_id = '{$board['gr_id']}',
|
||||
bo_subject = '$target_subject',
|
||||
bo_device = '{$board['bo_device']}',
|
||||
bo_admin = '{$board['bo_admin']}',
|
||||
bo_list_level = '{$board['bo_list_level']}',
|
||||
bo_read_level = '{$board['bo_read_level']}',
|
||||
bo_write_level = '{$board['bo_write_level']}',
|
||||
bo_reply_level = '{$board['bo_reply_level']}',
|
||||
bo_comment_level = '{$board['bo_comment_level']}',
|
||||
bo_upload_level = '{$board['bo_upload_level']}',
|
||||
bo_download_level = '{$board['bo_download_level']}',
|
||||
bo_html_level = '{$board['bo_html_level']}',
|
||||
bo_link_level = '{$board['bo_link_level']}',
|
||||
bo_count_modify = '{$board['bo_count_modify']}',
|
||||
bo_count_delete = '{$board['bo_count_delete']}',
|
||||
bo_read_point = '{$board['bo_read_point']}',
|
||||
bo_write_point = '{$board['bo_write_point']}',
|
||||
bo_comment_point = '{$board['bo_comment_point']}',
|
||||
bo_download_point = '{$board['bo_download_point']}',
|
||||
bo_use_category = '{$board['bo_use_category']}',
|
||||
bo_category_list = '{$board['bo_category_list']}',
|
||||
bo_use_sideview = '{$board['bo_use_sideview']}',
|
||||
bo_use_file_content = '{$board['bo_use_file_content']}',
|
||||
bo_use_secret = '{$board['bo_use_secret']}',
|
||||
bo_use_dhtml_editor = '{$board['bo_use_dhtml_editor']}',
|
||||
bo_use_rss_view = '{$board['bo_use_rss_view']}',
|
||||
bo_use_good = '{$board['bo_use_good']}',
|
||||
bo_use_nogood = '{$board['bo_use_nogood']}',
|
||||
bo_use_name = '{$board['bo_use_name']}',
|
||||
bo_use_signature = '{$board['bo_use_signature']}',
|
||||
bo_use_ip_view = '{$board['bo_use_ip_view']}',
|
||||
bo_use_list_view = '{$board['bo_use_list_view']}',
|
||||
bo_use_list_content = '{$board['bo_use_list_content']}',
|
||||
bo_use_list_file = '{$board['bo_use_list_file']}',
|
||||
bo_table_width = '{$board['bo_table_width']}',
|
||||
bo_subject_len = '{$board['bo_subject_len']}',
|
||||
bo_mobile_subject_len = '{$board['bo_mobile_subject_len']}',
|
||||
bo_page_rows = '{$board['bo_page_rows']}',
|
||||
bo_mobile_page_rows = '{$board['bo_mobile_page_rows']}',
|
||||
bo_new = '{$board['bo_new']}',
|
||||
bo_hot = '{$board['bo_hot']}',
|
||||
bo_image_width = '{$board['bo_image_width']}',
|
||||
bo_skin = '" . sql_real_escape_string($board['bo_skin']). "',
|
||||
bo_mobile_skin = '" . sql_real_escape_string($board['bo_mobile_skin']). "',
|
||||
bo_include_head = '" . sql_real_escape_string($board['bo_include_head']). "',
|
||||
bo_include_tail = '" . sql_real_escape_string($board['bo_include_tail']). "',
|
||||
bo_content_head = '" . addslashes($board['bo_content_head']) . "',
|
||||
bo_content_tail = '" . addslashes($board['bo_content_tail']) . "',
|
||||
bo_mobile_content_head = '" . addslashes($board['bo_mobile_content_head']) . "',
|
||||
bo_mobile_content_tail = '" . addslashes($board['bo_mobile_content_tail']) . "',
|
||||
bo_insert_content = '" . addslashes($board['bo_insert_content']) . "',
|
||||
bo_gallery_cols = '{$board['bo_gallery_cols']}',
|
||||
bo_gallery_width = '{$board['bo_gallery_width']}',
|
||||
bo_gallery_height = '{$board['bo_gallery_height']}',
|
||||
bo_mobile_gallery_width = '{$board['bo_mobile_gallery_width']}',
|
||||
bo_mobile_gallery_height = '{$board['bo_mobile_gallery_height']}',
|
||||
bo_upload_size = '{$board['bo_upload_size']}',
|
||||
bo_reply_order = '{$board['bo_reply_order']}',
|
||||
bo_use_search = '{$board['bo_use_search']}',
|
||||
bo_order = '{$board['bo_order']}',
|
||||
bo_notice = '{$board['bo_notice']}',
|
||||
bo_upload_count = '{$board['bo_upload_count']}',
|
||||
bo_use_email = '{$board['bo_use_email']}',
|
||||
bo_use_cert = '{$board['bo_use_cert']}',
|
||||
bo_use_sns = '{$board['bo_use_sns']}',
|
||||
bo_use_captcha = '{$board['bo_use_captcha']}',
|
||||
bo_sort_field = '{$board['bo_sort_field']}',
|
||||
bo_1_subj = '" . addslashes($board['bo_1_subj']) . "',
|
||||
bo_2_subj = '" . addslashes($board['bo_2_subj']) . "',
|
||||
bo_3_subj = '" . addslashes($board['bo_3_subj']) . "',
|
||||
bo_4_subj = '" . addslashes($board['bo_4_subj']) . "',
|
||||
bo_5_subj = '" . addslashes($board['bo_5_subj']) . "',
|
||||
bo_6_subj = '" . addslashes($board['bo_6_subj']) . "',
|
||||
bo_7_subj = '" . addslashes($board['bo_7_subj']) . "',
|
||||
bo_8_subj = '" . addslashes($board['bo_8_subj']) . "',
|
||||
bo_9_subj = '" . addslashes($board['bo_9_subj']) . "',
|
||||
bo_10_subj = '" . addslashes($board['bo_10_subj']) . "',
|
||||
bo_1 = '" . addslashes($board['bo_1']) . "',
|
||||
bo_2 = '" . addslashes($board['bo_2']) . "',
|
||||
bo_3 = '" . addslashes($board['bo_3']) . "',
|
||||
bo_4 = '" . addslashes($board['bo_4']) . "',
|
||||
bo_5 = '" . addslashes($board['bo_5']) . "',
|
||||
bo_6 = '" . addslashes($board['bo_6']) . "',
|
||||
bo_7 = '" . addslashes($board['bo_7']) . "',
|
||||
bo_8 = '" . addslashes($board['bo_8']) . "',
|
||||
bo_9 = '" . addslashes($board['bo_9']) . "',
|
||||
bo_10 = '" . addslashes($board['bo_10']) . "' ";
|
||||
sql_query($sql, false);
|
||||
|
||||
// 게시판 폴더 생성
|
||||
@mkdir(G5_DATA_PATH . '/file/' . $target_table, G5_DIR_PERMISSION);
|
||||
@chmod(G5_DATA_PATH . '/file/' . $target_table, G5_DIR_PERMISSION);
|
||||
|
||||
// 디렉토리에 있는 파일의 목록을 보이지 않게 한다.
|
||||
$board_path = G5_DATA_PATH . '/file/' . $target_table;
|
||||
$file = $board_path . '/index.php';
|
||||
$f = @fopen($file, 'w');
|
||||
@fwrite($f, '');
|
||||
@fclose($f);
|
||||
@chmod($file, G5_FILE_PERMISSION);
|
||||
|
||||
$copy_file = 0;
|
||||
if ($copy_case == 'schema_data_both') {
|
||||
$d = dir(G5_DATA_PATH . '/file/' . $bo_table);
|
||||
while ($entry = $d->read()) {
|
||||
if ($entry == '.' || $entry == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 김선용 201007 :
|
||||
if (is_dir(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry)) {
|
||||
$dd = dir(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry);
|
||||
@mkdir(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry, G5_DIR_PERMISSION);
|
||||
@chmod(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry, G5_DIR_PERMISSION);
|
||||
while ($entry2 = $dd->read()) {
|
||||
if ($entry2 == '.' || $entry2 == '..') {
|
||||
continue;
|
||||
}
|
||||
@copy(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry . '/' . $entry2, G5_DATA_PATH . '/file/' . $target_table . '/' . $entry . '/' . $entry2);
|
||||
@chmod(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry . '/' . $entry2, G5_DIR_PERMISSION);
|
||||
$copy_file++;
|
||||
}
|
||||
$dd->close();
|
||||
} else {
|
||||
@copy(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry, G5_DATA_PATH . '/file/' . $target_table . '/' . $entry);
|
||||
@chmod(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry, G5_DIR_PERMISSION);
|
||||
$copy_file++;
|
||||
}
|
||||
}
|
||||
$d->close();
|
||||
|
||||
run_event('admin_board_copy_file', $bo_table, $target_table);
|
||||
|
||||
// 글복사
|
||||
$sql = " insert into {$g5['write_prefix']}$target_table select * from {$g5['write_prefix']}$bo_table ";
|
||||
sql_query($sql, false);
|
||||
|
||||
// 게시글수 저장
|
||||
$sql = " select bo_count_write, bo_count_comment from {$g5['board_table']} where bo_table = '$bo_table' ";
|
||||
$row = sql_fetch($sql);
|
||||
$sql = " update {$g5['board_table']} set bo_count_write = '{$row['bo_count_write']}', bo_count_comment = '{$row['bo_count_comment']}' where bo_table = '$target_table' ";
|
||||
sql_query($sql, false);
|
||||
|
||||
// 4.00.01
|
||||
$sql = " select * from {$g5['board_file_table']} where bo_table = '$bo_table' ";
|
||||
$result = sql_query($sql, false);
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$file_copy[$i] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($file_copy)) {
|
||||
for ($i = 0; $i < count($file_copy); $i++) {
|
||||
$file_copy[$i] = run_replace('admin_copy_update_file', $file_copy[$i], $file_copy[$i]['bf_file'], $bo_table, $target_table);
|
||||
|
||||
$sql = " insert into {$g5['board_file_table']}
|
||||
set bo_table = '$target_table',
|
||||
wr_id = '{$file_copy[$i]['wr_id']}',
|
||||
bf_no = '{$file_copy[$i]['bf_no']}',
|
||||
bf_source = '" . addslashes($file_copy[$i]['bf_source']) . "',
|
||||
bf_file = '{$file_copy[$i]['bf_file']}',
|
||||
bf_download = '{$file_copy[$i]['bf_download']}',
|
||||
bf_content = '" . addslashes($file_copy[$i]['bf_content']) . "',
|
||||
bf_fileurl = '" . addslashes($file_copy[$i]['bf_fileurl']) . "',
|
||||
bf_thumburl = '" . addslashes($file_copy[$i]['bf_thumburl']) . "',
|
||||
bf_storage = '" . addslashes($file_copy[$i]['bf_storage']) . "',
|
||||
bf_filesize = '{$file_copy[$i]['bf_filesize']}',
|
||||
bf_width = '{$file_copy[$i]['bf_width']}',
|
||||
bf_height = '{$file_copy[$i]['bf_height']}',
|
||||
bf_type = '{$file_copy[$i]['bf_type']}',
|
||||
bf_datetime = '{$file_copy[$i]['bf_datetime']}' ";
|
||||
|
||||
sql_query($sql, false);
|
||||
}
|
||||
}
|
||||
|
||||
delete_cache_latest($bo_table);
|
||||
delete_cache_latest($target_table);
|
||||
|
||||
echo "<script>opener.document.location.reload();</script>";
|
||||
|
||||
alert("복사에 성공 했습니다.", './board_copy.php?bo_table=' . $bo_table . '&' . $qstr);
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// board_delete.php , boardgroup_delete.php 에서 include 하는 파일
|
||||
|
||||
// 개별 페이지 접근 불가
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
exit;
|
||||
}
|
||||
if (!defined('_BOARD_DELETE_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// $tmp_bo_table 에는 $bo_table 값을 넘겨주어야 함
|
||||
if (!$tmp_bo_table) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 게시판 1개는 삭제 불가 (게시판 복사를 위해서)
|
||||
//$row = sql_fetch(" select count(*) as cnt from $g5['board_table'] ");
|
||||
//if ($row['cnt'] <= 1) { return; }
|
||||
|
||||
// 게시판 설정 삭제
|
||||
sql_query(" delete from {$g5['board_table']} where bo_table = '{$tmp_bo_table}' ");
|
||||
|
||||
// 최신글 삭제
|
||||
sql_query(" delete from {$g5['board_new_table']} where bo_table = '{$tmp_bo_table}' ");
|
||||
|
||||
// 스크랩 삭제
|
||||
sql_query(" delete from {$g5['scrap_table']} where bo_table = '{$tmp_bo_table}' ");
|
||||
|
||||
// 파일 삭제
|
||||
sql_query(" delete from {$g5['board_file_table']} where bo_table = '{$tmp_bo_table}' ");
|
||||
|
||||
// 게시판 테이블 DROP
|
||||
sql_query(" drop table {$g5['write_prefix']}{$tmp_bo_table} ", false);
|
||||
|
||||
// 좋아요 테이블에서 기록 삭제
|
||||
sql_query(" delete from {$g5['board_good_table']} where bo_table = '{$tmp_bo_table}' ");
|
||||
|
||||
delete_cache_latest($tmp_bo_table);
|
||||
|
||||
// 게시판 폴더 전체 삭제
|
||||
rm_rf(G5_DATA_PATH . '/file/' . $tmp_bo_table);
|
||||
+1537
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,554 @@
|
||||
<?php
|
||||
$sub_menu = "300100";
|
||||
include_once('./_common.php');
|
||||
|
||||
if ($w == 'u') {
|
||||
check_demo();
|
||||
}
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$gr_id = isset($_POST['gr_id']) ? preg_replace('/[^a-z0-9_]/i', '', (string)$_POST['gr_id']) : '';
|
||||
$bo_admin = isset($_POST['bo_admin']) ? preg_replace('/[^a-z0-9_\, \|\#]/i', '', $_POST['bo_admin']) : '';
|
||||
$bo_subject = isset($_POST['bo_subject']) ? strip_tags(clean_xss_attributes($_POST['bo_subject'])) : '';
|
||||
$bo_mobile_subject = isset($_POST['bo_mobile_subject']) ? strip_tags(clean_xss_attributes($_POST['bo_mobile_subject'])) : '';
|
||||
|
||||
if (!$gr_id) {
|
||||
alert('그룹 ID는 반드시 선택하세요.');
|
||||
}
|
||||
if (!$bo_table) {
|
||||
alert('게시판 TABLE명은 반드시 입력하세요.');
|
||||
}
|
||||
if (!preg_match("/^([A-Za-z0-9_]{1,20})$/", $bo_table)) {
|
||||
alert('게시판 TABLE명은 공백없이 영문자, 숫자, _ 만 사용 가능합니다. (20자 이내)');
|
||||
}
|
||||
if (!$bo_subject) {
|
||||
alert('게시판 제목을 입력하세요.');
|
||||
}
|
||||
|
||||
// 게시판명이 금지된 단어로 되어 있으면
|
||||
if ($w == '' && in_array($bo_table, get_bo_table_banned_word())) {
|
||||
alert('입력한 게시판 TABLE명을 사용할수 없습니다. 다른 이름으로 입력해 주세요.');
|
||||
}
|
||||
|
||||
$bo_include_head = isset($_POST['bo_include_head']) ? preg_replace(array("#[\\\]+$#", "#(<\?php|<\?)#i"), "", substr($_POST['bo_include_head'], 0, 255)) : '';
|
||||
$bo_include_tail = isset($_POST['bo_include_tail']) ? preg_replace(array("#[\\\]+$#", "#(<\?php|<\?)#i"), "", substr($_POST['bo_include_tail'], 0, 255)) : '';
|
||||
|
||||
// 관리자가 자동등록방지를 사용해야 할 경우
|
||||
if ($board && (isset($board['bo_include_head']) && $board['bo_include_head'] !== $bo_include_head || $board['bo_include_tail'] !== $bo_include_tail) && function_exists('get_admin_captcha_by') && get_admin_captcha_by()) {
|
||||
include_once(G5_CAPTCHA_PATH . '/captcha.lib.php');
|
||||
|
||||
if (!chk_captcha()) {
|
||||
alert('자동등록방지 숫자가 틀렸습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($file = $bo_include_head) {
|
||||
$file_ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
|
||||
if (!$file_ext || !in_array($file_ext, array('php', 'htm', 'html')) || !preg_match('/^.*\.(php|htm|html)$/i', $file)) {
|
||||
alert('상단 파일 경로의 확장자는 php, htm, html 만 허용합니다.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($file = $bo_include_tail) {
|
||||
$file_ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
|
||||
if (!$file_ext || !in_array($file_ext, array('php', 'htm', 'html')) || !preg_match('/^.*\.(php|htm|html)$/i', $file)) {
|
||||
alert('하단 파일 경로의 확장자는 php, htm, html 만 허용합니다.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_include_path_check($bo_include_head, 1)) {
|
||||
alert('상단 파일 경로에 포함시킬수 없는 문자열이 있습니다.');
|
||||
}
|
||||
|
||||
if (!is_include_path_check($bo_include_tail, 1)) {
|
||||
alert('하단 파일 경로에 포함시킬수 없는 문자열이 있습니다.');
|
||||
}
|
||||
|
||||
if (function_exists('filter_input_include_path')) {
|
||||
$bo_include_head = filter_input_include_path($bo_include_head);
|
||||
$bo_include_tail = filter_input_include_path($bo_include_tail);
|
||||
}
|
||||
|
||||
$board_path = G5_DATA_PATH . '/file/' . $bo_table;
|
||||
|
||||
// 게시판 디렉토리 생성
|
||||
@mkdir($board_path, G5_DIR_PERMISSION);
|
||||
@chmod($board_path, G5_DIR_PERMISSION);
|
||||
|
||||
// 디렉토리에 있는 파일의 목록을 보이지 않게 한다.
|
||||
$file = $board_path . '/index.php';
|
||||
if ($f = @fopen($file, 'w')) {
|
||||
@fwrite($f, '');
|
||||
@fclose($f);
|
||||
@chmod($file, G5_FILE_PERMISSION);
|
||||
}
|
||||
|
||||
// 분류에 & 나 = 는 사용이 불가하므로 2바이트로 바꾼다.
|
||||
$src_char = array('&', '=');
|
||||
$dst_char = array('&', '〓');
|
||||
$bo_category_list = isset($_POST['bo_category_list']) ? str_replace($src_char, $dst_char, $_POST['bo_category_list']) : '';
|
||||
//https://github.com/gnuboard/gnuboard5/commit/f5f4925d4eb28ba1af728e1065fc2bdd9ce1da58 에 따른 조치
|
||||
$str_bo_category_list = preg_replace("/[\<\>\'\"\\\'\\\"\%\=\(\)\/\^\*]/", "", (string)$bo_category_list);
|
||||
|
||||
$bo_use_category = isset($_POST['bo_use_category']) ? (int) $_POST['bo_use_category'] : 0;
|
||||
$bo_use_sideview = isset($_POST['bo_use_sideview']) ? (int) $_POST['bo_use_sideview'] : 0;
|
||||
$bo_use_dhtml_editor = isset($_POST['bo_use_dhtml_editor']) ? (int) $_POST['bo_use_dhtml_editor'] : 0;
|
||||
$bo_use_good = isset($_POST['bo_use_good']) ? (int) $_POST['bo_use_good'] : 0;
|
||||
$bo_use_nogood = isset($_POST['bo_use_nogood']) ? (int) $_POST['bo_use_nogood'] : 0;
|
||||
$bo_use_name = isset($_POST['bo_use_name']) ? (int) $_POST['bo_use_name'] : 0;
|
||||
$bo_use_signature = isset($_POST['bo_use_signature']) ? (int) $_POST['bo_use_signature'] : 0;
|
||||
$bo_use_ip_view = isset($_POST['bo_use_ip_view']) ? (int) $_POST['bo_use_ip_view'] : 0;
|
||||
$bo_use_list_view = isset($_POST['bo_use_list_view']) ? (int) $_POST['bo_use_list_view'] : 0;
|
||||
$bo_use_list_file = isset($_POST['bo_use_list_file']) ? (int) $_POST['bo_use_list_file'] : 0;
|
||||
$bo_use_list_content = isset($_POST['bo_use_list_content']) ? (int) $_POST['bo_use_list_content'] : 0;
|
||||
$bo_use_email = isset($_POST['bo_use_email']) ? (int) $_POST['bo_use_email'] : 0;
|
||||
$bo_use_sns = isset($_POST['bo_use_sns']) ? (int) $_POST['bo_use_sns'] : 0;
|
||||
$bo_use_captcha = isset($_POST['bo_use_captcha']) ? (int) $_POST['bo_use_captcha'] : 0;
|
||||
$bo_table_width = isset($_POST['bo_table_width']) ? (int) $_POST['bo_table_width'] : 0;
|
||||
$bo_subject_len = isset($_POST['bo_subject_len']) ? (int) $_POST['bo_subject_len'] : 0;
|
||||
$bo_mobile_subject_len = isset($_POST['bo_mobile_subject_len']) ? (int) $_POST['bo_mobile_subject_len'] : 0;
|
||||
$bo_page_rows = isset($_POST['bo_page_rows']) ? (int) $_POST['bo_page_rows'] : 0;
|
||||
$bo_mobile_page_rows = isset($_POST['bo_mobile_page_rows']) ? (int) $_POST['bo_mobile_page_rows'] : 0;
|
||||
$bo_use_rss_view = isset($_POST['bo_use_rss_view']) ? (int) $_POST['bo_use_rss_view'] : 0;
|
||||
$bo_use_secret = isset($_POST['bo_use_secret']) ? (int) $_POST['bo_use_secret'] : 0;
|
||||
$bo_use_file_content = isset($_POST['bo_use_file_content']) ? (int) $_POST['bo_use_file_content'] : 0;
|
||||
$bo_new = isset($_POST['bo_new']) ? (int) $_POST['bo_new'] : 0;
|
||||
$bo_hot = isset($_POST['bo_hot']) ? (int) $_POST['bo_hot'] : 0;
|
||||
$bo_image_width = isset($_POST['bo_image_width']) ? (int) $_POST['bo_image_width'] : 0;
|
||||
$bo_use_search = isset($_POST['bo_use_search']) ? (int) $_POST['bo_use_search'] : 0;
|
||||
$bo_use_cert = isset($_POST['bo_use_cert']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['bo_use_cert']) : '';
|
||||
$bo_device = isset($_POST['bo_device']) ? clean_xss_tags($_POST['bo_device'], 1, 1) : '';
|
||||
$bo_list_level = isset($_POST['bo_list_level']) ? (int) $_POST['bo_list_level'] : 0;
|
||||
$bo_read_level = isset($_POST['bo_read_level']) ? (int) $_POST['bo_read_level'] : 0;
|
||||
$bo_write_level = isset($_POST['bo_write_level']) ? (int) $_POST['bo_write_level'] : 0;
|
||||
$bo_reply_level = isset($_POST['bo_reply_level']) ? (int) $_POST['bo_reply_level'] : 0;
|
||||
$bo_comment_level = isset($_POST['bo_comment_level']) ? (int) $_POST['bo_comment_level'] : 0;
|
||||
$bo_html_level = isset($_POST['bo_html_level']) ? (int) $_POST['bo_html_level'] : 0;
|
||||
$bo_link_level = isset($_POST['bo_link_level']) ? (int) $_POST['bo_link_level'] : 0;
|
||||
$bo_count_modify = isset($_POST['bo_count_modify']) ? (int) $_POST['bo_count_modify'] : 0;
|
||||
$bo_count_delete = isset($_POST['bo_count_delete']) ? (int) $_POST['bo_count_delete'] : 0;
|
||||
$bo_upload_level = isset($_POST['bo_upload_level']) ? (int) $_POST['bo_upload_level'] : 0;
|
||||
$bo_download_level = isset($_POST['bo_download_level']) ? (int) $_POST['bo_download_level'] : 0;
|
||||
$bo_read_point = isset($_POST['bo_read_point']) ? (int) $_POST['bo_read_point'] : 0;
|
||||
$bo_write_point = isset($_POST['bo_write_point']) ? (int) $_POST['bo_write_point'] : 0;
|
||||
$bo_comment_point = isset($_POST['bo_comment_point']) ? (int) $_POST['bo_comment_point'] : 0;
|
||||
$bo_download_point = isset($_POST['bo_download_point']) ? (int) $_POST['bo_download_point'] : 0;
|
||||
$bo_select_editor = isset($_POST['bo_select_editor']) ? clean_xss_tags($_POST['bo_select_editor'], 1, 1) : '';
|
||||
$bo_skin = isset($_POST['bo_skin']) ? clean_xss_tags($_POST['bo_skin'], 1, 1) : '';
|
||||
$bo_mobile_skin = isset($_POST['bo_mobile_skin']) ? clean_xss_tags($_POST['bo_mobile_skin'], 1, 1) : '';
|
||||
$bo_content_head = isset($_POST['bo_content_head']) ? $_POST['bo_content_head'] : '';
|
||||
$bo_content_tail = isset($_POST['bo_content_tail']) ? $_POST['bo_content_tail'] : '';
|
||||
$bo_mobile_content_head = isset($_POST['bo_mobile_content_head']) ? $_POST['bo_mobile_content_head'] : '';
|
||||
$bo_mobile_content_tail = isset($_POST['bo_mobile_content_tail']) ? $_POST['bo_mobile_content_tail'] : '';
|
||||
$bo_insert_content = isset($_POST['bo_insert_content']) ? $_POST['bo_insert_content'] : '';
|
||||
$bo_gallery_cols = isset($_POST['bo_gallery_cols']) ? (int) $_POST['bo_gallery_cols'] : 0;
|
||||
$bo_gallery_width = isset($_POST['bo_gallery_width']) ? (int) $_POST['bo_gallery_width'] : 0;
|
||||
$bo_gallery_height = isset($_POST['bo_gallery_height']) ? (int) $_POST['bo_gallery_height'] : 0;
|
||||
$bo_mobile_gallery_width = isset($_POST['bo_mobile_gallery_width']) ? (int) $_POST['bo_mobile_gallery_width'] : 0;
|
||||
$bo_mobile_gallery_height = isset($_POST['bo_mobile_gallery_height']) ? (int) $_POST['bo_mobile_gallery_height'] : 0;
|
||||
$bo_upload_count = isset($_POST['bo_upload_count']) ? (int) $_POST['bo_upload_count'] : 0;
|
||||
$bo_upload_size = isset($_POST['bo_upload_size']) ? (int) $_POST['bo_upload_size'] : 0;
|
||||
$bo_reply_order = isset($_POST['bo_reply_order']) ? (int) $_POST['bo_reply_order'] : 0;
|
||||
$bo_order = isset($_POST['bo_order']) ? (int) $_POST['bo_order'] : 0;
|
||||
$bo_write_min = isset($_POST['bo_write_min']) ? (int) $_POST['bo_write_min'] : 0;
|
||||
$bo_write_max = isset($_POST['bo_write_max']) ? (int) $_POST['bo_write_max'] : 0;
|
||||
$bo_comment_min = isset($_POST['bo_comment_min']) ? (int) $_POST['bo_comment_min'] : 0;
|
||||
$bo_comment_max = isset($_POST['bo_comment_max']) ? (int) $_POST['bo_comment_max'] : 0;
|
||||
$bo_sort_field = isset($_POST['bo_sort_field']) ? clean_xss_tags($_POST['bo_sort_field'], 1, 1) : '';
|
||||
// 💡 [추가] 예약 중복 검사 설정값
|
||||
$bo_use_reservation_overlap = isset($_POST['bo_use_reservation_overlap']) ? (int) $_POST['bo_use_reservation_overlap'] : 0;
|
||||
$bo_reservation_overlap_count = isset($_POST['bo_reservation_overlap_count']) ? (int) $_POST['bo_reservation_overlap_count'] : 0;
|
||||
|
||||
if (strpbrk($bo_skin.$bo_mobile_skin, "?%*:|\"<>") !== false) {
|
||||
alert('스킨 디렉토리명 오류!');
|
||||
}
|
||||
|
||||
$etcs = array();
|
||||
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$etcs['bo_' . $i . '_subj'] = ${'bo_' . $i . '_subj'} = isset($_POST['bo_' . $i . '_subj']) ? $_POST['bo_' . $i . '_subj'] : '';
|
||||
$etcs['bo_' . $i] = ${'bo_' . $i} = isset($_POST['bo_' . $i]) ? $_POST['bo_' . $i] : '';
|
||||
}
|
||||
|
||||
$sql_common = " gr_id = '{$gr_id}',
|
||||
bo_subject = '{$bo_subject}',
|
||||
bo_mobile_subject = '{$bo_mobile_subject}',
|
||||
bo_device = '{$bo_device}',
|
||||
bo_admin = '{$bo_admin}',
|
||||
bo_list_level = '{$bo_list_level}',
|
||||
bo_read_level = '{$bo_read_level}',
|
||||
bo_write_level = '{$bo_write_level}',
|
||||
bo_reply_level = '{$bo_reply_level}',
|
||||
bo_comment_level = '{$bo_comment_level}',
|
||||
bo_html_level = '{$bo_html_level}',
|
||||
bo_link_level = '{$bo_link_level}',
|
||||
bo_count_modify = '{$bo_count_modify}',
|
||||
bo_count_delete = '{$bo_count_delete}',
|
||||
bo_upload_level = '{$bo_upload_level}',
|
||||
bo_download_level = '{$bo_download_level}',
|
||||
bo_read_point = '{$bo_read_point}',
|
||||
bo_write_point = '{$bo_write_point}',
|
||||
bo_comment_point = '{$bo_comment_point}',
|
||||
bo_download_point = '{$bo_download_point}',
|
||||
bo_use_category = '{$bo_use_category}',
|
||||
bo_category_list = '{$str_bo_category_list}',
|
||||
bo_use_sideview = '{$bo_use_sideview}',
|
||||
bo_use_file_content = '{$bo_use_file_content}',
|
||||
bo_use_secret = '{$bo_use_secret}',
|
||||
bo_use_dhtml_editor = '{$bo_use_dhtml_editor}',
|
||||
bo_select_editor = '{$bo_select_editor}',
|
||||
bo_use_rss_view = '{$bo_use_rss_view}',
|
||||
bo_use_good = '{$bo_use_good}',
|
||||
bo_use_nogood = '{$bo_use_nogood}',
|
||||
bo_use_name = '{$bo_use_name}',
|
||||
bo_use_signature = '{$bo_use_signature}',
|
||||
bo_use_ip_view = '{$bo_use_ip_view}',
|
||||
bo_use_list_view = '{$bo_use_list_view}',
|
||||
bo_use_list_file = '{$bo_use_list_file}',
|
||||
bo_use_list_content = '{$bo_use_list_content}',
|
||||
bo_use_email = '{$bo_use_email}',
|
||||
bo_use_cert = '{$bo_use_cert}',
|
||||
bo_use_sns = '{$bo_use_sns}',
|
||||
bo_use_captcha = '{$bo_use_captcha}',
|
||||
bo_table_width = '{$bo_table_width}',
|
||||
bo_subject_len = '{$bo_subject_len}',
|
||||
bo_mobile_subject_len = '{$bo_mobile_subject_len}',
|
||||
bo_page_rows = '{$bo_page_rows}',
|
||||
bo_mobile_page_rows = '{$bo_mobile_page_rows}',
|
||||
bo_new = '{$bo_new}',
|
||||
bo_hot = '{$bo_hot}',
|
||||
bo_image_width = '{$bo_image_width}',
|
||||
bo_skin = '{$bo_skin}',
|
||||
bo_mobile_skin = '{$bo_mobile_skin}',
|
||||
bo_use_reservation_overlap = '{$bo_use_reservation_overlap}',
|
||||
bo_reservation_overlap_count = '{$bo_reservation_overlap_count}',
|
||||
";
|
||||
|
||||
// 최고 관리자인 경우에만 수정가능
|
||||
if ($is_admin === 'super') {
|
||||
$sql_common .= " bo_include_head = '" . $bo_include_head . "',
|
||||
bo_include_tail = '" . $bo_include_tail . "',
|
||||
bo_content_head = '{$bo_content_head}',
|
||||
bo_content_tail = '{$bo_content_tail}',
|
||||
bo_mobile_content_head = '{$bo_mobile_content_head}',
|
||||
bo_mobile_content_tail = '{$bo_mobile_content_tail}',
|
||||
";
|
||||
}
|
||||
|
||||
$sql_common .= " bo_insert_content = '{$bo_insert_content}',
|
||||
bo_gallery_cols = '{$bo_gallery_cols}',
|
||||
bo_gallery_width = '{$bo_gallery_width}',
|
||||
bo_gallery_height = '{$bo_gallery_height}',
|
||||
bo_mobile_gallery_width = '{$bo_mobile_gallery_width}',
|
||||
bo_mobile_gallery_height= '{$bo_mobile_gallery_height}',
|
||||
bo_upload_count = '{$bo_upload_count}',
|
||||
bo_upload_size = '{$bo_upload_size}',
|
||||
bo_reply_order = '{$bo_reply_order}',
|
||||
bo_use_search = '{$bo_use_search}',
|
||||
bo_order = '{$bo_order}',
|
||||
bo_write_min = '{$bo_write_min}',
|
||||
bo_write_max = '{$bo_write_max}',
|
||||
bo_comment_min = '{$bo_comment_min}',
|
||||
bo_comment_max = '{$bo_comment_max}',
|
||||
bo_sort_field = '{$bo_sort_field}',
|
||||
bo_1_subj = '{$bo_1_subj}',
|
||||
bo_2_subj = '{$bo_2_subj}',
|
||||
bo_3_subj = '{$bo_3_subj}',
|
||||
bo_4_subj = '{$bo_4_subj}',
|
||||
bo_5_subj = '{$bo_5_subj}',
|
||||
bo_6_subj = '{$bo_6_subj}',
|
||||
bo_7_subj = '{$bo_7_subj}',
|
||||
bo_8_subj = '{$bo_8_subj}',
|
||||
bo_9_subj = '{$bo_9_subj}',
|
||||
bo_10_subj = '{$bo_10_subj}',
|
||||
bo_1 = '{$bo_1}',
|
||||
bo_2 = '{$bo_2}',
|
||||
bo_3 = '{$bo_3}',
|
||||
bo_4 = '{$bo_4}',
|
||||
bo_5 = '{$bo_5}',
|
||||
bo_6 = '{$bo_6}',
|
||||
bo_7 = '{$bo_7}',
|
||||
bo_8 = '{$bo_8}',
|
||||
bo_9 = '{$bo_9}',
|
||||
bo_10 = '{$bo_10}' ";
|
||||
|
||||
if ($w == '') {
|
||||
$row = sql_fetch(" select count(*) as cnt from {$g5['board_table']} where bo_table = '{$bo_table}' ");
|
||||
if ($row['cnt']) {
|
||||
alert($bo_table . ' 은(는) 이미 존재하는 TABLE 입니다.');
|
||||
}
|
||||
|
||||
$sql = " insert into {$g5['board_table']}
|
||||
set bo_table = '{$bo_table}',
|
||||
bo_count_write = '0',
|
||||
bo_count_comment = '0',
|
||||
$sql_common ";
|
||||
sql_query($sql);
|
||||
|
||||
// 게시판 테이블 생성
|
||||
$file = file('./sql_write.sql');
|
||||
$file = get_db_create_replace($file);
|
||||
|
||||
$sql = implode("\n", $file);
|
||||
|
||||
$create_table = $g5['write_prefix'] . $bo_table;
|
||||
|
||||
// sql_board.sql 파일의 테이블명을 변환
|
||||
$source = array('/__TABLE_NAME__/', '/;/');
|
||||
$target = array($create_table, '');
|
||||
$sql = preg_replace($source, $target, $sql);
|
||||
sql_query($sql, false);
|
||||
} elseif ($w == 'u') {
|
||||
// 게시판의 글 수
|
||||
$sql = " select count(*) as cnt from {$g5['write_prefix']}{$bo_table} where wr_is_comment = 0 ";
|
||||
$row = sql_fetch($sql);
|
||||
$bo_count_write = $row['cnt'];
|
||||
|
||||
// 게시판의 코멘트 수
|
||||
$sql = " select count(*) as cnt from {$g5['write_prefix']}{$bo_table} where wr_is_comment = 1 ";
|
||||
$row = sql_fetch($sql);
|
||||
$bo_count_comment = $row['cnt'];
|
||||
|
||||
// 글수 조정
|
||||
/*
|
||||
엔피씨님의 팁으로 교체합니다. 130308
|
||||
http://sir.kr/g5_tiptech/27207
|
||||
*/
|
||||
if (isset($_POST['proc_count'])) {
|
||||
// 원글을 얻습니다.
|
||||
//$sql = " select wr_id from {$g5['write_prefix']}{$bo_table} where wr_is_comment = 0 ";
|
||||
$sql = " select a.wr_id, (count(b.wr_parent) - 1) as cnt from {$g5['write_prefix']}{$bo_table} a, {$g5['write_prefix']}{$bo_table} b where a.wr_id=b.wr_parent and a.wr_is_comment=0 group by a.wr_id ";
|
||||
$result = sql_query($sql);
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
/*
|
||||
// 코멘트수를 얻습니다.
|
||||
$sql2 = " select count(*) as cnt from {$g5['write_prefix']}$bo_table where wr_parent = '{$row['wr_id']}' and wr_is_comment = 1 ";
|
||||
$row2 = sql_fetch($sql2);
|
||||
*/
|
||||
|
||||
sql_query(" update {$g5['write_prefix']}{$bo_table} set wr_comment = '{$row['cnt']}' where wr_id = '{$row['wr_id']}' ");
|
||||
}
|
||||
}
|
||||
|
||||
// 공지사항에는 등록되어 있지만 실제 존재하지 않는 글 아이디는 삭제합니다.
|
||||
$bo_notice = "";
|
||||
$lf = "";
|
||||
if ($board['bo_notice']) {
|
||||
$tmp_array = explode(",", $board['bo_notice']);
|
||||
for ($i = 0; $i < count($tmp_array); $i++) {
|
||||
$tmp_wr_id = trim($tmp_array[$i]);
|
||||
$row = sql_fetch(" select count(*) as cnt from {$g5['write_prefix']}{$bo_table} where wr_id = '{$tmp_wr_id}' ");
|
||||
if ($row['cnt']) {
|
||||
$bo_notice .= $lf . $tmp_wr_id;
|
||||
$lf = ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sql = " update {$g5['board_table']}
|
||||
set bo_notice = '{$bo_notice}',
|
||||
bo_count_write = '{$bo_count_write}',
|
||||
bo_count_comment = '{$bo_count_comment}',
|
||||
{$sql_common}
|
||||
where bo_table = '{$bo_table}' ";
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
|
||||
// 같은 그룹내 게시판 동일 옵션 적용
|
||||
$grp_fields = '';
|
||||
if (is_checked('chk_grp_device')) $grp_fields .= " , bo_device = '{$bo_device}' ";
|
||||
if (is_checked('chk_grp_admin')) $grp_fields .= " , bo_admin = '{$bo_admin}' ";
|
||||
if (is_checked('chk_grp_list_level')) $grp_fields .= " , bo_list_level = '{$bo_list_level}' ";
|
||||
if (is_checked('chk_grp_read_level')) $grp_fields .= " , bo_read_level = '{$bo_read_level}' ";
|
||||
if (is_checked('chk_grp_write_level')) $grp_fields .= " , bo_write_level = '{$bo_write_level}' ";
|
||||
if (is_checked('chk_grp_reply_level')) $grp_fields .= " , bo_reply_level = '{$bo_reply_level}' ";
|
||||
if (is_checked('chk_grp_comment_level')) $grp_fields .= " , bo_comment_level = '{$bo_comment_level}' ";
|
||||
if (is_checked('chk_grp_link_level')) $grp_fields .= " , bo_link_level = '{$bo_link_level}' ";
|
||||
if (is_checked('chk_grp_upload_level')) $grp_fields .= " , bo_upload_level = '{$bo_upload_level}' ";
|
||||
if (is_checked('chk_grp_download_level')) $grp_fields .= " , bo_download_level = '{$bo_download_level}' ";
|
||||
if (is_checked('chk_grp_html_level')) $grp_fields .= " , bo_html_level = '{$bo_html_level}' ";
|
||||
if (is_checked('chk_grp_count_modify')) $grp_fields .= " , bo_count_modify = '{$bo_count_modify}' ";
|
||||
if (is_checked('chk_grp_count_delete')) $grp_fields .= " , bo_count_delete = '{$bo_count_delete}' ";
|
||||
if (is_checked('chk_grp_read_point')) $grp_fields .= " , bo_read_point = '{$bo_read_point}' ";
|
||||
if (is_checked('chk_grp_write_point')) $grp_fields .= " , bo_write_point = '{$bo_write_point}' ";
|
||||
if (is_checked('chk_grp_comment_point')) $grp_fields .= " , bo_comment_point = '{$bo_comment_point}' ";
|
||||
if (is_checked('chk_grp_download_point')) $grp_fields .= " , bo_download_point = '{$bo_download_point}' ";
|
||||
if (is_checked('chk_grp_category_list')) {
|
||||
$grp_fields .= " , bo_category_list = '{$str_bo_category_list}' ";
|
||||
$grp_fields .= " , bo_use_category = '{$bo_use_category}' ";
|
||||
}
|
||||
if (is_checked('chk_grp_use_sideview')) $grp_fields .= " , bo_use_sideview = '{$bo_use_sideview}' ";
|
||||
if (is_checked('chk_grp_use_file_content')) $grp_fields .= " , bo_use_file_content = '{$bo_use_file_content}' ";
|
||||
if (is_checked('chk_grp_use_secret')) $grp_fields .= " , bo_use_secret = '{$bo_use_secret}' ";
|
||||
if (is_checked('chk_grp_use_dhtml_editor')) $grp_fields .= " , bo_use_dhtml_editor = '{$bo_use_dhtml_editor}' ";
|
||||
if (is_checked('chk_grp_select_editor')) $grp_fields .= " , bo_select_editor = '{$bo_select_editor}' ";
|
||||
if (is_checked('chk_grp_use_rss_view')) $grp_fields .= " , bo_use_rss_view = '{$bo_use_rss_view}' ";
|
||||
if (is_checked('chk_grp_use_good')) $grp_fields .= " , bo_use_good = '{$bo_use_good}' ";
|
||||
if (is_checked('chk_grp_use_nogood')) $grp_fields .= " , bo_use_nogood = '{$bo_use_nogood}' ";
|
||||
if (is_checked('chk_grp_use_name')) $grp_fields .= " , bo_use_name = '{$bo_use_name}' ";
|
||||
if (is_checked('chk_grp_use_signature')) $grp_fields .= " , bo_use_signature = '{$bo_use_signature}' ";
|
||||
if (is_checked('chk_grp_use_ip_view')) $grp_fields .= " , bo_use_ip_view = '{$bo_use_ip_view}' ";
|
||||
if (is_checked('chk_grp_use_list_view')) $grp_fields .= " , bo_use_list_view = '{$bo_use_list_view}' ";
|
||||
if (is_checked('chk_grp_use_list_file')) $grp_fields .= " , bo_use_list_file = '{$bo_use_list_file}' ";
|
||||
if (is_checked('chk_grp_use_list_content')) $grp_fields .= " , bo_use_list_content = '{$bo_use_list_content}' ";
|
||||
if (is_checked('chk_grp_use_email')) $grp_fields .= " , bo_use_email = '{$bo_use_email}' ";
|
||||
if (is_checked('chk_grp_use_cert')) $grp_fields .= " , bo_use_cert = '{$bo_use_cert}' ";
|
||||
if (is_checked('chk_grp_use_sns')) $grp_fields .= " , bo_use_sns = '{$bo_use_sns}' ";
|
||||
if (is_checked('chk_grp_use_captcha')) $grp_fields .= " , bo_use_captcha = '{$bo_use_captcha}' ";
|
||||
if (is_checked('chk_grp_skin')) $grp_fields .= " , bo_skin = '{$bo_skin}' ";
|
||||
if (is_checked('chk_grp_mobile_skin')) $grp_fields .= " , bo_mobile_skin = '{$bo_mobile_skin}' ";
|
||||
if (is_checked('chk_grp_gallery_cols')) $grp_fields .= " , bo_gallery_cols = '{$bo_gallery_cols}' ";
|
||||
if (is_checked('chk_grp_gallery_width')) $grp_fields .= " , bo_gallery_width = '{$bo_gallery_width}' ";
|
||||
if (is_checked('chk_grp_gallery_height')) $grp_fields .= " , bo_gallery_height = '{$bo_gallery_height}' ";
|
||||
if (is_checked('chk_grp_mobile_gallery_width')) $grp_fields .= " , bo_mobile_gallery_width = '{$bo_mobile_gallery_width}' ";
|
||||
if (is_checked('chk_grp_mobile_gallery_height'))$grp_fields .= " , bo_mobile_gallery_height = '{$bo_mobile_gallery_height}' ";
|
||||
if (is_checked('chk_grp_table_width')) $grp_fields .= " , bo_table_width = '{$bo_table_width}' ";
|
||||
if (is_checked('chk_grp_page_rows')) $grp_fields .= " , bo_page_rows = '{$bo_page_rows}' ";
|
||||
if (is_checked('chk_grp_mobile_page_rows')) $grp_fields .= " , bo_mobile_page_rows = '{$bo_mobile_page_rows}' ";
|
||||
if (is_checked('chk_grp_subject_len')) $grp_fields .= " , bo_subject_len = '{$bo_subject_len}' ";
|
||||
if (is_checked('chk_grp_mobile_subject_len')) $grp_fields .= " , bo_mobile_subject_len = '{$bo_mobile_subject_len}' ";
|
||||
if (is_checked('chk_grp_new')) $grp_fields .= " , bo_new = '{$bo_new}' ";
|
||||
if (is_checked('chk_grp_hot')) $grp_fields .= " , bo_hot = '{$bo_hot}' ";
|
||||
if (is_checked('chk_grp_image_width')) $grp_fields .= " , bo_image_width = '{$bo_image_width}' ";
|
||||
if (is_checked('chk_grp_reply_order')) $grp_fields .= " , bo_reply_order = '{$bo_reply_order}' ";
|
||||
if (is_checked('chk_grp_sort_field')) $grp_fields .= " , bo_sort_field = '{$bo_sort_field}' ";
|
||||
if (is_checked('chk_grp_write_min')) $grp_fields .= " , bo_write_min = '{$bo_write_min}' ";
|
||||
if (is_checked('chk_grp_write_max')) $grp_fields .= " , bo_write_max = '{$bo_write_max}' ";
|
||||
if (is_checked('chk_grp_comment_min')) $grp_fields .= " , bo_comment_min = '{$bo_comment_min}' ";
|
||||
if (is_checked('chk_grp_comment_max')) $grp_fields .= " , bo_comment_max = '{$bo_comment_max}' ";
|
||||
if (is_checked('chk_grp_upload_count')) $grp_fields .= " , bo_upload_count = '{$bo_upload_count}' ";
|
||||
if (is_checked('chk_grp_upload_size')) $grp_fields .= " , bo_upload_size = '{$bo_upload_size}' ";
|
||||
if (is_checked('chk_grp_use_reservation_overlap')) $grp_fields .= " , bo_use_reservation_overlap = '{$bo_use_reservation_overlap}' ";
|
||||
if (is_checked('chk_grp_reservation_overlap_count')) $grp_fields .= " , bo_reservation_overlap_count = '{$bo_reservation_overlap_count}' ";
|
||||
|
||||
//최고관리자만 수정가능
|
||||
if ($is_admin === 'super') {
|
||||
if (is_checked('chk_grp_include_head')) $grp_fields .= " , bo_include_head = '{$bo_include_head}' ";
|
||||
if (is_checked('chk_grp_include_tail')) $grp_fields .= " , bo_include_tail = '{$bo_include_tail}' ";
|
||||
if (is_checked('chk_grp_content_head')) $grp_fields .= " , bo_content_head = '{$bo_content_head}' ";
|
||||
if (is_checked('chk_grp_content_tail')) $grp_fields .= " , bo_content_tail = '{$bo_content_tail}' ";
|
||||
if (is_checked('chk_grp_mobile_content_head')) $grp_fields .= " , bo_mobile_content_head = '{$bo_mobile_content_head}' ";
|
||||
if (is_checked('chk_grp_mobile_content_tail')) $grp_fields .= " , bo_mobile_content_tail = '{$bo_mobile_content_tail}' ";
|
||||
}
|
||||
|
||||
if (is_checked('chk_grp_insert_content')) $grp_fields .= " , bo_insert_content = '{$bo_insert_content}' ";
|
||||
if (is_checked('chk_grp_use_search')) $grp_fields .= " , bo_use_search = '{$bo_use_search}' ";
|
||||
if (is_checked('chk_grp_order')) $grp_fields .= " , bo_order = '{$bo_order}' ";
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
if (is_checked('chk_grp_' . $i)) {
|
||||
$grp_fields .= " , bo_{$i}_subj = '" . $etcs['bo_' . $i . '_subj'] . "' ";
|
||||
$grp_fields .= " , bo_{$i} = '" . $etcs['bo_' . $i] . "' ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($grp_fields) {
|
||||
sql_query(" update {$g5['board_table']} set bo_table = bo_table {$grp_fields} where gr_id = '$gr_id' ");
|
||||
}
|
||||
|
||||
|
||||
// 모든 게시판 동일 옵션 적용
|
||||
$all_fields = '';
|
||||
if (is_checked('chk_all_device')) $all_fields .= " , bo_device = '{$bo_device}' ";
|
||||
if (is_checked('chk_all_admin')) $all_fields .= " , bo_admin = '{$bo_admin}' ";
|
||||
if (is_checked('chk_all_list_level')) $all_fields .= " , bo_list_level = '{$bo_list_level}' ";
|
||||
if (is_checked('chk_all_read_level')) $all_fields .= " , bo_read_level = '{$bo_read_level}' ";
|
||||
if (is_checked('chk_all_write_level')) $all_fields .= " , bo_write_level = '{$bo_write_level}' ";
|
||||
if (is_checked('chk_all_reply_level')) $all_fields .= " , bo_reply_level = '{$bo_reply_level}' ";
|
||||
if (is_checked('chk_all_comment_level')) $all_fields .= " , bo_comment_level = '{$bo_comment_level}' ";
|
||||
if (is_checked('chk_all_link_level')) $all_fields .= " , bo_link_level = '{$bo_link_level}' ";
|
||||
if (is_checked('chk_all_upload_level')) $all_fields .= " , bo_upload_level = '{$bo_upload_level}' ";
|
||||
if (is_checked('chk_all_download_level')) $all_fields .= " , bo_download_level = '{$bo_download_level}' ";
|
||||
if (is_checked('chk_all_html_level')) $all_fields .= " , bo_html_level = '{$bo_html_level}' ";
|
||||
if (is_checked('chk_all_count_modify')) $all_fields .= " , bo_count_modify = '{$bo_count_modify}' ";
|
||||
if (is_checked('chk_all_count_delete')) $all_fields .= " , bo_count_delete = '{$bo_count_delete}' ";
|
||||
if (is_checked('chk_all_read_point')) $all_fields .= " , bo_read_point = '{$bo_read_point}' ";
|
||||
if (is_checked('chk_all_write_point')) $all_fields .= " , bo_write_point = '{$bo_write_point}' ";
|
||||
if (is_checked('chk_all_comment_point')) $all_fields .= " , bo_comment_point = '{$bo_comment_point}' ";
|
||||
if (is_checked('chk_all_download_point')) $all_fields .= " , bo_download_point = '{$bo_download_point}' ";
|
||||
if (is_checked('chk_all_category_list')) {
|
||||
$all_fields .= " , bo_category_list = '{$str_bo_category_list}' ";
|
||||
$all_fields .= " , bo_use_category = '{$bo_use_category}' ";
|
||||
}
|
||||
if (is_checked('chk_all_use_sideview')) $all_fields .= " , bo_use_sideview = '{$bo_use_sideview}' ";
|
||||
if (is_checked('chk_all_use_file_content')) $all_fields .= " , bo_use_file_content = '{$bo_use_file_content}' ";
|
||||
if (is_checked('chk_all_use_secret')) $all_fields .= " , bo_use_secret = '{$bo_use_secret}' ";
|
||||
if (is_checked('chk_all_use_dhtml_editor')) $all_fields .= " , bo_use_dhtml_editor = '{$bo_use_dhtml_editor}' ";
|
||||
if (is_checked('chk_all_select_editor')) $all_fields .= " , bo_select_editor = '{$bo_select_editor}' ";
|
||||
if (is_checked('chk_all_use_rss_view')) $all_fields .= " , bo_use_rss_view = '{$bo_use_rss_view}' ";
|
||||
if (is_checked('chk_all_use_good')) $all_fields .= " , bo_use_good = '{$bo_use_good}' ";
|
||||
if (is_checked('chk_all_use_nogood')) $all_fields .= " , bo_use_nogood = '{$bo_use_nogood}' ";
|
||||
if (is_checked('chk_all_use_name')) $all_fields .= " , bo_use_name = '{$bo_use_name}' ";
|
||||
if (is_checked('chk_all_use_signature')) $all_fields .= " , bo_use_signature = '{$bo_use_signature}' ";
|
||||
if (is_checked('chk_all_use_ip_view')) $all_fields .= " , bo_use_ip_view = '{$bo_use_ip_view}' ";
|
||||
if (is_checked('chk_all_use_list_view')) $all_fields .= " , bo_use_list_view = '{$bo_use_list_view}' ";
|
||||
if (is_checked('chk_all_use_list_file')) $all_fields .= " , bo_use_list_file = '{$bo_use_list_file}' ";
|
||||
if (is_checked('chk_all_use_list_content')) $all_fields .= " , bo_use_list_content = '{$bo_use_list_content}' ";
|
||||
if (is_checked('chk_all_use_email')) $all_fields .= " , bo_use_email = '{$bo_use_email}' ";
|
||||
if (is_checked('chk_all_use_cert')) $all_fields .= " , bo_use_cert = '{$bo_use_cert}' ";
|
||||
if (is_checked('chk_all_use_sns')) $all_fields .= " , bo_use_sns = '{$bo_use_sns}' ";
|
||||
if (is_checked('chk_all_use_captcha')) $all_fields .= " , bo_use_captcha = '{$bo_use_captcha}' ";
|
||||
if (is_checked('chk_all_skin')) $all_fields .= " , bo_skin = '{$bo_skin}' ";
|
||||
if (is_checked('chk_all_mobile_skin')) $all_fields .= " , bo_mobile_skin = '{$bo_mobile_skin}' ";
|
||||
if (is_checked('chk_all_gallery_cols')) $all_fields .= " , bo_gallery_cols = '{$bo_gallery_cols}' ";
|
||||
if (is_checked('chk_all_gallery_width')) $all_fields .= " , bo_gallery_width = '{$bo_gallery_width}' ";
|
||||
if (is_checked('chk_all_gallery_height')) $all_fields .= " , bo_gallery_height = '{$bo_gallery_height}' ";
|
||||
if (is_checked('chk_all_mobile_gallery_width')) $all_fields .= " , bo_mobile_gallery_width = '{$bo_mobile_gallery_width}' ";
|
||||
if (is_checked('chk_all_mobile_gallery_height')) $all_fields .= " , bo_mobile_gallery_height = '{$bo_mobile_gallery_height}' ";
|
||||
if (is_checked('chk_all_table_width')) $all_fields .= " , bo_table_width = '{$bo_table_width}' ";
|
||||
if (is_checked('chk_all_page_rows')) $all_fields .= " , bo_page_rows = '{$bo_page_rows}' ";
|
||||
if (is_checked('chk_all_mobile_page_rows')) $all_fields .= " , bo_mobile_page_rows = '{$bo_mobile_page_rows}' ";
|
||||
if (is_checked('chk_all_subject_len')) $all_fields .= " , bo_subject_len = '{$bo_subject_len}' ";
|
||||
if (is_checked('chk_all_mobile_subject_len')) $all_fields .= " , bo_mobile_subject_len = '{$bo_mobile_subject_len}' ";
|
||||
if (is_checked('chk_all_new')) $all_fields .= " , bo_new = '{$bo_new}' ";
|
||||
if (is_checked('chk_all_hot')) $all_fields .= " , bo_hot = '{$bo_hot}' ";
|
||||
if (is_checked('chk_all_image_width')) $all_fields .= " , bo_image_width = '{$bo_image_width}' ";
|
||||
if (is_checked('chk_all_reply_order')) $all_fields .= " , bo_reply_order = '{$bo_reply_order}' ";
|
||||
if (is_checked('chk_all_sort_field')) $all_fields .= " , bo_sort_field = '{$bo_sort_field}' ";
|
||||
if (is_checked('chk_all_write_min')) $all_fields .= " , bo_write_min = '{$bo_write_min}' ";
|
||||
if (is_checked('chk_all_write_max')) $all_fields .= " , bo_write_max = '{$bo_write_max}' ";
|
||||
if (is_checked('chk_all_comment_min')) $all_fields .= " , bo_comment_min = '{$bo_comment_min}' ";
|
||||
if (is_checked('chk_all_comment_max')) $all_fields .= " , bo_comment_max = '{$bo_comment_max}' ";
|
||||
if (is_checked('chk_all_upload_count')) $all_fields .= " , bo_upload_count = '{$bo_upload_count}' ";
|
||||
if (is_checked('chk_all_upload_size')) $all_fields .= " , bo_upload_size = '{$bo_upload_size}' ";
|
||||
if (is_checked('chk_all_use_reservation_overlap')) $all_fields .= " , bo_use_reservation_overlap = '{$bo_use_reservation_overlap}' ";
|
||||
if (is_checked('chk_all_reservation_overlap_count')) $all_fields .= " , bo_reservation_overlap_count = '{$bo_reservation_overlap_count}' ";
|
||||
|
||||
//최고관리자만 수정가능
|
||||
if ($is_admin === 'super') {
|
||||
if (is_checked('chk_all_include_head')) $all_fields .= " , bo_include_head = '{$bo_include_head}' ";
|
||||
if (is_checked('chk_all_include_tail')) $all_fields .= " , bo_include_tail = '{$bo_include_tail}' ";
|
||||
if (is_checked('chk_all_content_head')) $all_fields .= " , bo_content_head = '{$bo_content_head}' ";
|
||||
if (is_checked('chk_all_content_tail')) $all_fields .= " , bo_content_tail = '{$bo_content_tail}' ";
|
||||
if (is_checked('chk_all_mobile_content_head')) $all_fields .= " , bo_mobile_content_head = '{$bo_mobile_content_head}' ";
|
||||
if (is_checked('chk_all_mobile_content_tail')) $all_fields .= " , bo_mobile_content_tail = '{$bo_mobile_content_tail}' ";
|
||||
}
|
||||
|
||||
if (is_checked('chk_all_insert_content')) $all_fields .= " , bo_insert_content = '{$bo_insert_content}' ";
|
||||
if (is_checked('chk_all_use_search')) $all_fields .= " , bo_use_search = '{$bo_use_search}' ";
|
||||
if (is_checked('chk_all_order')) $all_fields .= " , bo_order = '{$bo_order}' ";
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
if (is_checked('chk_all_' . $i)) {
|
||||
$all_fields .= " , bo_{$i}_subj = '" . $etcs['bo_' . $i . '_subj'] . "' ";
|
||||
$all_fields .= " , bo_{$i} = '" . $etcs['bo_' . $i] . "' ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_fields) {
|
||||
sql_query(" update {$g5['board_table']} set bo_table = bo_table {$all_fields} ");
|
||||
}
|
||||
|
||||
delete_cache_latest($bo_table);
|
||||
|
||||
if (function_exists('get_admin_captcha_by')) {
|
||||
get_admin_captcha_by('remove');
|
||||
}
|
||||
|
||||
run_event('admin_board_form_update', $bo_table, $w);
|
||||
|
||||
goto_url("./board_form.php?w=u&bo_table={$bo_table}&{$qstr}");
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
$sub_menu = "300100";
|
||||
require_once './_common.php';
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$sql_common = " from {$g5['board_table']} a ";
|
||||
$sql_search = " where (1) ";
|
||||
|
||||
if ($is_admin != "super") {
|
||||
$sql_common .= " , {$g5['group_table']} b ";
|
||||
$sql_search .= " and (a.gr_id = b.gr_id and b.gr_admin = '{$member['mb_id']}') ";
|
||||
}
|
||||
|
||||
if ($stx) {
|
||||
$sql_search .= " and ( ";
|
||||
switch ($sfl) {
|
||||
case "bo_table":
|
||||
$sql_search .= " ($sfl like '$stx%') ";
|
||||
break;
|
||||
case "a.gr_id":
|
||||
$sql_search .= " ($sfl = '$stx') ";
|
||||
break;
|
||||
default:
|
||||
$sql_search .= " ($sfl like '%$stx%') ";
|
||||
break;
|
||||
}
|
||||
$sql_search .= " ) ";
|
||||
}
|
||||
|
||||
if (!$sst) {
|
||||
$sst = "a.gr_id, a.bo_table";
|
||||
$sod = "asc";
|
||||
}
|
||||
$sql_order = " order by $sst $sod ";
|
||||
|
||||
$sql = " select count(*) as cnt {$sql_common} {$sql_search} {$sql_order} ";
|
||||
$row = sql_fetch($sql);
|
||||
$total_count = $row['cnt'];
|
||||
|
||||
$rows = $config['cf_page_rows'];
|
||||
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
|
||||
if ($page < 1) {
|
||||
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
|
||||
}
|
||||
$from_record = ($page - 1) * $rows; // 시작 열을 구함
|
||||
|
||||
$sql = " select * {$sql_common} {$sql_search} {$sql_order} limit {$from_record}, {$rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall">전체목록</a>';
|
||||
|
||||
$g5['title'] = '게시판관리';
|
||||
require_once './admin.head.php';
|
||||
|
||||
$colspan = 15;
|
||||
?>
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
<?php echo $listall ?>
|
||||
<span class="btn_ov01"><span class="ov_txt">생성된 게시판수</span><span class="ov_num"> <?php echo number_format($total_count) ?>개</span></span>
|
||||
</div>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="bo_table" <?php echo get_selected($sfl, "bo_table", true); ?>>TABLE</option>
|
||||
<option value="bo_subject" <?php echo get_selected($sfl, "bo_subject"); ?>>제목</option>
|
||||
<option value="a.gr_id" <?php echo get_selected($sfl, "a.gr_id"); ?>>그룹ID</option>
|
||||
</select>
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" required class="required frm_input">
|
||||
<input type="submit" value="검색" class="btn_submit">
|
||||
</form>
|
||||
|
||||
<form name="fboardlist" id="fboardlist" action="./board_list_update.php" onsubmit="return fboardlist_submit(this);" method="post">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>">
|
||||
<input type="hidden" name="token" value="<?php echo isset($token) ? $token : ''; ?>">
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">게시판 전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col"><?php echo subject_sort_link('a.gr_id') ?>그룹</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_table') ?>TABLE</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_skin', '', 'desc') ?>스킨</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_mobile_skin', '', 'desc') ?>모바일<br>스킨</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_subject') ?>제목</a></th>
|
||||
<th scope="col">읽기P<span class="sound_only">포인트</span></th>
|
||||
<th scope="col">쓰기P<span class="sound_only">포인트</span></th>
|
||||
<th scope="col">댓글P<span class="sound_only">포인트</span></th>
|
||||
<th scope="col">다운P<span class="sound_only">포인트</span></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_use_sns') ?>SNS<br>사용</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_use_search') ?>검색<br>사용</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('bo_order') ?>출력<br>순서</a></th>
|
||||
<th scope="col">접속기기</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$one_update = '<a href="./board_form.php?w=u&bo_table=' . $row['bo_table'] . '&' . $qstr . '" class="btn btn_03">수정</a>';
|
||||
$one_copy = '<a href="./board_copy.php?bo_table=' . $row['bo_table'] . '" class="board_copy btn btn_02" target="win_board_copy">복사</a>';
|
||||
|
||||
$bg = 'bg' . ($i % 2);
|
||||
?>
|
||||
|
||||
<tr class="<?php echo $bg; ?>">
|
||||
<td class="td_chk">
|
||||
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo get_text($row['bo_subject']) ?></label>
|
||||
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($is_admin == 'super') { ?>
|
||||
<?php echo get_group_select("gr_id[$i]", $row['gr_id']) ?>
|
||||
<?php } else { ?>
|
||||
<input type="hidden" name="gr_id[<?php echo $i ?>]" value="<?php echo $row['gr_id'] ?>"><?php echo $row['gr_subject'] ?>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" name="board_table[<?php echo $i ?>]" value="<?php echo $row['bo_table'] ?>">
|
||||
<a href="<?php echo get_pretty_url($row['bo_table']) ?>"><?php echo $row['bo_table'] ?></a>
|
||||
</td>
|
||||
<td>
|
||||
<label for="bo_skin_<?php echo $i; ?>" class="sound_only">스킨</label>
|
||||
<?php echo get_skin_select('board', 'bo_skin_' . $i, "bo_skin[$i]", $row['bo_skin']); ?>
|
||||
</td>
|
||||
<td>
|
||||
<label for="bo_mobile_skin_<?php echo $i; ?>" class="sound_only">모바일 스킨</label>
|
||||
<?php echo get_mobile_skin_select('board', 'bo_mobile_skin_' . $i, "bo_mobile_skin[$i]", $row['bo_mobile_skin']); ?>
|
||||
</td>
|
||||
<td>
|
||||
<label for="bo_subject_<?php echo $i; ?>" class="sound_only">게시판 제목<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="bo_subject[<?php echo $i ?>]" value="<?php echo get_text($row['bo_subject']) ?>" id="bo_subject_<?php echo $i ?>" required class="required tbl_input bo_subject full_input" size="10">
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_read_point_<?php echo $i; ?>" class="sound_only">읽기 포인트</label>
|
||||
<input type="text" name="bo_read_point[<?php echo $i ?>]" value="<?php echo $row['bo_read_point'] ?>" id="bo_read_point_<?php echo $i; ?>" class="tbl_input" size="2">
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_write_point_<?php echo $i; ?>" class="sound_only">쓰기 포인트</label>
|
||||
<input type="text" name="bo_write_point[<?php echo $i ?>]" value="<?php echo $row['bo_write_point'] ?>" id="bo_write_point_<?php echo $i; ?>" class="tbl_input" size="2">
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_comment_point_<?php echo $i; ?>" class="sound_only">댓글 포인트</label>
|
||||
<input type="text" name="bo_comment_point[<?php echo $i ?>]" value="<?php echo $row['bo_comment_point'] ?>" id="bo_comment_point_<?php echo $i; ?>" class="tbl_input" size="2">
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_download_point_<?php echo $i; ?>" class="sound_only">다운<br>포인트</label>
|
||||
<input type="text" name="bo_download_point[<?php echo $i ?>]" value="<?php echo $row['bo_download_point'] ?>" id="bo_download_point_<?php echo $i; ?>" class="tbl_input" size="2">
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_use_sns_<?php echo $i; ?>" class="sound_only">SNS<br>사용</label>
|
||||
<input type="checkbox" name="bo_use_sns[<?php echo $i ?>]" value="1" id="bo_use_sns_<?php echo $i ?>" <?php echo $row['bo_use_sns'] ? "checked" : "" ?>>
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_use_search_<?php echo $i; ?>" class="sound_only">검색<br>사용</label>
|
||||
<input type="checkbox" name="bo_use_search[<?php echo $i ?>]" value="1" id="bo_use_search_<?php echo $i ?>" <?php echo $row['bo_use_search'] ? "checked" : "" ?>>
|
||||
</td>
|
||||
<td class="td_numsmall">
|
||||
<label for="bo_order_<?php echo $i; ?>" class="sound_only">출력<br>순서</label>
|
||||
<input type="text" name="bo_order[<?php echo $i ?>]" value="<?php echo $row['bo_order'] ?>" id="bo_order_<?php echo $i ?>" class="tbl_input" size="2">
|
||||
</td>
|
||||
<td class="td_mngsmall">
|
||||
<label for="bo_device_<?php echo $i; ?>" class="sound_only">접속기기</label>
|
||||
<select name="bo_device[<?php echo $i ?>]" id="bo_device_<?php echo $i ?>">
|
||||
<option value="both" <?php echo get_selected($row['bo_device'], 'both', true); ?>>모두</option>
|
||||
<option value="pc" <?php echo get_selected($row['bo_device'], 'pc'); ?>>PC</option>
|
||||
<option value="mobile" <?php echo get_selected($row['bo_device'], 'mobile'); ?>>모바일</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="td_mng td_mng_m">
|
||||
<?php echo $one_update ?>
|
||||
<?php echo $one_copy ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_fixed_top">
|
||||
<input type="submit" name="act_button" value="선택수정" onclick="document.pressed=this.value" class="btn_02 btn">
|
||||
<?php if ($is_admin == 'super') { ?>
|
||||
<input type="submit" name="act_button" value="선택삭제" onclick="document.pressed=this.value" class="btn_02 btn">
|
||||
<a href="./board_form.php" id="bo_add" class="btn_01 btn">게시판 추가</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'] . '?' . $qstr . '&page='); ?>
|
||||
|
||||
<script>
|
||||
function fboardlist_submit(f) {
|
||||
if (!is_checked("chk[]")) {
|
||||
alert(document.pressed + " 하실 항목을 하나 이상 선택하세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.pressed == "선택삭제") {
|
||||
if (!confirm("선택한 자료를 정말 삭제하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$(".board_copy").click(function() {
|
||||
window.open(this.href, "win_board_copy", "left=100,top=100,width=550,height=450");
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
$sub_menu = "300100";
|
||||
require_once './_common.php';
|
||||
|
||||
check_demo();
|
||||
|
||||
$post_count_chk = (isset($_POST['chk']) && is_array($_POST['chk'])) ? count($_POST['chk']) : 0;
|
||||
$chk = (isset($_POST['chk']) && is_array($_POST['chk'])) ? $_POST['chk'] : array();
|
||||
$act_button = isset($_POST['act_button']) ? strip_tags($_POST['act_button']) : '';
|
||||
$board_table = (isset($_POST['board_table']) && is_array($_POST['board_table'])) ? $_POST['board_table'] : array();
|
||||
|
||||
if (!$post_count_chk) {
|
||||
alert($act_button . " 하실 항목을 하나 이상 체크하세요.");
|
||||
}
|
||||
|
||||
check_admin_token();
|
||||
|
||||
if ($act_button === "선택수정") {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
for ($i = 0; $i < $post_count_chk; $i++) {
|
||||
// 실제 번호를 넘김
|
||||
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
|
||||
|
||||
$post_gr_id = isset($_POST['gr_id'][$k]) ? clean_xss_tags($_POST['gr_id'][$k], 1, 1) : '';
|
||||
$post_bo_device = isset($_POST['bo_device'][$k]) ? clean_xss_tags($_POST['bo_device'][$k], 1, 1) : '';
|
||||
$post_bo_skin = isset($_POST['bo_skin'][$k]) ? clean_xss_tags($_POST['bo_skin'][$k], 1, 1) : '';
|
||||
$post_bo_mobile_skin = isset($_POST['bo_mobile_skin'][$k]) ? clean_xss_tags($_POST['bo_mobile_skin'][$k], 1, 1) : '';
|
||||
$post_bo_read_point = isset($_POST['bo_read_point'][$k]) ? clean_xss_tags($_POST['bo_read_point'][$k], 1, 1) : '';
|
||||
$post_bo_write_point = isset($_POST['bo_write_point'][$k]) ? clean_xss_tags($_POST['bo_write_point'][$k], 1, 1) : '';
|
||||
$post_bo_comment_point = isset($_POST['bo_comment_point'][$k]) ? clean_xss_tags($_POST['bo_comment_point'][$k], 1, 1) : '';
|
||||
$post_bo_download_point = isset($_POST['bo_download_point'][$k]) ? clean_xss_tags($_POST['bo_download_point'][$k], 1, 1) : '';
|
||||
$post_bo_use_search = isset($_POST['bo_use_search'][$k]) ? clean_xss_tags($_POST['bo_use_search'][$k], 1, 1) : '';
|
||||
$post_bo_use_sns = isset($_POST['bo_use_sns'][$k]) ? clean_xss_tags($_POST['bo_use_sns'][$k], 1, 1) : '';
|
||||
$post_bo_order = isset($_POST['bo_order'][$k]) ? clean_xss_tags($_POST['bo_order'][$k], 1, 1) : '';
|
||||
$post_board_table = isset($_POST['board_table'][$k]) ? clean_xss_tags($_POST['board_table'][$k], 1, 1) : '';
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
$sql = " select count(*) as cnt from {$g5['board_table']} a, {$g5['group_table']} b
|
||||
where a.gr_id = '" . sql_real_escape_string($post_gr_id) . "'
|
||||
and a.gr_id = b.gr_id
|
||||
and b.gr_admin = '{$member['mb_id']}' ";
|
||||
$row = sql_fetch($sql);
|
||||
if (!$row['cnt']) {
|
||||
alert('최고관리자가 아닌 경우 다른 관리자의 게시판(' . $board_table[$k] . ')은 수정이 불가합니다.');
|
||||
}
|
||||
}
|
||||
|
||||
$p_bo_subject = is_array($_POST['bo_subject']) ? strip_tags(clean_xss_attributes($_POST['bo_subject'][$k])) : '';
|
||||
|
||||
$sql = " update {$g5['board_table']}
|
||||
set gr_id = '" . sql_real_escape_string($post_gr_id) . "',
|
||||
bo_subject = '" . $p_bo_subject . "',
|
||||
bo_device = '" . sql_real_escape_string($post_bo_device) . "',
|
||||
bo_skin = '" . sql_real_escape_string($post_bo_skin) . "',
|
||||
bo_mobile_skin = '" . sql_real_escape_string($post_bo_mobile_skin) . "',
|
||||
bo_read_point = '" . sql_real_escape_string($post_bo_read_point) . "',
|
||||
bo_write_point = '" . sql_real_escape_string($post_bo_write_point) . "',
|
||||
bo_comment_point = '" . sql_real_escape_string($post_bo_comment_point) . "',
|
||||
bo_download_point = '" . sql_real_escape_string($post_bo_download_point) . "',
|
||||
bo_use_search = '" . sql_real_escape_string($post_bo_use_search) . "',
|
||||
bo_use_sns = '" . sql_real_escape_string($post_bo_use_sns) . "',
|
||||
bo_order = '" . sql_real_escape_string($post_bo_order) . "'
|
||||
where bo_table = '" . sql_real_escape_string($post_board_table) . "' ";
|
||||
|
||||
sql_query($sql);
|
||||
}
|
||||
} elseif ($act_button === "선택삭제") {
|
||||
if ($is_admin != 'super') {
|
||||
alert('게시판 삭제는 최고관리자만 가능합니다.');
|
||||
}
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'd');
|
||||
|
||||
// _BOARD_DELETE_ 상수를 선언해야 board_delete.inc.php 가 정상 작동함
|
||||
/* 확인필요 22.05.27
|
||||
A file should declare new symbols (classes, functions, constants, etc.) and cause no other side effects,
|
||||
or it should execute logic with side effects, but should not do both.*/
|
||||
define('_BOARD_DELETE_', true);
|
||||
|
||||
for ($i = 0; $i < $post_count_chk; $i++) {
|
||||
// 실제 번호를 넘김
|
||||
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
|
||||
|
||||
// include 전에 $bo_table 값을 반드시 넘겨야 함
|
||||
$tmp_bo_table = isset($_POST['board_table'][$k]) ? trim(clean_xss_tags($_POST['board_table'][$k], 1, 1)) : '';
|
||||
|
||||
if (preg_match("/^[A-Za-z0-9_]+$/", $tmp_bo_table)) {
|
||||
include './board_delete.inc.php';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_event('admin_board_list_update', $act_button, $chk, $board_table, $qstr);
|
||||
|
||||
goto_url('./board_list.php?' . $qstr);
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
$sub_menu = '300100';
|
||||
require_once './_common.php';
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
if (!$board['bo_table']) {
|
||||
alert('존재하지 않는 게시판입니다.');
|
||||
}
|
||||
|
||||
$g5['title'] = $board['bo_subject'] . ' 게시판 썸네일 삭제';
|
||||
require_once './admin.head.php';
|
||||
?>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
완료 메세지가 나오기 전에 프로그램의 실행을 중지하지 마십시오.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$dir = G5_DATA_PATH . '/file/' . $bo_table;
|
||||
|
||||
$cnt = 0;
|
||||
if (is_dir($dir)) {
|
||||
echo '<ul>';
|
||||
$files = glob($dir . '/thumb-*');
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $thumbnail) {
|
||||
$cnt++;
|
||||
@unlink($thumbnail);
|
||||
|
||||
echo '<li>' . $thumbnail . '</li>' . PHP_EOL;
|
||||
|
||||
flush();
|
||||
|
||||
if (($cnt % 10) == 0) {
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo '<li>완료됨</li></ul>' . PHP_EOL;
|
||||
echo '<div class="local_desc01 local_desc"><p><strong>썸네일 ' . $cnt . '건의 삭제 완료됐습니다.</strong></p></div>' . PHP_EOL;
|
||||
} else {
|
||||
echo '<p>첨부파일 디렉토리가 존재하지 않습니다.</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm"><a href="./board_form.php?w=u&bo_table=<?php echo $bo_table; ?>&<?php echo $qstr; ?>">게시판 수정으로 돌아가기</a></div>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
if ($is_admin != 'super' && $w == '') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
$html_title = '게시판그룹';
|
||||
$gr_id_attr = '';
|
||||
$sound_only = '';
|
||||
|
||||
if (!isset($group['gr_id'])) {
|
||||
$group['gr_id'] = '';
|
||||
$group['gr_subject'] = '';
|
||||
$group['gr_device'] = '';
|
||||
}
|
||||
|
||||
$gr = array('gr_use_access' => 0, 'gr_admin' => '');
|
||||
if ($w == '') {
|
||||
$gr_id_attr = 'required';
|
||||
$sound_only = '<strong class="sound_only"> 필수</strong>';
|
||||
$html_title .= ' 생성';
|
||||
} elseif ($w == 'u') {
|
||||
$gr_id_attr = 'readonly';
|
||||
$gr = sql_fetch(" select * from {$g5['group_table']} where gr_id = '$gr_id' ");
|
||||
$html_title .= ' 수정';
|
||||
} else {
|
||||
alert('제대로 된 값이 넘어오지 않았습니다.');
|
||||
}
|
||||
|
||||
if (!isset($group['gr_device'])) {
|
||||
sql_query(" ALTER TABLE `{$g5['group_table']}` ADD `gr_device` ENUM('both','pc','mobile') NOT NULL DEFAULT 'both' AFTER `gr_subject` ", false);
|
||||
}
|
||||
|
||||
// 접근회원수
|
||||
$sql1 = " select count(*) as cnt from {$g5['group_member_table']} where gr_id = '{$gr_id}' ";
|
||||
$row1 = sql_fetch($sql1);
|
||||
$group_member_count = $row1['cnt'];
|
||||
|
||||
$g5['title'] = $html_title;
|
||||
require_once './admin.head.php';
|
||||
?>
|
||||
|
||||
<form name="fboardgroup" id="fboardgroup" action="./boardgroup_form_update.php" onsubmit="return fboardgroup_check(this);" method="post" autocomplete="off">
|
||||
<input type="hidden" name="w" value="<?php echo $w ?>">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>">
|
||||
<input type="hidden" name="token" value="">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?></caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="gr_id">그룹 ID<?php echo $sound_only ?></label></th>
|
||||
<td><input type="text" name="gr_id" value="<?php echo $group['gr_id'] ?>" id="gr_id" <?php echo $gr_id_attr; ?> class="<?php echo $gr_id_attr; ?> alnum_ frm_input" maxlength="10">
|
||||
<?php
|
||||
if ($w == '') {
|
||||
echo '영문자, 숫자, _ 만 가능 (공백없이)';
|
||||
} else {
|
||||
echo '<a href="' . G5_BBS_URL . '/group.php?gr_id=' . $group['gr_id'] . '" class="btn_frmline">게시판그룹 바로가기</a>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="gr_subject">그룹 제목<strong class="sound_only"> 필수</strong></label></th>
|
||||
<td>
|
||||
<input type="text" name="gr_subject" value="<?php echo get_text($group['gr_subject']) ?>" id="gr_subject" required class="required frm_input" size="80">
|
||||
<?php
|
||||
if ($w == 'u') {
|
||||
echo '<a href="./board_form.php?gr_id=' . $gr_id . '" class="btn_frmline">게시판생성</a>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="gr_device">접속기기</label></th>
|
||||
<td>
|
||||
<?php echo help("PC 와 모바일 사용을 구분합니다.") ?>
|
||||
<select id="gr_device" name="gr_device">
|
||||
<option value="both" <?php echo get_selected($group['gr_device'], 'both', true); ?>>PC와 모바일에서 모두 사용</option>
|
||||
<option value="pc" <?php echo get_selected($group['gr_device'], 'pc'); ?>>PC 전용</option>
|
||||
<option value="mobile" <?php echo get_selected($group['gr_device'], 'mobile'); ?>>모바일 전용</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php
|
||||
if ($is_admin == 'super') {
|
||||
echo '<label for="gr_admin">그룹 관리자</label>';
|
||||
} else {
|
||||
echo '그룹 관리자';
|
||||
}
|
||||
?>
|
||||
</th>
|
||||
<td>
|
||||
<?php
|
||||
if ($is_admin == 'super') {
|
||||
echo '<input type="text" id="gr_admin" name="gr_admin" class="frm_input" value="' . $gr['gr_admin'] . '" maxlength="20">';
|
||||
} else {
|
||||
echo '<input type="hidden" id="gr_admin" name="gr_admin" value="' . $gr['gr_admin'] . '">' . $gr['gr_admin'];
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="gr_use_access">접근회원사용</label></th>
|
||||
<td>
|
||||
<?php echo help("사용에 체크하시면 이 그룹에 속한 게시판은 접근가능한 회원만 접근이 가능합니다.") ?>
|
||||
<input type="checkbox" name="gr_use_access" value="1" id="gr_use_access" <?php echo $gr['gr_use_access'] ? 'checked' : ''; ?>>
|
||||
사용
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">접근회원수</th>
|
||||
<td>
|
||||
<?php
|
||||
echo '<a href="./boardgroupmember_list.php?gr_id=' . $gr_id . '">' . $group_member_count . '</a>';
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php for ($i = 1; $i <= 10; $i++) { ?>
|
||||
<tr>
|
||||
<th scope="row">여분필드<?php echo $i ?></th>
|
||||
<td class="td_extra">
|
||||
<label for="gr_<?php echo $i ?>_subj">여분필드 <?php echo $i ?> 제목</label>
|
||||
<input type="text" name="gr_<?php echo $i ?>_subj" value="<?php echo isset($group['gr_' . $i . '_subj']) ? get_text($group['gr_' . $i . '_subj']) : ''; ?>" id="gr_<?php echo $i ?>_subj" class="frm_input">
|
||||
<label for="gr_<?php echo $i ?>">여분필드 <?php echo $i ?> 내용</label>
|
||||
<input type="text" name="gr_<?php echo $i ?>" value="<?php echo isset($gr['gr_' . $i]) ? get_sanitize_input($gr['gr_' . $i]) : ''; ?>" id="gr_<?php echo $i ?>" class="frm_input">
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_fixed_top">
|
||||
<a href="./boardgroup_list.php?<?php echo $qstr ?>" class="btn btn_02">목록</a>
|
||||
<input type="submit" class="btn_submit btn" accesskey="s" value="확인">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
게시판을 생성하시려면 1개 이상의 게시판그룹이 필요합니다.<br>
|
||||
게시판그룹을 이용하시면 더 효과적으로 게시판을 관리할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fboardgroup_check(f) {
|
||||
f.action = './boardgroup_form_update.php';
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
if ($w == 'u') {
|
||||
check_demo();
|
||||
}
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
if ($is_admin != 'super' && $w == '') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$gr_id = isset($_POST['gr_id']) ? $_POST['gr_id'] : '';
|
||||
|
||||
if (!preg_match("/^([A-Za-z0-9_]{1,10})$/", $gr_id)) {
|
||||
alert('그룹 ID는 공백없이 영문자, 숫자, _ 만 사용 가능합니다. (10자 이내)');
|
||||
}
|
||||
|
||||
if (empty($gr_subject)) {
|
||||
alert('그룹 제목을 입력하세요.');
|
||||
}
|
||||
|
||||
$posts = array();
|
||||
|
||||
$check_keys = array(
|
||||
'gr_subject' => '',
|
||||
'gr_device' => '',
|
||||
'gr_admin' => '',
|
||||
);
|
||||
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$check_keys['gr_' . $i . '_subj'] = isset($_POST['gr_' . $i . '_subj']) ? $_POST['gr_' . $i . '_subj'] : '';
|
||||
$check_keys['gr_' . $i] = isset($_POST['gr_' . $i]) ? $_POST['gr_' . $i] : '';
|
||||
}
|
||||
|
||||
foreach ($check_keys as $key => $value) {
|
||||
if ($key === 'gr_subject') {
|
||||
$posts[$key] = isset($_POST[$key]) ? strip_tags(clean_xss_attributes($_POST[$key])) : '';
|
||||
} else {
|
||||
$posts[$key] = isset($_POST[$key]) ? $_POST[$key] : '';
|
||||
}
|
||||
}
|
||||
|
||||
$sql_common = " gr_subject = '{$posts['gr_subject']}',
|
||||
gr_device = '{$posts['gr_device']}',
|
||||
gr_admin = '{$posts['gr_admin']}',
|
||||
gr_1_subj = '{$posts['gr_1_subj']}',
|
||||
gr_2_subj = '{$posts['gr_2_subj']}',
|
||||
gr_3_subj = '{$posts['gr_3_subj']}',
|
||||
gr_4_subj = '{$posts['gr_4_subj']}',
|
||||
gr_5_subj = '{$posts['gr_5_subj']}',
|
||||
gr_6_subj = '{$posts['gr_6_subj']}',
|
||||
gr_7_subj = '{$posts['gr_7_subj']}',
|
||||
gr_8_subj = '{$posts['gr_8_subj']}',
|
||||
gr_9_subj = '{$posts['gr_9_subj']}',
|
||||
gr_10_subj = '{$posts['gr_10_subj']}',
|
||||
gr_1 = '{$posts['gr_1']}',
|
||||
gr_2 = '{$posts['gr_2']}',
|
||||
gr_3 = '{$posts['gr_3']}',
|
||||
gr_4 = '{$posts['gr_4']}',
|
||||
gr_5 = '{$posts['gr_5']}',
|
||||
gr_6 = '{$posts['gr_6']}',
|
||||
gr_7 = '{$posts['gr_7']}',
|
||||
gr_8 = '{$posts['gr_8']}',
|
||||
gr_9 = '{$posts['gr_9']}',
|
||||
gr_10 = '{$posts['gr_10']}' ";
|
||||
if (isset($_POST['gr_use_access'])) {
|
||||
$sql_common .= ", gr_use_access = '{$_POST['gr_use_access']}' ";
|
||||
} else {
|
||||
$sql_common .= ", gr_use_access = '' ";
|
||||
}
|
||||
|
||||
if ($w == '') {
|
||||
$sql = " select count(*) as cnt from {$g5['group_table']} where gr_id = '{$gr_id}' ";
|
||||
$row = sql_fetch($sql);
|
||||
if ($row['cnt']) {
|
||||
alert('이미 존재하는 그룹 ID 입니다.');
|
||||
}
|
||||
|
||||
$sql = " insert into {$g5['group_table']}
|
||||
set gr_id = '{$gr_id}',
|
||||
{$sql_common} ";
|
||||
sql_query($sql);
|
||||
} elseif ($w == "u") {
|
||||
$sql = " update {$g5['group_table']}
|
||||
set {$sql_common}
|
||||
where gr_id = '{$gr_id}' ";
|
||||
sql_query($sql);
|
||||
} else {
|
||||
alert('제대로 된 값이 넘어오지 않았습니다.');
|
||||
}
|
||||
|
||||
run_event('admin_boardgroup_form_update', $gr_id, $w);
|
||||
|
||||
goto_url('./boardgroup_form.php?w=u&gr_id=' . $gr_id . '&' . $qstr);
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
if (!isset($group['gr_device'])) {
|
||||
// 게시판 그룹 사용 필드 추가
|
||||
// both : pc, mobile 둘다 사용
|
||||
// pc : pc 전용 사용
|
||||
// mobile : mobile 전용 사용
|
||||
// none : 사용 안함
|
||||
sql_query(" ALTER TABLE `{$g5['group_table']}` ADD `gr_device` ENUM( 'both', 'pc', 'mobile' ) NOT NULL DEFAULT 'both' AFTER `gr_subject` ", false);
|
||||
}
|
||||
|
||||
$sql_common = " from {$g5['group_table']} ";
|
||||
|
||||
$sql_search = " where (1) ";
|
||||
if ($is_admin != 'super') {
|
||||
$sql_search .= " and (gr_admin = '{$member['mb_id']}') ";
|
||||
}
|
||||
|
||||
if ($stx) {
|
||||
$sql_search .= " and ( ";
|
||||
switch ($sfl) {
|
||||
case "gr_id":
|
||||
case "gr_admin":
|
||||
$sql_search .= " ({$sfl} = '{$stx}') ";
|
||||
break;
|
||||
default:
|
||||
$sql_search .= " ({$sfl} like '%{$stx}%') ";
|
||||
break;
|
||||
}
|
||||
$sql_search .= " ) ";
|
||||
}
|
||||
|
||||
if ($sst) {
|
||||
$sql_order = " order by {$sst} {$sod} ";
|
||||
} else {
|
||||
$sql_order = " order by gr_id asc ";
|
||||
}
|
||||
|
||||
$sql = " select count(*) as cnt {$sql_common} {$sql_search} {$sql_order} ";
|
||||
$row = sql_fetch($sql);
|
||||
$total_count = $row['cnt'];
|
||||
|
||||
$rows = $config['cf_page_rows'];
|
||||
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
|
||||
if ($page < 1) {
|
||||
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
|
||||
}
|
||||
$from_record = ($page - 1) * $rows; // 시작 열을 구함
|
||||
|
||||
$sql = " select * {$sql_common} {$sql_search} {$sql_order} limit {$from_record}, {$rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall">처음</a>';
|
||||
|
||||
$g5['title'] = '게시판그룹설정';
|
||||
require_once './admin.head.php';
|
||||
|
||||
$colspan = 10;
|
||||
?>
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
<?php echo $listall ?>
|
||||
<span class="btn_ov01"><span class="ov_txt">전체그룹</span><span class="ov_num"> <?php echo number_format($total_count) ?>개</span></span>
|
||||
</div>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="gr_subject" <?php echo get_selected($sfl, "gr_subject"); ?>>제목</option>
|
||||
<option value="gr_id" <?php echo get_selected($sfl, "gr_id"); ?>>ID</option>
|
||||
<option value="gr_admin" <?php echo get_selected($sfl, "gr_admin"); ?>>그룹관리자</option>
|
||||
</select>
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" id="stx" value="<?php echo $stx ?>" required class="required frm_input">
|
||||
<input type="submit" value="검색" class="btn_submit">
|
||||
</form>
|
||||
|
||||
|
||||
<form name="fboardgrouplist" id="fboardgrouplist" action="./boardgroup_list_update.php" onsubmit="return fboardgrouplist_submit(this);" method="post">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>">
|
||||
<input type="hidden" name="token" value="">
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">그룹 전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col"><?php echo subject_sort_link('gr_id') ?>그룹아이디</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('gr_subject') ?>제목</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('gr_admin') ?>그룹관리자</a></th>
|
||||
<th scope="col">게시판</th>
|
||||
<th scope="col">접근<br>사용</th>
|
||||
<th scope="col">접근<br>회원수</th>
|
||||
<th scope="col"><?php echo subject_sort_link('gr_order') ?>출력<br>순서</a></th>
|
||||
<th scope="col">접속기기</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
// 접근회원수
|
||||
$sql1 = " select count(*) as cnt from {$g5['group_member_table']} where gr_id = '{$row['gr_id']}' ";
|
||||
$row1 = sql_fetch($sql1);
|
||||
|
||||
// 게시판수
|
||||
$sql2 = " select count(*) as cnt from {$g5['board_table']} where gr_id = '{$row['gr_id']}' ";
|
||||
$row2 = sql_fetch($sql2);
|
||||
|
||||
$s_upd = '<a href="./boardgroup_form.php?' . $qstr . '&w=u&gr_id=' . $row['gr_id'] . '" class="btn_03 btn">수정</a>';
|
||||
|
||||
$bg = 'bg' . ($i % 2);
|
||||
?>
|
||||
|
||||
<tr class="<?php echo $bg; ?>">
|
||||
<td class="td_chk">
|
||||
<input type="hidden" name="group_id[<?php echo $i ?>]" value="<?php echo $row['gr_id'] ?>">
|
||||
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo get_text($row['gr_subject']); ?> 그룹</label>
|
||||
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
|
||||
</td>
|
||||
<td class="td_left"><a href="<?php echo G5_BBS_URL ?>/group.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo $row['gr_id'] ?></a></td>
|
||||
<td class="td_input">
|
||||
<label for="gr_subject_<?php echo $i; ?>" class="sound_only">그룹제목</label>
|
||||
<input type="text" name="gr_subject[<?php echo $i ?>]" value="<?php echo get_text($row['gr_subject']) ?>" id="gr_subject_<?php echo $i ?>" class="tbl_input">
|
||||
</td>
|
||||
<td class="td_mng td_input">
|
||||
<?php if ($is_admin == 'super') { ?>
|
||||
<label for="gr_admin_<?php echo $i; ?>" class="sound_only">그룹관리자</label>
|
||||
<input type="text" name="gr_admin[<?php echo $i ?>]" value="<?php echo get_sanitize_input($row['gr_admin']); ?>" id="gr_admin_<?php echo $i ?>" class="tbl_input" size="10" maxlength="20">
|
||||
<?php } else { ?>
|
||||
<input type="hidden" name="gr_admin[<?php echo $i ?>]" value="<?php echo get_sanitize_input($row['gr_admin']); ?>"><?php echo get_text($row['gr_admin']); ?>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="td_num"><a href="./board_list.php?sfl=a.gr_id&stx=<?php echo $row['gr_id'] ?>"><?php echo $row2['cnt'] ?></a></td>
|
||||
<td class="td_numsmall">
|
||||
<label for="gr_use_access_<?php echo $i; ?>" class="sound_only">접근회원 사용</label>
|
||||
<input type="checkbox" name="gr_use_access[<?php echo $i ?>]" <?php echo $row['gr_use_access'] ? 'checked' : '' ?> value="1" id="gr_use_access_<?php echo $i ?>">
|
||||
</td>
|
||||
<td class="td_num"><a href="./boardgroupmember_list.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo $row1['cnt'] ?></a></td>
|
||||
<td class="td_numsmall">
|
||||
<label for="gr_order_<?php echo $i; ?>" class="sound_only">메인메뉴 출력순서</label>
|
||||
<input type="text" name="gr_order[<?php echo $i ?>]" value="<?php echo $row['gr_order'] ?>" id="gr_order_<?php echo $i ?>" class="tbl_input" size="2">
|
||||
</td>
|
||||
<td class="td_mng">
|
||||
<label for="gr_device_<?php echo $i; ?>" class="sound_only">접속기기</label>
|
||||
<select name="gr_device[<?php echo $i ?>]" id="gr_device_<?php echo $i ?>">
|
||||
<option value="both" <?php echo get_selected($row['gr_device'], 'both'); ?>>모두</option>
|
||||
<option value="pc" <?php echo get_selected($row['gr_device'], 'pc'); ?>>PC</option>
|
||||
<option value="mobile" <?php echo get_selected($row['gr_device'], 'mobile'); ?>>모바일</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="td_mng td_mng_s"><?php echo $s_upd ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_fixed_top">
|
||||
<input type="submit" name="act_button" onclick="document.pressed=this.value" value="선택수정" class="btn btn_02">
|
||||
<input type="submit" name="act_button" onclick="document.pressed=this.value" value="선택삭제" class="btn btn_02">
|
||||
<a href="./boardgroup_form.php" class="btn btn_01">게시판그룹 추가</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
접근사용 옵션을 설정하시면 관리자가 지정한 회원만 해당 그룹에 접근할 수 있습니다.<br>
|
||||
접근사용 옵션은 해당 그룹에 속한 모든 게시판에 적용됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$pagelist = get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'] . '?' . $qstr . '&page=');
|
||||
echo $pagelist;
|
||||
?>
|
||||
|
||||
<script>
|
||||
function fboardgrouplist_submit(f) {
|
||||
if (!is_checked("chk[]")) {
|
||||
alert(document.pressed + " 하실 항목을 하나 이상 선택하세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.pressed == "선택삭제") {
|
||||
if (!confirm("선택한 자료를 정말 삭제하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
check_demo();
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$post_chk = isset($_POST['chk']) ? (array) $_POST['chk'] : array();
|
||||
$post_group_id = isset($_POST['group_id']) ? (array) $_POST['group_id'] : array();
|
||||
$act_button = isset($_POST['act_button']) ? $_POST['act_button'] : '';
|
||||
|
||||
$chk_count = count($post_chk);
|
||||
|
||||
if (!$chk_count) {
|
||||
alert($act_button . '할 게시판그룹을 1개이상 선택해 주세요.');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $chk_count; $i++) {
|
||||
$k = isset($post_chk[$i]) ? (int) $post_chk[$i] : 0;
|
||||
$gr_id = preg_replace('/[^a-z0-9_]/i', '', $post_group_id[$k]);
|
||||
$gr_subject = isset($_POST['gr_subject'][$k]) ? strip_tags(clean_xss_attributes($_POST['gr_subject'][$k])) : '';
|
||||
$gr_admin = isset($_POST['gr_admin'][$k]) ? strip_tags(clean_xss_attributes($_POST['gr_admin'][$k])) : '';
|
||||
$gr_device = isset($_POST['gr_device'][$k]) ? clean_xss_tags($_POST['gr_device'][$k], 1, 1, 10) : '';
|
||||
$gr_use_access = isset($_POST['gr_use_access'][$k]) ? (int) $_POST['gr_use_access'][$k] : 0;
|
||||
$gr_order = isset($_POST['gr_order'][$k]) ? (int) $_POST['gr_order'][$k] : 0;
|
||||
|
||||
if ($act_button == '선택수정') {
|
||||
$sql = " update {$g5['group_table']}
|
||||
set gr_subject = '{$gr_subject}',
|
||||
gr_device = '" . sql_real_escape_string($gr_device) . "',
|
||||
gr_admin = '" . sql_real_escape_string($gr_admin) . "',
|
||||
gr_use_access = '" . $gr_use_access . "',
|
||||
gr_order = '" . $gr_order . "'
|
||||
where gr_id = '{$gr_id}' ";
|
||||
if ($is_admin != 'super') {
|
||||
$sql .= " and gr_admin = '{$gr_admin}' ";
|
||||
}
|
||||
sql_query($sql);
|
||||
} elseif ($act_button == '선택삭제') {
|
||||
$row = sql_fetch(" select count(*) as cnt from {$g5['board_table']} where gr_id = '$gr_id' ");
|
||||
if ($row['cnt']) {
|
||||
alert("이 그룹에 속한 게시판이 존재하여 게시판 그룹을 삭제할 수 없습니다.\\n\\n이 그룹에 속한 게시판을 먼저 삭제하여 주십시오.", './board_list.php?sfl=gr_id&stx=' . $gr_id);
|
||||
}
|
||||
|
||||
// 그룹 삭제
|
||||
sql_query(" delete from {$g5['group_table']} where gr_id = '$gr_id' ");
|
||||
|
||||
// 그룹접근 회원 삭제
|
||||
sql_query(" delete from {$g5['group_member_table']} where gr_id = '$gr_id' ");
|
||||
}
|
||||
}
|
||||
|
||||
run_event('admin_boardgroup_list_update', $act_button, $post_chk, $post_group_id, $qstr);
|
||||
|
||||
goto_url('./boardgroup_list.php?' . $qstr);
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
$mb = get_member($mb_id);
|
||||
$token = isset($token) ? $token : '';
|
||||
|
||||
if (!(isset($mb['mb_id']) && $mb['mb_id'])) {
|
||||
alert('존재하지 않는 회원입니다.');
|
||||
}
|
||||
|
||||
$g5['title'] = '접근가능그룹';
|
||||
require_once './admin.head.php';
|
||||
|
||||
$colspan = 4;
|
||||
?>
|
||||
|
||||
<form name="fboardgroupmember_form" id="fboardgroupmember_form" action="./boardgroupmember_update.php" onsubmit="return boardgroupmember_form_check(this)" method="post">
|
||||
<input type="hidden" name="mb_id" value="<?php echo $mb['mb_id'] ?>" id="mb_id">
|
||||
<input type="hidden" name="token" value="" id="token">
|
||||
|
||||
<div class="local_ov01 local_ov">
|
||||
<span class="btn_ov01"><span class="ov_txt"> 아이디</span><span class="ov_num"><?php echo $mb['mb_id'] ?></span></span>
|
||||
<span class="btn_ov01"><span class="ov_txt"> 이름</span><span class="ov_num"><?php echo get_text($mb['mb_name']); ?></span></span>
|
||||
<span class="btn_ov01"><span class="ov_txt"> 닉네임</span><span class="ov_num"><?php echo $mb['mb_nick'] ?></span></span>
|
||||
</div>
|
||||
|
||||
<div class="local_cmd01 local_cmd">
|
||||
<label for="gr_id">그룹지정</label>
|
||||
<select name="gr_id" id="gr_id">
|
||||
<option value="">접근가능 그룹을 선택하세요.</option>
|
||||
<?php
|
||||
$sql = " select *
|
||||
from {$g5['group_table']}
|
||||
where gr_use_access = 1 ";
|
||||
if ($is_admin != 'super') {
|
||||
$sql .= " and gr_admin = '{$member['mb_id']}' ";
|
||||
}
|
||||
$sql .= " order by gr_id ";
|
||||
$result = sql_query($sql);
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
echo "<option value=\"" . $row['gr_id'] . "\">" . $row['gr_subject'] . "</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<input type="submit" value="선택" class="btn_submit btn" accesskey="s">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form name="fboardgroupmember" id="fboardgroupmember" action="./boardgroupmember_update.php" onsubmit="return fboardgroupmember_submit(this);" method="post">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>" id="sst">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>" id="sod">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>" id="sfl">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>" id="stx">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>" id="page">
|
||||
<input type="hidden" name="token" value="<?php echo get_sanitize_input($token); ?>" id="token">
|
||||
<input type="hidden" name="mb_id" value="<?php echo $mb['mb_id'] ?>" id="mb_id">
|
||||
<input type="hidden" name="w" value="d" id="w">
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">접근가능그룹 전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col">그룹아이디</th>
|
||||
<th scope="col">그룹</th>
|
||||
<th scope="col">처리일시</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$sql = " select * from {$g5['group_member_table']} a, {$g5['group_table']} b
|
||||
where a.mb_id = '{$mb['mb_id']}'
|
||||
and a.gr_id = b.gr_id ";
|
||||
if ($is_admin != 'super') {
|
||||
$sql .= " and b.gr_admin = '{$member['mb_id']}' ";
|
||||
}
|
||||
$sql .= " order by a.gr_id desc ";
|
||||
$result = sql_query($sql);
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
?>
|
||||
<tr>
|
||||
<td class="td_chk">
|
||||
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo $row['gr_subject'] ?> 그룹</label>
|
||||
<input type="checkbox" name="chk[]" value="<?php echo $row['gm_id'] ?>" id="chk_<?php echo $i ?>">
|
||||
</td>
|
||||
<td class="td_grid"><a href="<?php echo G5_BBS_URL; ?>/group.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo $row['gr_id'] ?></a></td>
|
||||
<td class="td_category"><?php echo $row['gr_subject'] ?></td>
|
||||
<td class="td_datetime"><?php echo $row['gm_datetime'] ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="' . $colspan . '" class="empty_table">접근가능한 그룹이 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_list01 btn_list">
|
||||
<input type="submit" name="" value="선택삭제" class="btn btn_02">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function fboardgroupmember_submit(f) {
|
||||
if (!is_checked("chk[]")) {
|
||||
alert("선택삭제 하실 항목을 하나 이상 선택하세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function boardgroupmember_form_check(f) {
|
||||
if (f.gr_id.value == '') {
|
||||
alert('접근가능 그룹을 선택하세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$gr = get_group($gr_id);
|
||||
if (!$gr['gr_id']) {
|
||||
alert('존재하지 않는 그룹입니다.');
|
||||
}
|
||||
|
||||
$sql_common = " from {$g5['group_member_table']} a
|
||||
left outer join {$g5['member_table']} b on (a.mb_id = b.mb_id) ";
|
||||
$sql_search = " where gr_id = '{$gr_id}' ";
|
||||
|
||||
// 회원아이디로 검색되지 않던 오류를 수정
|
||||
if (isset($stx) && $stx) {
|
||||
$sql_search .= " and ( ";
|
||||
switch ($sfl) {
|
||||
default:
|
||||
$sql_search .= " ($sfl like '%$stx%') ";
|
||||
break;
|
||||
}
|
||||
$sql_search .= " ) ";
|
||||
}
|
||||
|
||||
if (!$sst) {
|
||||
$sst = "gm_datetime";
|
||||
$sod = "desc";
|
||||
}
|
||||
$sql_order = " order by {$sst} {$sod} ";
|
||||
|
||||
$sql = " select count(*) as cnt
|
||||
{$sql_common}
|
||||
{$sql_search}
|
||||
{$sql_order} ";
|
||||
$row = sql_fetch($sql);
|
||||
$total_count = $row['cnt'];
|
||||
|
||||
$rows = $config['cf_page_rows'];
|
||||
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
|
||||
if ($page < 1) {
|
||||
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
|
||||
}
|
||||
$from_record = ($page - 1) * $rows; // 시작 열을 구함
|
||||
|
||||
$sql = " select *
|
||||
{$sql_common}
|
||||
{$sql_search}
|
||||
{$sql_order}
|
||||
limit {$from_record}, {$rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$g5['title'] = $gr['gr_subject'] . ' 그룹 접근가능회원 (그룹아이디:' . $gr['gr_id'] . ')';
|
||||
require_once './admin.head.php';
|
||||
|
||||
$colspan = 7;
|
||||
?>
|
||||
|
||||
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
|
||||
<input type="hidden" name="gr_id" value="<?php echo $gr_id ?>">
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="a.mb_id" <?php echo get_selected($sfl, "a.mb_id") ?>>회원아이디</option>
|
||||
</select>
|
||||
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
|
||||
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" required class="required frm_input">
|
||||
<input type="submit" value="검색" class="btn_submit">
|
||||
</form>
|
||||
|
||||
<form name="fboardgroupmember" id="fboardgroupmember" action="./boardgroupmember_update.php" onsubmit="return fboardgroupmember_submit(this);" method="post">
|
||||
<input type="hidden" name="sst" value="<?php echo $sst ?>">
|
||||
<input type="hidden" name="sod" value="<?php echo $sod ?>">
|
||||
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
|
||||
<input type="hidden" name="stx" value="<?php echo $stx ?>">
|
||||
<input type="hidden" name="page" value="<?php echo $page ?>">
|
||||
<input type="hidden" name="token" value="<?php echo $token ?>">
|
||||
<input type="hidden" name="gr_id" value="<?php echo $gr_id ?>">
|
||||
<input type="hidden" name="w" value="ld">
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption><?php echo $g5['title']; ?> 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<label for="chkall" class="sound_only">접근가능회원 전체</label>
|
||||
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
|
||||
</th>
|
||||
<th scope="col">그룹</th>
|
||||
<th scope="col"><?php echo subject_sort_link('b.mb_id', 'gr_id=' . $gr_id) ?>회원아이디</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('b.mb_name', 'gr_id=' . $gr_id) ?>이름</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('b.mb_nick', 'gr_id=' . $gr_id) ?>별명</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('b.mb_today_login', 'gr_id=' . $gr_id) ?>최종접속</a></th>
|
||||
<th scope="col"><?php echo subject_sort_link('a.gm_datetime', 'gr_id=' . $gr_id) ?>처리일시</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
// 접근가능한 그룹수
|
||||
$sql2 = " select count(*) as cnt from {$g5['group_member_table']} where mb_id = '{$row['mb_id']}' ";
|
||||
$row2 = sql_fetch($sql2);
|
||||
$group = "";
|
||||
if ($row2['cnt']) {
|
||||
$group = '<a href="./boardgroupmember_form.php?mb_id=' . $row['mb_id'] . '">' . $row2['cnt'] . '</a>';
|
||||
}
|
||||
|
||||
$mb_nick = get_sideview($row['mb_id'], $row['mb_nick'], $row['mb_email'], $row['mb_homepage']);
|
||||
|
||||
$bg = 'bg' . ($i % 2);
|
||||
?>
|
||||
<tr class="<?php echo $bg; ?>">
|
||||
<td class="td_chk">
|
||||
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo $row['mb_nick'] ?> 회원</label>
|
||||
<input type="checkbox" name="chk[]" value="<?php echo $row['gm_id'] ?>" id="chk_<?php echo $i ?>">
|
||||
</td>
|
||||
<td class="td_grid"><?php echo $group ?></td>
|
||||
<td class="td_mbid"><?php echo $row['mb_id'] ?></td>
|
||||
<td class="td_mbname"><?php echo get_text($row['mb_name']); ?></td>
|
||||
<td class="td_name sv_use"><?php echo $mb_nick ?></td>
|
||||
<td class="td_datetime"><?php echo substr($row['mb_today_login'], 2, 8) ?></td>
|
||||
<td class="td_datetime"><?php echo $row['gm_datetime'] ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_list01 btn_list">
|
||||
<input type="submit" name="" value="선택삭제">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$qstr&gr_id=$gr_id&page=");
|
||||
?>
|
||||
|
||||
<script>
|
||||
function fboardgroupmember_submit(f) {
|
||||
if (!is_checked("chk[]")) {
|
||||
alert("선택삭제 하실 항목을 하나 이상 선택하세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
$sub_menu = "300200";
|
||||
require_once './_common.php';
|
||||
|
||||
sql_query(" ALTER TABLE {$g5['group_member_table']} CHANGE `gm_id` `gm_id` INT( 11 ) DEFAULT '0' NOT NULL AUTO_INCREMENT ", false);
|
||||
|
||||
if ($w == '') {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
$mb = get_member($mb_id);
|
||||
if (empty($mb['mb_id'])) {
|
||||
alert('존재하지 않는 회원입니다.');
|
||||
}
|
||||
|
||||
$gr = get_group($gr_id);
|
||||
if (empty($gr['gr_id'])) {
|
||||
alert('존재하지 않는 그룹입니다.');
|
||||
}
|
||||
|
||||
$sql = " select count(*) as cnt
|
||||
from {$g5['group_member_table']}
|
||||
where gr_id = '{$gr_id}'
|
||||
and mb_id = '{$mb_id}' ";
|
||||
$row = sql_fetch($sql);
|
||||
if ($row['cnt']) {
|
||||
alert('이미 등록되어 있는 자료입니다.');
|
||||
} else {
|
||||
check_admin_token();
|
||||
|
||||
$sql = " insert into {$g5['group_member_table']}
|
||||
set gr_id = '{$_POST['gr_id']}',
|
||||
mb_id = '{$_POST['mb_id']}',
|
||||
gm_datetime = '" . G5_TIME_YMDHIS . "' ";
|
||||
sql_query($sql);
|
||||
}
|
||||
} elseif ($w == 'd' || $w == 'ld') {
|
||||
auth_check_menu($auth, $sub_menu, 'd');
|
||||
|
||||
$count = count($_POST['chk']);
|
||||
if (!$count) {
|
||||
alert('삭제할 목록을 하나이상 선택해 주세요.');
|
||||
}
|
||||
|
||||
check_admin_token();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$gm_id = (int) $_POST['chk'][$i];
|
||||
$sql = " select * from {$g5['group_member_table']} where gm_id = '$gm_id' ";
|
||||
$gm = sql_fetch($sql);
|
||||
if (!$gm['gm_id']) {
|
||||
if ($count == 1) {
|
||||
alert('존재하지 않는 자료입니다.');
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$sql = " delete from {$g5['group_member_table']} where gm_id = '$gm_id' ";
|
||||
sql_query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
if ($w == 'ld') {
|
||||
goto_url('./boardgroupmember_list.php?gr_id=' . $gr_id);
|
||||
} else {
|
||||
goto_url('./boardgroupmember_form.php?mb_id=' . $mb_id);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
$sub_menu = "100510";
|
||||
require_once './_common.php';
|
||||
|
||||
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
|
||||
alert('사용할 수 없는 기능입니다.', correct_goto_url(G5_ADMIN_URL));
|
||||
}
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
$g5['title'] = 'Browscap 업데이트';
|
||||
require_once './admin.head.php';
|
||||
?>
|
||||
|
||||
<div id="processing">
|
||||
<p>Browscap 정보를 업데이트하시려면 아래 업데이트 버튼을 클릭해 주세요.</p>
|
||||
<button type="button" id="run_update">업데이트</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$("#run_update").on("click", function() {
|
||||
$("#processing").html('<div class="update_processing"></div><p>Browscap 정보를 업데이트 중입니다.</p>');
|
||||
|
||||
$.ajax({
|
||||
url: "./browscap_update.php",
|
||||
async: true,
|
||||
cache: false,
|
||||
dataType: "html",
|
||||
success: function(data) {
|
||||
if (data != "") {
|
||||
alert(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
$("#processing").html("<div class='check_processing'></div><p>Browscap 정보를 업데이트 했습니다.</p>");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
$sub_menu = "100520";
|
||||
require_once './_common.php';
|
||||
|
||||
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
|
||||
alert('사용할 수 없는 기능입니다.', correct_goto_url(G5_ADMIN_URL));
|
||||
}
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
$rows = isset($_GET['rows']) ? preg_replace('#[^0-9]#', '', $_GET['rows']) : 0;
|
||||
if (!$rows) {
|
||||
$rows = 100;
|
||||
}
|
||||
|
||||
$g5['title'] = '접속로그 변환';
|
||||
require_once './admin.head.php';
|
||||
?>
|
||||
|
||||
<div id="processing">
|
||||
<p>접속로그 정보를 Browscap 정보로 변환하시려면 아래 업데이트 버튼을 클릭해 주세요.</p>
|
||||
<button type="button" id="run_update">업데이트</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$(document).on("click", "#run_update", function() {
|
||||
$("#processing").html('<div class="update_processing"></div><p>Browscap 정보로 변환 중입니다.</p>');
|
||||
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "./browscap_converter.php",
|
||||
data: {
|
||||
rows: "<?php echo strval($rows); ?>"
|
||||
},
|
||||
async: true,
|
||||
cache: false,
|
||||
dataType: "html",
|
||||
success: function(data) {
|
||||
$("#processing").html(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
ini_set('memory_limit', '-1');
|
||||
require_once './_common.php';
|
||||
|
||||
// clean the output buffer
|
||||
ob_end_clean();
|
||||
|
||||
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
|
||||
die('사용할 수 없는 기능입니다.');
|
||||
}
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
die('최고관리자로 로그인 후 실행해 주세요.');
|
||||
}
|
||||
|
||||
// browscap cache 파일 체크
|
||||
if (!is_file(G5_DATA_PATH . '/cache/browscap_cache.php')) {
|
||||
echo '<p>Browscap 정보가 없습니다. 아래 링크로 이동해 Browscap 정보를 업데이트 하세요.</p>' . PHP_EOL;
|
||||
echo '<p><a href="' . G5_ADMIN_URL . '/browscap.php">Browscap 업데이트</a></p>' . PHP_EOL;
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once G5_PLUGIN_PATH . '/browscap/Browscap.php';
|
||||
$browscap = new phpbrowscap\Browscap(G5_DATA_PATH . '/cache');
|
||||
$browscap->doAutoUpdate = false;
|
||||
$browscap->cacheFilename = 'browscap_cache.php';
|
||||
|
||||
// 데이터 변환
|
||||
$rows = isset($_GET['rows']) ? preg_replace('#[^0-9]#', '', $_GET['rows']) : 0;
|
||||
if (!$rows) {
|
||||
$rows = 100;
|
||||
}
|
||||
|
||||
$sql_common = " from {$g5['visit_table']} where vi_agent <> '' and ( vi_browser = '' or vi_os = '' or vi_device = '' ) ";
|
||||
$sql_order = " order by vi_id desc ";
|
||||
$sql_limit = " limit 0, " . strval($rows) . " ";
|
||||
|
||||
$sql = " select count(vi_id) as cnt $sql_common ";
|
||||
$row = sql_fetch($sql);
|
||||
$total_count = $row['cnt'];
|
||||
|
||||
$sql = " select vi_id, vi_agent, vi_browser, vi_os, vi_device
|
||||
$sql_common
|
||||
$sql_order
|
||||
$sql_limit ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
$cnt = 0;
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$info = $browscap->getBrowser($row['vi_agent']);
|
||||
|
||||
$brow = $row['vi_browser'];
|
||||
if (!$brow) {
|
||||
$brow = $info->Comment;
|
||||
}
|
||||
|
||||
$os = $row['vi_os'];
|
||||
if (!$os) {
|
||||
$os = $info->Platform;
|
||||
}
|
||||
|
||||
$device = $row['vi_device'];
|
||||
if (!$device) {
|
||||
$device = $info->Device_Type;
|
||||
}
|
||||
|
||||
$sql2 = " update {$g5['visit_table']}
|
||||
set vi_browser = '$brow',
|
||||
vi_os = '$os',
|
||||
vi_device = '$device'
|
||||
where vi_id = '{$row['vi_id']}' ";
|
||||
sql_query($sql2);
|
||||
|
||||
$cnt++;
|
||||
}
|
||||
|
||||
if (($total_count - $cnt) == 0 || $total_count == 0) {
|
||||
echo '<div class="check_processing"></div><p>변환완료</p>';
|
||||
} else {
|
||||
echo '<p>총 ' . number_format($total_count) . '건 중 ' . number_format($cnt) . '건 변환완료<br><br>접속로그를 추가로 변환하시려면 아래 업데이트 버튼을 클릭해 주세요.</p><button type="button" id="run_update">업데이트</button>';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
ini_set('memory_limit', '-1');
|
||||
|
||||
$sub_menu = "100510";
|
||||
require_once './_common.php';
|
||||
|
||||
// clean the output buffer
|
||||
ob_end_clean();
|
||||
|
||||
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
|
||||
die('사용할 수 없는 기능입니다.');
|
||||
}
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
die('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
require_once G5_PLUGIN_PATH . '/browscap/Browscap.php';
|
||||
|
||||
$browscap = new phpbrowscap\Browscap(G5_DATA_PATH . '/cache');
|
||||
$browscap->updateMethod = 'cURL';
|
||||
$browscap->cacheFilename = 'browscap_cache.php';
|
||||
$browscap->updateCache();
|
||||
|
||||
die('');
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
$sub_menu = '100900';
|
||||
require_once './_common.php';
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.', G5_URL);
|
||||
}
|
||||
|
||||
@require_once './safe_check.php';
|
||||
if (function_exists('social_log_file_delete')) {
|
||||
social_log_file_delete();
|
||||
}
|
||||
|
||||
run_event('adm_cache_file_delete_before');
|
||||
|
||||
$g5['title'] = '캐시파일 일괄삭제';
|
||||
require_once './admin.head.php';
|
||||
?>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
완료 메세지가 나오기 전에 프로그램의 실행을 중지하지 마십시오.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
flush();
|
||||
|
||||
if (!$dir = @opendir(G5_DATA_PATH . '/cache')) {
|
||||
echo '<p>캐시디렉토리를 열지못했습니다.</p>';
|
||||
}
|
||||
|
||||
$cnt = 0;
|
||||
echo '<ul class="session_del">' . PHP_EOL;
|
||||
|
||||
$files = glob(G5_DATA_PATH . '/cache/latest-*');
|
||||
$content_files = glob(G5_DATA_PATH . '/cache/content-*');
|
||||
|
||||
$files = array_merge($files, $content_files);
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $cache_file) {
|
||||
$cnt++;
|
||||
unlink($cache_file);
|
||||
echo '<li>' . $cache_file . '</li>' . PHP_EOL;
|
||||
|
||||
flush();
|
||||
|
||||
if ($cnt % 10 == 0) {
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_event('adm_cache_file_delete');
|
||||
|
||||
echo '<li>완료됨</li></ul>' . PHP_EOL;
|
||||
echo '<div class="local_desc01 local_desc"><p><strong>최신글 캐시파일 ' . $cnt . '건 삭제 완료됐습니다.</strong><br>프로그램의 실행을 끝마치셔도 좋습니다.</p></div>' . PHP_EOL;
|
||||
?>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
$sub_menu = '100910';
|
||||
require_once './_common.php';
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.', G5_URL);
|
||||
}
|
||||
|
||||
$g5['title'] = '캡챠파일 일괄삭제';
|
||||
require_once './admin.head.php';
|
||||
?>
|
||||
|
||||
<div class="local_desc02 local_desc">
|
||||
<p>
|
||||
완료 메세지가 나오기 전에 프로그램의 실행을 중지하지 마십시오.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
flush();
|
||||
|
||||
if (!$dir = @opendir(G5_DATA_PATH . '/cache')) {
|
||||
echo '<p>캐시디렉토리를 열지못했습니다.</p>';
|
||||
}
|
||||
|
||||
$cnt = 0;
|
||||
echo '<ul class="session_del">' . PHP_EOL;
|
||||
|
||||
$files = glob(G5_DATA_PATH . '/cache/?captcha-*');
|
||||
if (is_array($files)) {
|
||||
$before_time = G5_SERVER_TIME - 3600; // 한시간전
|
||||
foreach ($files as $gcaptcha_file) {
|
||||
$modification_time = filemtime($gcaptcha_file); // 파일접근시간
|
||||
|
||||
if ($modification_time > $before_time) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cnt++;
|
||||
unlink($gcaptcha_file);
|
||||
echo '<li>' . $gcaptcha_file . '</li>' . PHP_EOL;
|
||||
|
||||
flush();
|
||||
|
||||
if ($cnt % 10 == 0) {
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo '<li>완료됨</li></ul>' . PHP_EOL;
|
||||
echo '<div class="local_desc01 local_desc"><p><strong>캡챠파일 ' . $cnt . '건의 삭제 완료됐습니다.</strong><br>프로그램의 실행을 끝마치셔도 좋습니다.</p></div>' . PHP_EOL;
|
||||
?>
|
||||
|
||||
<?php
|
||||
require_once './admin.tail.php';
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
define('G5_IS_ADMIN', true);
|
||||
require_once '../../common.php';
|
||||
require_once G5_ADMIN_PATH . '/admin.lib.php';
|
||||
|
||||
// 💡 [추가] 코드 관리자 모듈 테이블 정의
|
||||
$g5['ui_manager_table'] = G5_TABLE_PREFIX.'ui_manager';
|
||||
$g5['form_category_table'] = G5_TABLE_PREFIX.'form_category';
|
||||
$g5['common_lang_table'] = G5_TABLE_PREFIX.'common_lang';
|
||||
$g5['form_option_history_table'] = G5_TABLE_PREFIX.'form_option_history';
|
||||
|
||||
if (isset($token)) {
|
||||
$token = @htmlspecialchars(strip_tags($token), ENT_QUOTES);
|
||||
}
|
||||
|
||||
run_event('admin_common');
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 💡 [수정] 700번대 최상위 메뉴 배열에 아이콘 클래스('fa-puzzle-piece')를 추가합니다.
|
||||
$menu['menu700'][] = array('700000', 'UI/폼 관리', G5_ADMIN_URL.'/code_manager/ui_manager_list.php', 'code_manager', 'fa-puzzle-piece');
|
||||
|
||||
// 'UI/폼 관리'의 하위 메뉴들을 정의합니다.
|
||||
$menu['menu700'][] = array('700100', 'UI 리소스 관리', G5_ADMIN_URL.'/code_manager/ui_manager_list.php', 'ui_resource_manager');
|
||||
$menu['menu700'][] = array('700900', '솔루션 설치', G5_ADMIN_URL.'/code_manager/install.php', 'ui_solution_install');
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
$sub_menu = '700100';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 💡 [추가] w 값을 먼저 정의합니다.
|
||||
$w = isset($_REQUEST['w']) ? substr(trim($_REQUEST['w']), 0, 1) : '';
|
||||
|
||||
// ==================================================================
|
||||
// 💡 [핵심 수정] 폼 제출 처리 로직 (신규/수정)
|
||||
// ==================================================================
|
||||
// POST 요청이고, w값이 넘어왔을 때 (신규='', 수정='u') 처리
|
||||
if (isset($_POST['w']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
check_admin_token();
|
||||
|
||||
$w_from_post = $_POST['w']; // POST로 받은 w값을 기준으로 처리
|
||||
$um_id = (int)$_POST['um_id'];
|
||||
$parent_id = (int)$_POST['parent_id'];
|
||||
$fc_key = trim($_POST['fc_key']);
|
||||
$cl_name = trim($_POST['cl_name']);
|
||||
$fc_order = (int)$_POST['fc_order'];
|
||||
$is_used = (int)$_POST['is_used'];
|
||||
|
||||
if (!$um_id) {
|
||||
alert('잘못된 접근입니다.');
|
||||
}
|
||||
if (!$fc_key) {
|
||||
alert('옵션 키(Key)를 입력해주세요.');
|
||||
}
|
||||
if (!$cl_name) {
|
||||
alert('옵션 이름을 입력해주세요.');
|
||||
}
|
||||
|
||||
// 1. g5_form_category 테이블에 마스터 정보 저장
|
||||
$sql_common = "
|
||||
um_id = '{$um_id}',
|
||||
parent_id = '{$parent_id}',
|
||||
fc_key = '{$fc_key}',
|
||||
fc_order = '{$fc_order}',
|
||||
is_used = '{$is_used}',
|
||||
updated_at = '" . G5_TIME_YMDHIS . "',
|
||||
updated_by = '{$member['mb_id']}'
|
||||
";
|
||||
|
||||
if ($w_from_post == 'u') { // 수정
|
||||
$fc_id = (int)$_POST['fc_id'];
|
||||
if (!$fc_id) alert('fc_id 값이 없습니다.');
|
||||
|
||||
$sql = "UPDATE {$g5['form_category_table']} SET {$sql_common} WHERE fc_id = '{$fc_id}'";
|
||||
sql_query($sql);
|
||||
} else { // 신규
|
||||
$sql_common .= ", created_at = '" . G5_TIME_YMDHIS . "', created_by = '{$member['mb_id']}'";
|
||||
$sql = "INSERT INTO {$g5['form_category_table']} SET {$sql_common}";
|
||||
sql_query($sql);
|
||||
$fc_id = sql_insert_id();
|
||||
}
|
||||
|
||||
// 2. g5_common_lang 테이블에 다국어 이름 저장 (없으면 생성, 있으면 수정)
|
||||
$sql = "SELECT cl_id FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['form_category_table']}' AND target_id = '{$fc_id}' AND lang_code = 'ko'";
|
||||
$lang_row = sql_fetch($sql);
|
||||
|
||||
if (isset($lang_row['cl_id'])) {
|
||||
$sql = "UPDATE {$g5['common_lang_table']} SET cl_name = '{$cl_name}', updated_at = '" . G5_TIME_YMDHIS . "', updated_by = '{$member['mb_id']}' WHERE cl_id = '{$lang_row['cl_id']}'";
|
||||
} else {
|
||||
$sql = "INSERT INTO {$g5['common_lang_table']} SET target_table = '{$g5['form_category_table']}', target_id = '{$fc_id}', lang_code = 'ko', cl_name = '{$cl_name}', updated_at = '" . G5_TIME_YMDHIS . "', updated_by = '{$member['mb_id']}'";
|
||||
}
|
||||
sql_query($sql);
|
||||
|
||||
goto_url("./category_list.php?um_id=$um_id");
|
||||
}
|
||||
// ==================================================================
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
// 그룹 ID가 없으면 되돌려 보냄
|
||||
$um_id = isset($_GET['um_id']) ? (int)$_GET['um_id'] : 0;
|
||||
if (!$um_id) {
|
||||
alert('UI 리소스 ID가 올바르지 않습니다.', './ui_manager_list.php');
|
||||
}
|
||||
|
||||
// 현재 관리하려는 리소스의 정보를 가져옴
|
||||
$sql = "SELECT * FROM {$g5['ui_manager_table']} WHERE um_id = '{$um_id}'";
|
||||
$ui_resource = sql_fetch($sql);
|
||||
|
||||
if (!isset($ui_resource['um_id']) || $ui_resource['resource_type'] != 'DATA') {
|
||||
alert('존재하지 않거나 데이터 타입이 아닌 리소스입니다.', './ui_manager_list.php');
|
||||
}
|
||||
|
||||
// 계층형으로 정렬된 카테고리 목록을 가져오는 함수
|
||||
function get_category_view_list($um_id)
|
||||
{
|
||||
global $g5;
|
||||
$sql = "SELECT
|
||||
A.*,
|
||||
B.cl_name
|
||||
FROM
|
||||
{$g5['form_category_table']} AS A
|
||||
LEFT JOIN
|
||||
{$g5['common_lang_table']} AS B
|
||||
ON
|
||||
(A.fc_id = B.target_id AND B.target_table = '{$g5['form_category_table']}' AND B.lang_code = 'ko')
|
||||
WHERE
|
||||
A.um_id = '{$um_id}' AND A.is_deleted = 0
|
||||
ORDER BY
|
||||
A.parent_id, A.fc_order, A.fc_id";
|
||||
$result = sql_query($sql);
|
||||
$categories = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$categories[] = $row;
|
||||
}
|
||||
|
||||
$view_list = [];
|
||||
// 재귀 함수를 사용하여 계층 구조를 평탄화하고 depth를 추가
|
||||
generate_category_list($categories, 0, 0, $view_list);
|
||||
return $view_list;
|
||||
}
|
||||
|
||||
function generate_category_list(&$source, $parent_id, $depth, &$result_list)
|
||||
{
|
||||
foreach ($source as $item) {
|
||||
if ($item['parent_id'] == $parent_id) {
|
||||
$item['depth'] = $depth;
|
||||
$result_list[] = $item;
|
||||
generate_category_list($source, $item['fc_id'], $depth + 1, $result_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$category_list = get_category_view_list($um_id);
|
||||
$category_list_count = count($category_list);
|
||||
|
||||
$g5['title'] = get_text($ui_resource['resource_desc']) . ' : 옵션/카테고리 관리';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.1">', 0);
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
<strong>'<?php echo get_text($ui_resource['resource_desc']); ?>'</strong> 리소스에 포함될 옵션 또는 카테고리를 관리합니다.<br>
|
||||
'부모 카테고리'를 지정하여 대/중/소 분류와 같은 계층 구조를 만들 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section id="code_manager">
|
||||
<div class="code-manager-header">
|
||||
<h2 class="code-manager-title">
|
||||
<a href="./ui_manager_list.php" class="btn btn_02">리소스 목록으로</a>
|
||||
<?php echo get_text($ui_resource['resource_desc']); ?>
|
||||
(<code><?php echo get_text($ui_resource['resource_code']); ?></code>)
|
||||
</h2>
|
||||
<div class="code-manager-actions">
|
||||
<button type="button" id="add-category-btn" class="btn btn_01">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> 새 옵션 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 옵션(카테고리) 추가 폼 -->
|
||||
<div id="category-form-container" style="display: none; margin-top: 20px;">
|
||||
<form name="fcategoryform" id="fcategoryform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
|
||||
<input type="hidden" name="w" value="">
|
||||
<input type="hidden" name="um_id" value="<?php echo $um_id; ?>">
|
||||
<input type="hidden" name="fc_id" value="">
|
||||
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>옵션/카테고리 추가/수정 폼</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="parent_id">부모 카테고리</label></th>
|
||||
<td>
|
||||
<select name="parent_id" id="parent_id">
|
||||
<option value="0">최상위 카테고리</option>
|
||||
<?php foreach ($category_list as $cat) : ?>
|
||||
<option value="<?php echo $cat['fc_id']; ?>">
|
||||
<?php echo str_repeat(' ', $cat['depth']); ?><?php echo get_text($cat['cl_name']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="fc_key">옵션 키(Key)</label></th>
|
||||
<td>
|
||||
<input type="text" name="fc_key" id="fc_key" required class="required frm_input" size="30">
|
||||
<span class="frm_info">DB에 저장될 고유한 값입니다. (영문, 숫자, _ 사용)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="cl_name">옵션 이름</label></th>
|
||||
<td>
|
||||
<input type="text" name="cl_name" id="cl_name" required class="required frm_input" size="50">
|
||||
<span class="frm_info">화면에 표시될 이름입니다.</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="fc_order">정렬순서</label></th>
|
||||
<td>
|
||||
<input type="number" name="fc_order" value="0" id="fc_order" class="frm_input" size="5">
|
||||
<span class="frm_info">숫자가 낮을수록 먼저 표시됩니다.</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">사용 여부</th>
|
||||
<td>
|
||||
<label><input type="radio" name="is_used" value="1" checked> 사용함</label>
|
||||
<label><input type="radio" name="is_used" value="0"> 사용안함</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<button type="button" id="cancel-category-btn" class="btn_cancel btn">취소</button>
|
||||
<input type="submit" value="저장" class="btn_submit btn" accesskey="s">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 등록된 옵션(카테고리) 목록 테이블 -->
|
||||
<div class="tbl_head01 tbl_wrap" style="margin-top: 20px;">
|
||||
<table>
|
||||
<caption>옵션/카테고리 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">카테고리 이름</th>
|
||||
<th scope="col" style="width: 150px;">옵션 키 (Key)</th>
|
||||
<th scope="col" style="width: 100px;">부모 ID</th>
|
||||
<th scope="col" style="width: 60px;">순서</th>
|
||||
<th scope="col" style="width: 60px;">사용</th>
|
||||
<th scope="col" style="width: 180px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($category_list_count > 0) : ?>
|
||||
<?php foreach ($category_list as $cat) : ?>
|
||||
<tr>
|
||||
<td class="td_left category-name-depth-<?php echo $cat['depth']; ?>">
|
||||
<?php if ($cat['depth'] > 0) : ?>
|
||||
<span class="depth-prefix">└</span>
|
||||
<?php endif; ?>
|
||||
<?php echo get_text($cat['cl_name']); ?>
|
||||
</td>
|
||||
<td><?php echo get_text($cat['fc_key']); ?></td>
|
||||
<td><?php echo $cat['parent_id']; ?></td>
|
||||
<td><?php echo $cat['fc_order']; ?></td>
|
||||
<td><?php echo $cat['is_used'] ? 'Y' : 'N'; ?></td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<button type="button" class="btn btn_02 btn_edit_category"
|
||||
data-fc_id="<?php echo $cat['fc_id']; ?>"
|
||||
data-parent_id="<?php echo $cat['parent_id']; ?>"
|
||||
data-fc_key="<?php echo get_text($cat['fc_key']); ?>"
|
||||
data-cl_name="<?php echo get_text($cat['cl_name']); ?>"
|
||||
data-fc_order="<?php echo $cat['fc_order']; ?>"
|
||||
data-is_used="<?php echo $cat['is_used']; ?>">수정
|
||||
</button>
|
||||
<a href="./lang_manager.php?target_table=<?php echo $g5['form_category_table']; ?>&target_id=<?php echo $cat['fc_id']; ?>"
|
||||
class="btn btn_01">다국어</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<tr class="empty_table">
|
||||
<td colspan="6">등록된 옵션이 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/ui_manager.js?ver=1.4"></script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
$sub_menu = '100900'; // admin.menu100.php 에 정의된 메뉴 코드
|
||||
include_once('./_common.php');
|
||||
|
||||
// ==================================================================
|
||||
// 💡 [핵심] 폼 제출 처리 로직
|
||||
// ==================================================================
|
||||
if (isset($w) && $w == 'u' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
check_admin_token();
|
||||
|
||||
// 입력값 정리
|
||||
$screen_code = trim($_POST['screen_code']);
|
||||
$group_code = trim($_POST['group_code']);
|
||||
$resource_code = trim($_POST['resource_code']);
|
||||
$resource_type = trim($_POST['resource_type']);
|
||||
$resource_desc = trim($_POST['resource_desc']);
|
||||
$cl_name = isset($_POST['cl_name']) ? trim($_POST['cl_name']) : ''; // LABEL 타입일 때만 넘어옴
|
||||
|
||||
// 유효성 검사
|
||||
if (!$screen_code || !$group_code || !$resource_code || !$resource_type) {
|
||||
alert('필수 항목을 모두 입력해주세요.');
|
||||
}
|
||||
if ($resource_type == 'LABEL' && !$cl_name) {
|
||||
alert('UI 라벨 타입은 한국어 라벨명을 필수로 입력해야 합니다.');
|
||||
}
|
||||
|
||||
// 중복 검사
|
||||
$sql = "SELECT COUNT(*) as cnt FROM {$g5['ui_manager_table']} WHERE screen_code = '{$screen_code}' AND group_code = '{$group_code}' AND resource_code = '{$resource_code}'";
|
||||
$row = sql_fetch($sql);
|
||||
if ($row['cnt']) {
|
||||
alert('이미 등록된 리소스 코드입니다.');
|
||||
}
|
||||
|
||||
// 1. g5_ui_manager 테이블에 리소스 '설계' 정보 저장
|
||||
$sql = "INSERT INTO {$g5['ui_manager_table']}
|
||||
SET screen_code = '{$screen_code}',
|
||||
group_code = '{$group_code}',
|
||||
resource_code = '{$resource_code}',
|
||||
resource_type = '{$resource_type}',
|
||||
resource_desc = '{$resource_desc}',
|
||||
is_used = '1',
|
||||
created_at = '".G5_TIME_YMDHIS."',
|
||||
created_by = '{$member['mb_id']}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'";
|
||||
sql_query($sql);
|
||||
$um_id = sql_insert_id();
|
||||
|
||||
// 2. 리소스 타입이 'LABEL'인 경우, g5_common_lang 테이블에 실제 텍스트 저장
|
||||
if ($resource_type == 'LABEL' && $cl_name) {
|
||||
$sql = "INSERT INTO {$g5['common_lang_table']}
|
||||
SET target_table = '{$g5['ui_manager_table']}',
|
||||
target_id = '{$um_id}',
|
||||
lang_code = 'ko',
|
||||
cl_name = '{$cl_name}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'";
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
goto_url('./code_list.php');
|
||||
}
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
// 등록된 리소스 목록 조회
|
||||
$sql = "SELECT * FROM {$g5['ui_manager_table']} ORDER BY screen_code, group_code, resource_code";
|
||||
$result = sql_query($sql);
|
||||
$resource_list = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$resource_list[] = $row;
|
||||
}
|
||||
$resource_list_count = count($resource_list);
|
||||
|
||||
$g5['title'] = 'UI 리소스 관리';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
// 솔루션 전용 CSS/JS 파일을 불러옵니다.
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css">', 0);
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
웹사이트의 모든 화면에 사용되는 텍스트(라벨)와 선택 옵션(데이터)을 체계적으로 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section id="code_manager">
|
||||
<div class="code-manager-header">
|
||||
<h2 class="code-manager-title">UI 리소스 목록</h2>
|
||||
<div class="code-manager-actions">
|
||||
<button type="button" id="add-resource-btn" class="btn btn_01">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> 새 리소스 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 리소스 추가 폼 -->
|
||||
<div id="resource-form-container" style="display: none;">
|
||||
<form name="fresourceform" id="fresourceform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
|
||||
<input type="hidden" name="w" value="u">
|
||||
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>UI 리소스 추가 폼</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="screen_code">화면 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="screen_code" id="screen_code" required class="required frm_input" size="30">
|
||||
<span class="frm_info">리소스가 사용될 화면의 고유 코드 (예: order_form, member_join)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="group_code">그룹 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="group_code" id="group_code" required class="required frm_input" size="30">
|
||||
<span class="frm_info">화면 내에서 리소스를 묶어줄 그룹 코드 (예: address_info, common_options)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">리소스 타입</th>
|
||||
<td>
|
||||
<label><input type="radio" name="resource_type" value="LABEL" checked> UI 라벨 (단일 텍스트)</label>
|
||||
<label><input type="radio" name="resource_type" value="DATA"> 데이터 (선택 옵션)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="resource-type-field" id="label-field">
|
||||
<th scope="row"><label for="cl_name">한국어 라벨명</label></th>
|
||||
<td>
|
||||
<input type="text" name="cl_name" id="cl_name" class="frm_input" size="50">
|
||||
<span class="frm_info">화면에 표시될 실제 텍스트 (예: 집의 유형, 창호 재질)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="resource_code">리소스 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="resource_code" id="resource_code" required class="required frm_input" size="30">
|
||||
<span class="frm_info">개발자가 이 리소스를 호출할 때 사용할 고유 코드 (예: house_type_label, house_type_data)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="resource_desc">설명</label></th>
|
||||
<td>
|
||||
<input type="text" name="resource_desc" id="resource_desc" class="frm_input" size="80">
|
||||
<span class="frm_info">이 리소스의 용도에 대한 설명 (관리자 참고용)</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<button type="button" id="cancel-resource-btn" class="btn_cancel btn">취소</button>
|
||||
<input type="submit" value="리소스 등록" class="btn_submit btn" accesskey="s">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 등록된 리소스 목록 테이블 -->
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>UI 리소스 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">화면 코드</th>
|
||||
<th scope="col">그룹 코드</th>
|
||||
<th scope="col">리소스 코드</th>
|
||||
<th scope="col">타입</th>
|
||||
<th scope="col">설명</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($resource_list_count > 0) : ?>
|
||||
<?php foreach ($resource_list as $res) : ?>
|
||||
<tr>
|
||||
<td><?php echo get_text($res['screen_code']); ?></td>
|
||||
<td><?php echo get_text($res['group_code']); ?></td>
|
||||
<td><?php echo get_text($res['resource_code']); ?></td>
|
||||
<td><?php echo $res['resource_type']; ?></td>
|
||||
<td class="td_left"><?php echo get_text($res['resource_desc']); ?></td>
|
||||
<td class="td_mng">
|
||||
<?php if ($res['resource_type'] == 'DATA') : ?>
|
||||
<a href="./category_list.php?um_id=<?php echo $res['um_id']; ?>" class="btn btn_03">옵션 관리</a>
|
||||
<?php endif; ?>
|
||||
<a href="./category_list.php?w=u&um_id=<?php echo $res['um_id']; ?>" class="btn btn_02">수정</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<tr class="empty_table">
|
||||
<td colspan="6">등록된 리소스가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/code_manager.js"></script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
@@ -0,0 +1,196 @@
|
||||
/* 폼 옵션 관리 솔루션 전용 스타일 */
|
||||
#code_manager .code-manager-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
#code_manager .code-manager-title {
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
#code_manager .code-manager-actions .btn {
|
||||
padding: 5px 12px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 새 리소스 추가 폼 */
|
||||
#resource-form-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#resource-form-container .frm_info {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 💡 [추가] 아코디언 UI 스타일 */
|
||||
#resource-list-accordion {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.accordion-item {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.accordion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #f7f7f7;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
.accordion-header:hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
.accordion-header .screen-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.accordion-header .screen-title .fa {
|
||||
margin-right: 8px;
|
||||
color: #555;
|
||||
}
|
||||
.accordion-header .resource-count {
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
background-color: #888;
|
||||
padding: 3px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.accordion-header .accordion-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.accordion-item.active .accordion-header .accordion-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.accordion-content {
|
||||
display: none;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.accordion-content .tbl_wrap {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 💡 [추가] 리소스 타입 시각적 구분 */
|
||||
.res-type-label, .res-type-data {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.res-type-label {
|
||||
background-color: #3498db; /* 파란색 계열 */
|
||||
}
|
||||
.res-type-data {
|
||||
background-color: #2ecc71; /* 녹색 계열 */
|
||||
}
|
||||
|
||||
/* 기타 스타일 */
|
||||
.tbl_head01 .td_left {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
/* ... 기존 CSS 코드 ... */
|
||||
|
||||
/* 💡 [추가] 카테고리 목록 들여쓰기 스타일 */
|
||||
.category-name-depth-0 { font-weight: bold; }
|
||||
.category-name-depth-1 { padding-left: 25px !important; }
|
||||
.category-name-depth-2 { padding-left: 50px !important; }
|
||||
.category-name-depth-3 { padding-left: 75px !important; }
|
||||
.category-name-depth-4 { padding-left: 100px !important; }
|
||||
|
||||
.depth-prefix {
|
||||
font-family: "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
|
||||
font-weight: normal;
|
||||
color: #aaa;
|
||||
margin-right: 5px;
|
||||
}
|
||||
/* ... 기존 CSS 코드 ... */
|
||||
|
||||
/* 💡 [추가] 삭제 버튼 스타일 */
|
||||
.btn_delete {
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
height: 28px;
|
||||
line-height: 26px;
|
||||
border: 1px solid #d43f3a;
|
||||
background: #d9534f;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn_delete:hover {
|
||||
background: #c9302c;
|
||||
border-color: #ac2925;
|
||||
}
|
||||
/* ... 기존 CSS 코드 ... */
|
||||
|
||||
/* 💡 [추가] 다국어 관리 페이지 스타일 */
|
||||
.h2_frm {
|
||||
margin: 20px 0 10px;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.td_alignc {
|
||||
text-align: center;
|
||||
}
|
||||
#flangform textarea {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
/* ... 기존 CSS 코드 ... */
|
||||
|
||||
/* 💡 [추가] 검색 폼 스타일 */
|
||||
#resource_search_form {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e9e9e9;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
#resource_search_form .h2_frm {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
#resource_search_form .search-form-inner {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
#resource_search_form select,
|
||||
#resource_search_form .frm_input {
|
||||
height: 35px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
#resource_search_form .btn_submit {
|
||||
height: 35px;
|
||||
padding: 0 20px;
|
||||
font-size: 1em;
|
||||
}
|
||||
/* ... 기존 CSS 코드 ... */
|
||||
|
||||
/* 💡 [추가] 페이징 스타일 */
|
||||
.pagination_wrap {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.pagination_wrap .pg_wrap {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
$sub_menu = '700900';
|
||||
include_once('./_common.php');
|
||||
include_once(__DIR__ . '/lib/SchemaManager.class.php');
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일에서 테이블 이름을 추출하는 함수
|
||||
*/
|
||||
function get_tables_from_sql_file($filepath) {
|
||||
$tables = [];
|
||||
if (!file_exists($filepath)) {
|
||||
return $tables;
|
||||
}
|
||||
|
||||
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `([^`]+)`/i', $line, $matches)) {
|
||||
$tables[] = $matches[1];
|
||||
}
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
$g5['title'] = 'UI 리소스 관리 솔루션 설치';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
$install_result = null;
|
||||
$delete_result = null;
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
// 💡 [수정] SQL 파일에서 테이블 목록 동적 로드
|
||||
$tables_to_check = get_tables_from_sql_file(__DIR__ . '/install.sql');
|
||||
|
||||
if ($action === 'install') {
|
||||
check_admin_token();
|
||||
// ... (설치 로직은 기존과 동일)
|
||||
$copy_results = [];
|
||||
$solution_files = [
|
||||
['source' => __DIR__ . '/lib/ui_manager.extend.php', 'target' => G5_EXTEND_PATH . '/ui_manager.extend.php', 'desc' => '핵심 기능 파일'],
|
||||
['source' => __DIR__ . '/admin.menu700.code_manager.php', 'target' => G5_ADMIN_PATH . '/admin.menu700.code_manager.php', 'desc' => '관리자 메뉴 파일']
|
||||
];
|
||||
foreach ($solution_files as $file) {
|
||||
$key = $file['target'];
|
||||
if (file_exists($file['source']) && is_writable(dirname($file['target']))) {
|
||||
@copy($file['source'], $file['target']);
|
||||
}
|
||||
}
|
||||
$sql_file = __DIR__ . '/install.sql';
|
||||
$db_results = [];
|
||||
try {
|
||||
$schemaManager = new SchemaManager($sql_file);
|
||||
$schemaManager->execute();
|
||||
$db_results = $schemaManager->get_results();
|
||||
} catch (Exception $e) { $db_results['errors'][] = $e->getMessage(); }
|
||||
$install_result = ['db' => $db_results];
|
||||
|
||||
} else if ($action === 'delete') {
|
||||
check_admin_token();
|
||||
$delete_result = ['tables' => [], 'menu' => ''];
|
||||
// 💡 [수정] 삭제할 테이블 목록도 동적으로 가져옴
|
||||
$tables_to_delete = get_tables_from_sql_file(__DIR__ . '/install.sql');
|
||||
foreach ($tables_to_delete as $table) {
|
||||
sql_query("DROP TABLE IF EXISTS `{$table}`", false);
|
||||
$delete_result['tables'][] = $table;
|
||||
}
|
||||
// $menu_file = G5_ADMIN_PATH . '/admin.menu700.code_manager.php';
|
||||
$solution_files = [
|
||||
['source' => __DIR__ . '/lib/ui_manager.extend.php', 'target' => G5_EXTEND_PATH . '/ui_manager.extend.php', 'desc' => '핵심 기능 파일'],
|
||||
['source' => __DIR__ . '/admin.menu700.code_manager.php', 'target' => G5_ADMIN_PATH . '/admin.menu700.code_manager.php', 'desc' => '관리자 메뉴 파일']
|
||||
];
|
||||
foreach ($solution_files as $file) {
|
||||
$key = $file['target'];
|
||||
var_dump($key);
|
||||
if (file_exists($key)) {
|
||||
if (@unlink($key)) {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 성공';
|
||||
} else {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (file_exists($menu_file)) {
|
||||
// if (@unlink($menu_file)) {
|
||||
// $delete_result['menu'] = '메뉴 파일 삭제 성공';
|
||||
// } else {
|
||||
// $delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
$existing_tables = array();
|
||||
foreach ($tables_to_check as $table) {
|
||||
if (sql_query("SHOW TABLES LIKE '$table'", false) && sql_num_rows(sql_query("SHOW TABLES LIKE '$table'", false)) > 0) {
|
||||
$existing_tables[] = $table;
|
||||
}
|
||||
}
|
||||
$is_installed = count($existing_tables) == count($tables_to_check);
|
||||
?>
|
||||
|
||||
<style>
|
||||
.install-container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.install-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #AA20FF; }
|
||||
.install-header h1 { color: #AA20FF; margin-bottom: 10px; }
|
||||
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0; }
|
||||
.feature-card { padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center; }
|
||||
.feature-card i { font-size: 2em; color: #AA20FF; margin-bottom: 10px; }
|
||||
.status-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
.status-table th, .status-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
.status-table th { background-color: #fff; font-weight: bold; }
|
||||
.status-ok { color: #28a745; font-weight: bold; }
|
||||
.status-missing { color: #dc3545; font-weight: bold; }
|
||||
.install-btn { display: block; width: 200px; margin: 30px auto; padding: 15px 30px; background: #AA20FF; color: white; text-align: center; text-decoration: none; border-radius: 5px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; transition: background-color 0.3s; }
|
||||
.install-btn:hover { background: #8A1ACC; color: white; }
|
||||
.install-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
.alert { padding: 15px; margin: 20px 0; border-radius: 5px; }
|
||||
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.alert-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||||
.btn-secondary { background: #6c757d; color: white; border-color: #6c757d; padding: 5px 10px; border-radius: 4px; text-decoration: none; }
|
||||
.btn-secondary:hover { background: #5a6268; }
|
||||
.btn-danger { background: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
|
||||
.btn-danger:hover { background: #c82333; }
|
||||
.button-group { display: flex; justify-content: center; align-items: center; gap: 10px; }
|
||||
</style>
|
||||
|
||||
<div class="install-container">
|
||||
<div class="install-header">
|
||||
<h1><i class="fa fa-code"></i> UI 리소스 관리 솔루션</h1>
|
||||
<p>CSS, JS 등 UI 리소스를 효율적으로 관리하는 시스템</p>
|
||||
</div>
|
||||
|
||||
<?php if ($install_result): ?>
|
||||
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 작업 완료</h4><p>데이터베이스 설치 작업이 완료되었습니다.</p><p><a href="./ui_manager_list.php" class="btn btn-primary">UI 리소스 관리로 이동</a></p></div>
|
||||
<?php elseif ($delete_result): ?>
|
||||
<div class="alert alert-danger"><h4><i class="fa fa-trash"></i> 삭제 작업 완료</h4><p>솔루션 관련 데이터와 파일이 삭제되었습니다.</p><ul><?php foreach($delete_result['tables'] as $tbl) echo "<li>{$tbl} 테이블 삭제됨</li>"; ?><li><?php echo $delete_result['menu']; ?></li></ul></div>
|
||||
<?php elseif ($is_installed): ?>
|
||||
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 완료</h4><p>UI 리소스 관리 솔루션이 이미 설치되어 있습니다.</p><p><a href="./ui_manager_list.php" class="btn btn-primary">UI 리소스 관리로 이동</a></p></div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info"><h4><i class="fa fa-info-circle"></i> 설치 필요</h4><p>UI 리소스 관리 솔루션을 사용하기 위해 설치가 필요합니다.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3><i class="fa fa-database"></i> 설치 상태</h3>
|
||||
<table class="status-table">
|
||||
<thead><tr><th>테이블명</th><th>설명</th><th>상태</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($tables_to_check as $table): ?>
|
||||
<tr>
|
||||
<td><code><?php echo $table; ?></code></td>
|
||||
<td><?php echo array('g5_ui_manager' => 'UI 리소스 마스터', 'g5_form_category' => '계층형 폼 카테고리', 'g5_common_lang' => '공용 다국어 정보', 'g5_form_option_history' => '폼 옵션 변경 이력')[$table] ?? '데이터 테이블'; ?></td>
|
||||
<td>
|
||||
<?php if (in_array($table, $existing_tables)): ?>
|
||||
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
|
||||
<?php else: ?>
|
||||
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if (!$is_installed): ?>
|
||||
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_installed && !$install_result && !$delete_result): ?>
|
||||
<div class="button-group" style="text-align: center; margin-top: 20px;">
|
||||
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터와 파일이 영구적으로 삭제됩니다.');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,64 @@
|
||||
-- 1. [핵심] UI 리소스 마스터 테이블
|
||||
-- 이 테이블은 웹사이트의 모든 UI 요소(라벨, 데이터)의 '설계도' 역할을 합니다.
|
||||
CREATE TABLE IF NOT EXISTS `g5_ui_manager` (
|
||||
`um_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`screen_code` varchar(50) NOT NULL COMMENT '화면 코드 (e.g. order_form)',
|
||||
`group_code` varchar(50) NOT NULL COMMENT '화면 내 그룹 코드 (e.g. address_info)',
|
||||
`resource_code` varchar(50) NOT NULL COMMENT '리소스 코드 (개발자가 사용)',
|
||||
`resource_type` enum('LABEL','DATA') NOT NULL COMMENT '리소스 타입 (라벨, 데이터)',
|
||||
`resource_desc` varchar(255) DEFAULT NULL COMMENT '리소스에 대한 설명 (관리자용)',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
|
||||
`created_by` varchar(20) NOT NULL COMMENT '생성자',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일',
|
||||
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`um_id`),
|
||||
UNIQUE KEY `resource_identifier` (`screen_code`,`group_code`,`resource_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] UI 리소스 관리자';
|
||||
|
||||
-- 2. 계층형 카테고리 테이블 ('DATA' 타입 리소스가 사용할 데이터)
|
||||
-- um_id를 통해 어떤 리소스에 속한 데이터인지 명시합니다.
|
||||
CREATE TABLE IF NOT EXISTS `g5_form_category` (
|
||||
`fc_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '카테고리 고유 ID',
|
||||
`um_id` int(11) NOT NULL COMMENT 'UI 리소스 ID (g5_ui_manager.um_id)',
|
||||
`parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '부모 카테고리 ID (0이면 최상위)',
|
||||
`fc_key` varchar(255) NOT NULL COMMENT 'DB에 저장될 값 (고유값)',
|
||||
`fc_order` int(11) NOT NULL DEFAULT '0' COMMENT '정렬 순서',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
|
||||
`created_by` varchar(20) NOT NULL COMMENT '생성자',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일',
|
||||
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`fc_id`),
|
||||
KEY `um_id` (`um_id`),
|
||||
KEY `parent_id` (`parent_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] 계층형 폼 카테고리';
|
||||
|
||||
-- 3. 공용 다국어 정보 테이블
|
||||
-- 'LABEL' 타입의 실제 텍스트와 'DATA' 타입의 카테고리 이름을 모두 저장합니다.
|
||||
CREATE TABLE IF NOT EXISTS `g5_common_lang` (
|
||||
`cl_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`target_table` varchar(50) NOT NULL COMMENT '대상 테이블명',
|
||||
`target_id` int(11) NOT NULL COMMENT '대상 레코드 ID',
|
||||
`lang_code` varchar(10) NOT NULL COMMENT '언어 코드',
|
||||
`cl_name` varchar(255) NOT NULL COMMENT '화면에 표시될 이름/값',
|
||||
`cl_description` text COMMENT '부가 설명 (툴팁 등)',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일',
|
||||
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
|
||||
PRIMARY KEY (`cl_id`),
|
||||
UNIQUE KEY `target_lang` (`target_table`,`target_id`,`lang_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] 공용 다국어 정보';
|
||||
|
||||
-- 4. 변경 이력 테이블
|
||||
CREATE TABLE IF NOT EXISTS `g5_form_option_history` (
|
||||
`fh_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`table_name` varchar(50) NOT NULL COMMENT '변경된 테이블명',
|
||||
`record_id` int(11) NOT NULL COMMENT '변경된 레코드 ID',
|
||||
`action_type` varchar(10) NOT NULL COMMENT '작업 종류',
|
||||
`change_data` longtext COMMENT '변경된 데이터 (JSON)',
|
||||
`changed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경일',
|
||||
`changed_by` varchar(20) NOT NULL COMMENT '변경자',
|
||||
PRIMARY KEY (`fh_id`),
|
||||
KEY `table_name_record_id` (`table_name`,`record_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] 폼 옵션 변경 이력';
|
||||
@@ -0,0 +1,90 @@
|
||||
-- 1. 마스터 테이블: 옵션 그룹 (예: '집 유형', '창호 색상')
|
||||
CREATE TABLE IF NOT EXISTS `g5_form_group` (
|
||||
`fg_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`project_code` varchar(50) NOT NULL DEFAULT 'default' COMMENT '프로젝트 코드',
|
||||
`site_code` varchar(50) NOT NULL DEFAULT 'default' COMMENT '사이트 코드',
|
||||
`fg_code` varchar(50) NOT NULL COMMENT '그룹 코드 (프로그램에서 사용)',
|
||||
`fg_order` int(11) NOT NULL DEFAULT '0' COMMENT '정렬 순서',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
|
||||
`created_at` datetime NOT NULL COMMENT '생성일',
|
||||
`created_by` varchar(20) NOT NULL COMMENT '생성자',
|
||||
`updated_at` datetime NOT NULL COMMENT '수정일',
|
||||
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
|
||||
`fg_temp_1` varchar(255) DEFAULT NULL COMMENT '임시 필드 1',
|
||||
`fg_temp_2` varchar(255) DEFAULT NULL COMMENT '임시 필드 2',
|
||||
`fg_temp_3` varchar(255) DEFAULT NULL COMMENT '임시 필드 3',
|
||||
`fg_temp_4` text DEFAULT NULL COMMENT '임시 필드 4',
|
||||
`fg_temp_5` text DEFAULT NULL COMMENT '임시 필드 5',
|
||||
`fg_extra_1` varchar(255) DEFAULT NULL COMMENT '여분 필드 1',
|
||||
`fg_extra_2` varchar(255) DEFAULT NULL COMMENT '여분 필드 2',
|
||||
`fg_extra_3` varchar(255) DEFAULT NULL COMMENT '여분 필드 3',
|
||||
`fg_extra_4` text DEFAULT NULL COMMENT '여분 필드 4',
|
||||
`fg_extra_5` text DEFAULT NULL COMMENT '여분 필드 5',
|
||||
PRIMARY KEY (`fg_id`),
|
||||
UNIQUE KEY `project_site_fg_code` (`project_code`,`site_code`,`fg_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 폼 옵션 그룹';
|
||||
|
||||
-- 2. 디테일 테이블: 그룹에 속한 개별 옵션
|
||||
CREATE TABLE IF NOT EXISTS `g5_form_option` (
|
||||
`fo_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`fg_id` int(11) NOT NULL COMMENT '그룹 ID (g5_form_group.fg_id)',
|
||||
`fo_key` varchar(255) NOT NULL COMMENT 'DB에 저장될 값 (고유값)',
|
||||
`fo_order` int(11) NOT NULL DEFAULT '0' COMMENT '정렬 순서',
|
||||
`is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '기본 선택 여부 (1:기본값)',
|
||||
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
|
||||
`created_at` datetime NOT NULL COMMENT '생성일',
|
||||
`created_by` varchar(20) NOT NULL COMMENT '생성자',
|
||||
`updated_at` datetime NOT NULL COMMENT '수정일',
|
||||
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
|
||||
`fo_temp_1` varchar(255) DEFAULT NULL COMMENT '임시 필드 1',
|
||||
`fo_temp_2` varchar(255) DEFAULT NULL COMMENT '임시 필드 2',
|
||||
`fo_temp_3` varchar(255) DEFAULT NULL COMMENT '임시 필드 3',
|
||||
`fo_temp_4` text DEFAULT NULL COMMENT '임시 필드 4',
|
||||
`fo_temp_5` text DEFAULT NULL COMMENT '임시 필드 5',
|
||||
`fo_extra_1` varchar(255) DEFAULT NULL COMMENT '여분 필드 1',
|
||||
`fo_extra_2` varchar(255) DEFAULT NULL COMMENT '여분 필드 2',
|
||||
`fo_extra_3` varchar(255) DEFAULT NULL COMMENT '여분 필드 3',
|
||||
`fo_extra_4` text DEFAULT NULL COMMENT '여분 필드 4',
|
||||
`fo_extra_5` text DEFAULT NULL COMMENT '여분 필드 5',
|
||||
PRIMARY KEY (`fo_id`),
|
||||
KEY `fg_id` (`fg_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 폼 옵션 항목';
|
||||
|
||||
-- 3. [통합] 서브 디테일 테이블: 그룹과 옵션의 다국어 이름/설명
|
||||
CREATE TABLE IF NOT EXISTS `g5_common_lang` (
|
||||
`cl_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`target_table` varchar(50) NOT NULL COMMENT '대상 테이블명 (예: g5_form_group)',
|
||||
`target_id` int(11) NOT NULL COMMENT '대상 레코드 ID',
|
||||
`lang_code` varchar(10) NOT NULL COMMENT '언어 코드 (ko, en, ja)',
|
||||
`cl_name` varchar(255) NOT NULL COMMENT '화면에 표시될 이름/값',
|
||||
`cl_description` text COMMENT '부가 설명 (툴팁 등)',
|
||||
`updated_at` datetime NOT NULL COMMENT '수정일',
|
||||
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
|
||||
`cl_temp_1` varchar(255) DEFAULT NULL COMMENT '임시 필드 1',
|
||||
`cl_temp_2` varchar(255) DEFAULT NULL COMMENT '임시 필드 2',
|
||||
`cl_temp_3` varchar(255) DEFAULT NULL COMMENT '임시 필드 3',
|
||||
`cl_temp_4` text DEFAULT NULL COMMENT '임시 필드 4',
|
||||
`cl_temp_5` text DEFAULT NULL COMMENT '임시 필드 5',
|
||||
`cl_extra_1` varchar(255) DEFAULT NULL COMMENT '여분 필드 1',
|
||||
`cl_extra_2` varchar(255) DEFAULT NULL COMMENT '여분 필드 2',
|
||||
`cl_extra_3` varchar(255) DEFAULT NULL COMMENT '여분 필드 3',
|
||||
`cl_extra_4` text DEFAULT NULL COMMENT '여분 필드 4',
|
||||
`cl_extra_5` text DEFAULT NULL COMMENT '여분 필드 5',
|
||||
PRIMARY KEY (`cl_id`),
|
||||
UNIQUE KEY `target_lang` (`target_table`,`target_id`,`lang_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 공용 다국어 정보';
|
||||
|
||||
-- 4. 변경 이력 테이블
|
||||
CREATE TABLE IF NOT EXISTS `g5_form_option_history` (
|
||||
`fh_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
|
||||
`table_name` varchar(50) NOT NULL COMMENT '변경된 테이블명',
|
||||
`record_id` int(11) NOT NULL COMMENT '변경된 레코드 ID',
|
||||
`action_type` varchar(10) NOT NULL COMMENT '작업 종류 (INSERT, UPDATE, DELETE)',
|
||||
`change_data` longtext COMMENT '변경된 데이터 (JSON)',
|
||||
`changed_at` datetime NOT NULL COMMENT '변경일',
|
||||
`changed_by` varchar(20) NOT NULL COMMENT '변경자',
|
||||
PRIMARY KEY (`fh_id`),
|
||||
KEY `table_name_record_id` (`table_name`,`record_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 폼 옵션 변경 이력';
|
||||
@@ -0,0 +1,41 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const addResourceButton = document.getElementById('add-resource-btn');
|
||||
const resourceFormContainer = document.getElementById('resource-form-container');
|
||||
const cancelResourceButton = document.getElementById('cancel-resource-btn');
|
||||
const resourceTypeRadios = document.querySelectorAll('input[name="resource_type"]');
|
||||
const labelField = document.getElementById('label-field');
|
||||
|
||||
// '새 리소스 추가' 버튼 클릭 이벤트
|
||||
if (addResourceButton) {
|
||||
addResourceButton.addEventListener('click', function() {
|
||||
if (resourceFormContainer) resourceFormContainer.style.display = 'block';
|
||||
this.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// '취소' 버튼 클릭 이벤트
|
||||
if (cancelResourceButton) {
|
||||
cancelResourceButton.addEventListener('click', function() {
|
||||
if (resourceFormContainer) resourceFormContainer.style.display = 'none';
|
||||
if (addResourceButton) addResourceButton.style.display = 'inline-block';
|
||||
});
|
||||
}
|
||||
|
||||
// 💡 [핵심] 리소스 타입 라디오 버튼 변경 이벤트
|
||||
function toggleResourceTypeFields() {
|
||||
const selectedType = document.querySelector('input[name="resource_type"]:checked').value;
|
||||
if (selectedType === 'LABEL') {
|
||||
labelField.style.display = ''; // 테이블 행이므로 기본값으로
|
||||
} else {
|
||||
labelField.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypeRadios.length > 0) {
|
||||
resourceTypeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', toggleResourceTypeFields);
|
||||
});
|
||||
// 페이지 로드 시 초기 상태 설정
|
||||
toggleResourceTypeFields();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// =================================================================
|
||||
// 1. UI 리소스 목록 페이지 (ui_manager_list.php)
|
||||
// =================================================================
|
||||
const addResourceButton = document.getElementById('add-resource-btn');
|
||||
const resourceFormContainer = document.getElementById('resource-form-container');
|
||||
const cancelResourceButton = document.getElementById('cancel-resource-btn');
|
||||
const resourceTypeRadios = document.querySelectorAll('input[name="resource_type"]');
|
||||
const labelField = document.getElementById('label-field');
|
||||
const accordionItems = document.querySelectorAll('.accordion-item');
|
||||
const searchForm = document.getElementById('fsearch');
|
||||
const pageRowsSelect = document.getElementById('page_rows');
|
||||
|
||||
if (addResourceButton) {
|
||||
addResourceButton.addEventListener('click', function() {
|
||||
if (resourceFormContainer) resourceFormContainer.style.display = 'block';
|
||||
this.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelResourceButton) {
|
||||
cancelResourceButton.addEventListener('click', function() {
|
||||
if (resourceFormContainer) resourceFormContainer.style.display = 'none';
|
||||
if (addResourceButton) addResourceButton.style.display = 'inline-block';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleResourceTypeFields() {
|
||||
if (!document.querySelector('input[name="resource_type"]:checked')) return;
|
||||
const selectedType = document.querySelector('input[name="resource_type"]:checked').value;
|
||||
if (labelField) {
|
||||
labelField.style.display = (selectedType === 'LABEL') ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceTypeRadios.length > 0) {
|
||||
resourceTypeRadios.forEach(radio => radio.addEventListener('change', toggleResourceTypeFields));
|
||||
toggleResourceTypeFields();
|
||||
}
|
||||
|
||||
if (accordionItems.length > 0) {
|
||||
accordionItems.forEach(item => {
|
||||
const header = item.querySelector('.accordion-header');
|
||||
const content = item.querySelector('.accordion-content');
|
||||
if (header && content) {
|
||||
header.addEventListener('click', () => {
|
||||
item.classList.toggle('active');
|
||||
content.style.display = item.classList.contains('active') ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchForm) {
|
||||
searchForm.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
// 엔터키 입력 시 기본 동작(폼 제출)을 막지 않음
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pageRowsSelect) {
|
||||
pageRowsSelect.addEventListener('change', function() {
|
||||
if (searchForm) {
|
||||
searchForm.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 💡 [핵심] 옵션/카테고리 관리 페이지 (category_list.php) 로직 추가
|
||||
// =================================================================
|
||||
const addCategoryButton = document.getElementById('add-category-btn');
|
||||
const categoryFormContainer = document.getElementById('category-form-container');
|
||||
const cancelCategoryButton = document.getElementById('cancel-category-btn');
|
||||
const categoryForm = document.getElementById('fcategoryform');
|
||||
const editCategoryButtons = document.querySelectorAll('.btn_edit_category');
|
||||
|
||||
// "새 옵션 추가" 버튼 클릭
|
||||
if (addCategoryButton && categoryFormContainer) {
|
||||
addCategoryButton.addEventListener('click', function() {
|
||||
if (categoryForm) {
|
||||
categoryForm.reset();
|
||||
categoryForm.w.value = ''; // 신규 등록 모드
|
||||
categoryForm.fc_id.value = '';
|
||||
const submitButton = categoryForm.querySelector('input[type="submit"]');
|
||||
if (submitButton) submitButton.value = '저장';
|
||||
}
|
||||
categoryFormContainer.style.display = 'block';
|
||||
this.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// "취소" 버튼 클릭
|
||||
if (cancelCategoryButton && categoryFormContainer && addCategoryButton) {
|
||||
cancelCategoryButton.addEventListener('click', function() {
|
||||
categoryFormContainer.style.display = 'none';
|
||||
addCategoryButton.style.display = 'inline-block';
|
||||
});
|
||||
}
|
||||
|
||||
// "수정" 버튼 클릭
|
||||
if (editCategoryButtons.length > 0 && categoryForm) {
|
||||
editCategoryButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const data = this.dataset;
|
||||
|
||||
// 폼에 데이터 채우기
|
||||
categoryForm.w.value = 'u'; // 수정 모드
|
||||
categoryForm.fc_id.value = data.fc_id;
|
||||
categoryForm.parent_id.value = data.parent_id;
|
||||
categoryForm.fc_key.value = data.fc_key;
|
||||
categoryForm.cl_name.value = data.cl_name;
|
||||
categoryForm.fc_order.value = data.fc_order;
|
||||
|
||||
const isUsedRadio = categoryForm.querySelector(`input[name="is_used"][value="${data.is_used}"]`);
|
||||
if (isUsedRadio) isUsedRadio.checked = true;
|
||||
|
||||
const submitButton = categoryForm.querySelector('input[type="submit"]');
|
||||
if (submitButton) submitButton.value = '수정';
|
||||
|
||||
if (categoryFormContainer) categoryFormContainer.style.display = 'block';
|
||||
if (addCategoryButton) addCategoryButton.style.display = 'none';
|
||||
|
||||
categoryFormContainer.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. 리소스 삭제 기능
|
||||
// =================================================================
|
||||
const deleteResourceButtons = document.querySelectorAll('.btn_delete_resource');
|
||||
|
||||
if (deleteResourceButtons.length > 0) {
|
||||
deleteResourceButtons.forEach(button => {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const um_id = this.dataset.um_id;
|
||||
const resource_desc_element = this.closest('tr').querySelector('.td_left');
|
||||
const resource_desc = resource_desc_element ? resource_desc_element.textContent.trim() : `ID: ${um_id}`;
|
||||
|
||||
if (confirm(`'${resource_desc}' 리소스를 정말 삭제하시겠습니까?\n\n이 리소스와 관련된 모든 하위 옵션(카테고리) 및 언어 데이터가 함께 영구적으로 삭제됩니다.`)) {
|
||||
const qstr = new URLSearchParams(window.location.search).toString();
|
||||
location.href = `./ui_manager_list.php?mode=delete&um_id=${um_id}&token=${g5_admin_token}&${qstr}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
$sub_menu = '700100';
|
||||
include_once('./_common.php');
|
||||
|
||||
// ... (폼 제출 처리 로직은 변경 없음) ...
|
||||
$target_table = isset($_REQUEST['target_table']) ? preg_replace('/[^a-z0-9_]/i', '', $_REQUEST['target_table']) : '';
|
||||
$target_id = isset($_REQUEST['target_id']) ? (int)$_REQUEST['target_id'] : 0;
|
||||
|
||||
if (!$target_table || !$target_id) {
|
||||
alert('잘못된 접근입니다.');
|
||||
}
|
||||
|
||||
if (isset($w) && $w == 'u' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
check_admin_token();
|
||||
$lang_code = trim($_POST['lang_code']);
|
||||
$cl_name = trim($_POST['cl_name']);
|
||||
$cl_description = trim($_POST['cl_description']);
|
||||
if (!$lang_code) alert('언어 코드를 선택해주세요.');
|
||||
if (!$cl_name) alert('이름/값을 입력해주세요.');
|
||||
$sql = "INSERT INTO {$g5['common_lang_table']}
|
||||
SET target_table = '{$target_table}',
|
||||
target_id = '{$target_id}',
|
||||
lang_code = '{$lang_code}',
|
||||
cl_name = '{$cl_name}',
|
||||
cl_description = '{$cl_description}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
cl_name = '{$cl_name}',
|
||||
cl_description = '{$cl_description}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'";
|
||||
sql_query($sql);
|
||||
goto_url("./lang_manager.php?target_table=$target_table&target_id=$target_id");
|
||||
}
|
||||
if (isset($mode) && $mode == 'delete') {
|
||||
auth_check_menu($auth, $sub_menu, 'd');
|
||||
check_admin_token();
|
||||
$cl_id = isset($_GET['cl_id']) ? (int)$_GET['cl_id'] : 0;
|
||||
if (!$cl_id) alert('cl_id 값이 없습니다.');
|
||||
$sql = "DELETE FROM {$g5['common_lang_table']} WHERE cl_id = '{$cl_id}' AND target_table = '{$target_table}' AND target_id = '{$target_id}'";
|
||||
sql_query($sql);
|
||||
goto_url("./lang_manager.php?target_table=$target_table&target_id=$target_id");
|
||||
}
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
// 💡 [핵심 추가] 이전 페이지로 돌아가기 위한 링크 생성
|
||||
$back_link = './ui_manager_list.php'; // 기본 돌아가기 링크
|
||||
if ($target_table == $g5['form_category_table']) {
|
||||
// 카테고리 다국어 관리였다면, 해당 카테고리가 속한 리소스의 옵션 관리 페이지로 돌아가야 함
|
||||
$sql_back = "SELECT um_id FROM {$g5['form_category_table']} WHERE fc_id = '{$target_id}'";
|
||||
$back_row = sql_fetch($sql_back);
|
||||
if (isset($back_row['um_id'])) {
|
||||
$back_link = './category_list.php?um_id=' . $back_row['um_id'];
|
||||
}
|
||||
}
|
||||
|
||||
// UiManager 클래스를 사용하여 사용 가능한 언어 목록 가져오기
|
||||
$active_languages = ui_manager()->get_data('language_list');
|
||||
|
||||
// 현재 관리 대상의 기본 정보(한국어)를 가져옴
|
||||
$sql = "SELECT cl_name FROM {$g5['common_lang_table']} WHERE target_table = '{$target_table}' AND target_id = '{$target_id}' AND lang_code = 'ko'";
|
||||
$parent_info = sql_fetch($sql);
|
||||
$parent_name = isset($parent_info['cl_name']) ? get_text($parent_info['cl_name']) : "ID: {$target_id}";
|
||||
|
||||
// 등록된 다국어 목록 조회
|
||||
$sql = "SELECT * FROM {$g5['common_lang_table']} WHERE target_table = '{$target_table}' AND target_id = '{$target_id}' ORDER BY lang_code";
|
||||
$result = sql_query($sql);
|
||||
$lang_list = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$lang_list[] = $row;
|
||||
}
|
||||
$lang_list_count = count($lang_list);
|
||||
|
||||
$g5['title'] = '다국어 관리';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.1">', 0);
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
<strong>'<?php echo $parent_name; ?>'</strong> 항목에 대한 다국어 이름과 설명을 관리합니다.<br>
|
||||
'언어 코드'는 중복하여 등록할 수 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section id="code_manager">
|
||||
<h2 class="h2_frm">다국어 등록/수정</h2>
|
||||
<form name="flangform" id="flangform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
|
||||
<input type="hidden" name="w" value="u">
|
||||
<input type="hidden" name="target_table" value="<?php echo $target_table; ?>">
|
||||
<input type="hidden" name="target_id" value="<?php echo $target_id; ?>">
|
||||
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>다국어 정보 추가/수정 폼</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="lang_code">언어 코드</label></th>
|
||||
<td>
|
||||
<select name="lang_code" id="lang_code" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($active_languages as $lang_item) : ?>
|
||||
<option value="<?php echo $lang_item['fc_key']; ?>"><?php echo get_text($lang_item['cl_name']); ?> (<?php echo $lang_item['fc_key']; ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="cl_name">이름/값</label></th>
|
||||
<td>
|
||||
<input type="text" name="cl_name" id="cl_name" required class="required frm_input" size="80">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="cl_description">부가 설명</label></th>
|
||||
<td>
|
||||
<textarea name="cl_description" id="cl_description" rows="5"></textarea>
|
||||
<span class="frm_info">툴팁 등 부가적으로 사용될 설명을 입력합니다. (선택사항)</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<!-- 💡 [핵심 수정] 목록으로 돌아가는 버튼 추가 -->
|
||||
<a href="<?php echo $back_link; ?>" class="btn_cancel btn">목록으로</a>
|
||||
<input type="submit" value="저장" class="btn_submit btn" accesskey="s">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 등록된 다국어 목록 테이블 -->
|
||||
<div class="tbl_head01 tbl_wrap" style="margin-top: 20px;">
|
||||
<h2 class="h2_frm">등록된 다국어 목록</h2>
|
||||
<table>
|
||||
<caption>등록된 다국어 목록</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 100px;">언어 코드</th>
|
||||
<th scope="col">이름/값</th>
|
||||
<th scope="col">부가 설명</th>
|
||||
<th scope="col" style="width: 120px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($lang_list_count > 0) : ?>
|
||||
<?php foreach ($lang_list as $lang) : ?>
|
||||
<tr>
|
||||
<td class="td_alignc"><?php echo get_text($lang['lang_code']); ?></td>
|
||||
<td class="td_left"><?php echo get_text($lang['cl_name']); ?></td>
|
||||
<td class="td_left"><?php echo get_text($lang['cl_description']); ?></td>
|
||||
<td class="td_mng">
|
||||
<button type="button" class="btn btn_02 btn_edit_lang"
|
||||
data-lang_code="<?php echo get_text($lang['lang_code']); ?>"
|
||||
data-cl_name="<?php echo get_text($lang['cl_name']); ?>"
|
||||
data-cl_description="<?php echo get_text($lang['cl_description']); ?>">수정</button>
|
||||
<a href="./lang_manager.php?mode=delete&cl_id=<?php echo $lang['cl_id']; ?>&target_table=<?php echo $target_table; ?>&target_id=<?php echo $target_id; ?>&token=<?php echo get_admin_token(); ?>"
|
||||
class="btn btn_delete" onclick="return confirm('정말 삭제하시겠습니까?');">삭제</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<tr class="empty_table">
|
||||
<td colspan="4">등록된 다국어 정보가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const editLangButtons = document.querySelectorAll('.btn_edit_lang');
|
||||
const langForm = document.getElementById('flangform');
|
||||
if (editLangButtons.length > 0 && langForm) {
|
||||
editLangButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const data = this.dataset;
|
||||
langForm.lang_code.value = data.lang_code;
|
||||
langForm.cl_name.value = data.cl_name;
|
||||
langForm.cl_description.value = data.cl_description;
|
||||
langForm.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
/**
|
||||
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
|
||||
*/
|
||||
class SchemaManager
|
||||
{
|
||||
private $sql_file_path;
|
||||
private $results;
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
* @param string $sql_file_path install.sql 파일의 절대 경로
|
||||
*/
|
||||
public function __construct($sql_file_path)
|
||||
{
|
||||
if (!file_exists($sql_file_path)) {
|
||||
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
|
||||
}
|
||||
$this->sql_file_path = $sql_file_path;
|
||||
$this->results = [
|
||||
'created' => [],
|
||||
'existing' => [],
|
||||
'updated' => [],
|
||||
'failed' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스키마 설치/업데이트를 실행합니다.
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$sql_statements = $this->parse_sql_file();
|
||||
|
||||
foreach ($sql_statements as $stmt) {
|
||||
// CREATE TABLE 문인지 확인
|
||||
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
|
||||
$schema = $this->parse_create_table_sql($stmt);
|
||||
if ($schema && !empty($schema['name'])) {
|
||||
$this->process_table_schema($stmt, $schema);
|
||||
}
|
||||
} else {
|
||||
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
|
||||
sql_query($stmt, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 처리 결과를 반환합니다.
|
||||
* @return array
|
||||
*/
|
||||
public function get_results()
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
|
||||
* @param string $create_sql 전체 CREATE TABLE 구문
|
||||
* @param array $schema 파싱된 스키마 정보
|
||||
*/
|
||||
private function process_table_schema($create_sql, $schema)
|
||||
{
|
||||
$table_name = $schema['name'];
|
||||
|
||||
if ($this->table_exists($table_name)) {
|
||||
// 테이블이 존재하면, 컬럼 비교 및 추가
|
||||
$this->results['existing'][] = $table_name;
|
||||
$this->update_table_columns($table_name, $schema['columns']);
|
||||
} else {
|
||||
// 테이블이 존재하지 않으면, 새로 생성
|
||||
if (sql_query($create_sql, false)) {
|
||||
$this->results['created'][] = $table_name;
|
||||
} else {
|
||||
$this->results['failed'][] = $table_name;
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . sql_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 컬럼 구조를 업데이트합니다.
|
||||
* @param string $table_name
|
||||
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
|
||||
*/
|
||||
private function update_table_columns($table_name, $target_columns)
|
||||
{
|
||||
$current_columns = $this->get_current_columns($table_name);
|
||||
$added_columns_in_table = [];
|
||||
|
||||
foreach ($target_columns as $col_name => $col_definition) {
|
||||
// 현재 테이블에 해당 컬럼이 없으면 추가
|
||||
if (!isset($current_columns[$col_name])) {
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
|
||||
if (sql_query($alter_sql, false)) {
|
||||
$added_columns_in_table[] = $col_name;
|
||||
} else {
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . sql_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($added_columns_in_table)) {
|
||||
$this->results['updated'][$table_name] = $added_columns_in_table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일을 읽고 각 구문으로 분리합니다.
|
||||
* @return array
|
||||
*/
|
||||
private function parse_sql_file()
|
||||
{
|
||||
$sql = file_get_contents($this->sql_file_path);
|
||||
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
|
||||
$sql = preg_replace('/--.*/', '', $sql);
|
||||
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
|
||||
$sql = trim($sql);
|
||||
|
||||
// 세미콜론(;)을 기준으로 쿼리 분리
|
||||
return array_filter(array_map('trim', explode(';', $sql)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 존재 여부를 확인합니다.
|
||||
* @param string $table_name
|
||||
* @return bool
|
||||
*/
|
||||
private function table_exists($table_name)
|
||||
{
|
||||
$res = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
|
||||
return sql_num_rows($res) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
|
||||
* @param string $table_name
|
||||
* @return array
|
||||
*/
|
||||
private function get_current_columns($table_name)
|
||||
{
|
||||
$res = sql_query("SHOW COLUMNS FROM `{$table_name}`", false);
|
||||
$columns = [];
|
||||
while ($row = sql_fetch_array($res)) {
|
||||
$columns[$row['Field']] = true;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
|
||||
* @param string $query CREATE TABLE 구문
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_create_table_sql($query)
|
||||
{
|
||||
$table_name = '';
|
||||
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
|
||||
$table_name = $matches[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 괄호 안의 내용만 추출
|
||||
$start = strpos($query, '(');
|
||||
$end = strrpos($query, ')');
|
||||
if ($start === false || $end === false) {
|
||||
return ['name' => $table_name, 'columns' => []];
|
||||
}
|
||||
$content = substr($query, $start + 1, $end - $start - 1);
|
||||
|
||||
// 줄 단위로 분리
|
||||
$lines = explode("\n", $content);
|
||||
|
||||
$columns = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t\n\r\0\x0B,"); // 양쪽 공백과 마지막 쉼표 제거
|
||||
|
||||
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
|
||||
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
|
||||
$col_name = $match[1];
|
||||
$col_definition = $match[2];
|
||||
$columns[$col_name] = $col_definition;
|
||||
}
|
||||
}
|
||||
|
||||
return ['name' => $table_name, 'columns' => $columns];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
/**
|
||||
* UI 리소스 관리 클래스
|
||||
* Singleton 패턴을 사용하여 인스턴스를 한 번만 생성하고,
|
||||
* 불러온 데이터를 캐시하여 DB 조회를 최소화합니다.
|
||||
*/
|
||||
class UiManager
|
||||
{
|
||||
private static $instance = null;
|
||||
private $resources = []; // 데이터를 캐시할 배열
|
||||
|
||||
// 외부에서 new 키워드로 인스턴스 생성을 막음
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* 클래스의 유일한 인스턴스를 반환합니다.
|
||||
* @return UiManager
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'LABEL' 타입의 UI 텍스트를 가져옵니다.
|
||||
* @param string $resource_code 리소스 코드
|
||||
* @param string $lang 언어 코드 (기본값: 'ko')
|
||||
* @return string 라벨 텍스트 (없으면 resource_code 반환)
|
||||
*/
|
||||
public function get_label($resource_code, $lang = 'ko')
|
||||
{
|
||||
// 캐시 확인
|
||||
if (isset($this->resources['labels'][$lang][$resource_code])) {
|
||||
return $this->resources['labels'][$lang][$resource_code];
|
||||
}
|
||||
|
||||
global $g5;
|
||||
$resource_code_escaped = sql_real_escape_string($resource_code);
|
||||
$lang_escaped = sql_real_escape_string($lang);
|
||||
|
||||
$sql = "SELECT B.cl_name
|
||||
FROM {$g5['ui_manager_table']} AS A
|
||||
LEFT JOIN {$g5['common_lang_table']} AS B
|
||||
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = '{$lang_escaped}')
|
||||
WHERE A.resource_code = '{$resource_code_escaped}' AND A.resource_type = 'LABEL'";
|
||||
$row = sql_fetch($sql);
|
||||
|
||||
$label_text = $row['cl_name'] ?? $resource_code;
|
||||
|
||||
// 결과 캐시
|
||||
$this->resources['labels'][$lang][$resource_code] = $label_text;
|
||||
|
||||
return $label_text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'DATA' 타입의 옵션 목록을 배열로 가져옵니다.
|
||||
* @param string $resource_code 리소스 코드
|
||||
* @param string $lang 언어 코드 (기본값: 'ko')
|
||||
* @return array 옵션 목록 배열
|
||||
*/
|
||||
public function get_data($resource_code, $lang = 'ko')
|
||||
{
|
||||
// 캐시 확인
|
||||
if (isset($this->resources['data'][$lang][$resource_code])) {
|
||||
return $this->resources['data'][$lang][$resource_code];
|
||||
}
|
||||
|
||||
global $g5;
|
||||
$resource_code_escaped = sql_real_escape_string($resource_code);
|
||||
$lang_escaped = sql_real_escape_string($lang);
|
||||
|
||||
$sql_um = "SELECT um_id FROM {$g5['ui_manager_table']} WHERE resource_code = '{$resource_code_escaped}' AND resource_type = 'DATA'";
|
||||
$um_row = sql_fetch($sql_um);
|
||||
|
||||
if (!isset($um_row['um_id'])) {
|
||||
$this->resources['data'][$lang][$resource_code] = []; // 빈 결과도 캐시
|
||||
return [];
|
||||
}
|
||||
$um_id = $um_row['um_id'];
|
||||
|
||||
$sql = "SELECT A.fc_id, A.parent_id, A.fc_key, A.fc_order, B.cl_name
|
||||
FROM {$g5['form_category_table']} AS A
|
||||
LEFT JOIN {$g5['common_lang_table']} AS B
|
||||
ON (A.fc_id = B.target_id AND B.target_table = '{$g5['form_category_table']}' AND B.lang_code = '{$lang_escaped}')
|
||||
WHERE A.um_id = '{$um_id}' AND A.is_used = 1 AND A.is_deleted = 0
|
||||
ORDER BY A.fc_order, A.fc_id";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$options = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$options[] = $row;
|
||||
}
|
||||
|
||||
// 결과 캐시
|
||||
$this->resources['data'][$lang][$resource_code] = $options;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'DATA' 타입 리소스를 사용하여 HTML <select> 태그를 생성합니다.
|
||||
* @param string $resource_code 리소스 코드
|
||||
* @param string $select_name <select> 태그의 name 속성
|
||||
* @param string $selected_value 미리 선택될 옵션의 값(fc_key)
|
||||
* @param string $attributes <select> 태그에 추가할 HTML 속성 (e.g., 'id="my-id" class="my-class"')
|
||||
* @param string $lang 언어 코드 (기본값: 'ko')
|
||||
* @return string 생성된 HTML <select> 태그
|
||||
*/
|
||||
public function render_select($resource_code, $select_name, $selected_value = '', $attributes = '', $lang = 'ko')
|
||||
{
|
||||
$options = $this->get_data($resource_code, $lang);
|
||||
|
||||
if (empty($options)) {
|
||||
return "<select name=\"{$select_name}\" {$attributes}><option value=\"\">옵션 없음</option></select>";
|
||||
}
|
||||
|
||||
$html = "<select name=\"{$select_name}\" {$attributes}>";
|
||||
$html .= "<option value=\"\">선택</option>";
|
||||
foreach ($options as $option) {
|
||||
$selected = ($option['fc_key'] == $selected_value) ? ' selected' : '';
|
||||
$html .= "<option value=\"" . htmlspecialchars($option['fc_key']) . "\"{$selected}>" . htmlspecialchars($option['cl_name']) . "</option>";
|
||||
}
|
||||
$html .= "</select>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
/**
|
||||
* UI 리소스 관리자 클래스 인스턴스를 반환하는 헬퍼 함수
|
||||
* @return UiManager
|
||||
*/
|
||||
function ui_manager() {
|
||||
// 클래스가 아직 로드되지 않았다면 인스턴스 생성
|
||||
if (!class_exists('UiManager')) {
|
||||
// UiManager 클래스 정의
|
||||
class UiManager
|
||||
{
|
||||
private static $instance = null;
|
||||
private $resources = []; // 데이터를 캐시할 배열
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function get_label($resource_code, $lang = 'ko')
|
||||
{
|
||||
if (isset($this->resources['labels'][$lang][$resource_code])) {
|
||||
return $this->resources['labels'][$lang][$resource_code];
|
||||
}
|
||||
|
||||
global $g5;
|
||||
$resource_code_escaped = sql_real_escape_string($resource_code);
|
||||
$lang_escaped = sql_real_escape_string($lang);
|
||||
|
||||
$sql = "SELECT B.cl_name
|
||||
FROM {$g5['ui_manager_table']} AS A
|
||||
LEFT JOIN {$g5['common_lang_table']} AS B
|
||||
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = '{$lang_escaped}')
|
||||
WHERE A.resource_code = '{$resource_code_escaped}' AND A.resource_type = 'LABEL'";
|
||||
$row = sql_fetch($sql);
|
||||
|
||||
$label_text = $row['cl_name'] ?? $resource_code;
|
||||
$this->resources['labels'][$lang][$resource_code] = $label_text;
|
||||
return $label_text;
|
||||
}
|
||||
|
||||
public function get_data($resource_code, $lang = 'ko')
|
||||
{
|
||||
if (isset($this->resources['data'][$lang][$resource_code])) {
|
||||
return $this->resources['data'][$lang][$resource_code];
|
||||
}
|
||||
|
||||
global $g5;
|
||||
$resource_code_escaped = sql_real_escape_string($resource_code);
|
||||
$lang_escaped = sql_real_escape_string($lang);
|
||||
|
||||
$sql_um = "SELECT um_id FROM {$g5['ui_manager_table']} WHERE resource_code = '{$resource_code_escaped}' AND resource_type = 'DATA'";
|
||||
$um_row = sql_fetch($sql_um);
|
||||
|
||||
if (!isset($um_row['um_id'])) {
|
||||
$this->resources['data'][$lang][$resource_code] = [];
|
||||
return [];
|
||||
}
|
||||
$um_id = $um_row['um_id'];
|
||||
|
||||
$sql = "SELECT A.fc_id, A.parent_id, A.fc_key, A.fc_order, B.cl_name
|
||||
FROM {$g5['form_category_table']} AS A
|
||||
LEFT JOIN {$g5['common_lang_table']} AS B
|
||||
ON (A.fc_id = B.target_id AND B.target_table = '{$g5['form_category_table']}' AND B.lang_code = '{$lang_escaped}')
|
||||
WHERE A.um_id = '{$um_id}' AND A.is_used = 1 AND A.is_deleted = 0
|
||||
ORDER BY A.fc_order, A.fc_id";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$options = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$options[] = $row;
|
||||
}
|
||||
|
||||
$this->resources['data'][$lang][$resource_code] = $options;
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function render_select($resource_code, $select_name, $selected_value = '', $attributes = '', $lang = 'ko')
|
||||
{
|
||||
$options = $this->get_data($resource_code, $lang);
|
||||
|
||||
if (empty($options)) {
|
||||
return "<select name=\"".htmlspecialchars($select_name)."\" {$attributes}><option value=\"\">옵션 없음</option></select>";
|
||||
}
|
||||
|
||||
$html = "<select name=\"".htmlspecialchars($select_name)."\" {$attributes}>";
|
||||
$html .= "<option value=\"\">선택</option>";
|
||||
foreach ($options as $option) {
|
||||
$selected = ($option['fc_key'] == $selected_value) ? ' selected' : '';
|
||||
$html .= "<option value=\"" . htmlspecialchars($option['fc_key']) . "\"{$selected}>" . htmlspecialchars($option['cl_name']) . "</option>";
|
||||
}
|
||||
$html .= "</select>";
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
}
|
||||
return UiManager::getInstance();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
$sub_menu = '100350';
|
||||
include_once('./_common.php');
|
||||
|
||||
$w = isset($_REQUEST['w']) ? substr(trim($_REQUEST['w']), 0, 1) : '';
|
||||
$um_id = isset($_REQUEST['um_id']) ? (int)$_REQUEST['um_id'] : 0;
|
||||
|
||||
// ==================================================================
|
||||
// 💡 [핵심] 폼 제출(수정) 처리 로직
|
||||
// ==================================================================
|
||||
if ($w == 'u' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
check_admin_token();
|
||||
|
||||
if (!$um_id) {
|
||||
alert('um_id 값이 없습니다.');
|
||||
}
|
||||
|
||||
// 입력값 정리
|
||||
$screen_code = trim($_POST['screen_code']);
|
||||
$group_code = trim($_POST['group_code']);
|
||||
$resource_code = trim($_POST['resource_code']);
|
||||
$resource_type = trim($_POST['resource_type']);
|
||||
$resource_desc = trim($_POST['resource_desc']);
|
||||
$cl_name = isset($_POST['cl_name']) ? trim($_POST['cl_name']) : '';
|
||||
|
||||
// 유효성 검사
|
||||
if (!$screen_code || !$group_code || !$resource_code || !$resource_type) {
|
||||
alert('필수 항목을 모두 입력해주세요.');
|
||||
}
|
||||
if ($resource_type == 'LABEL' && !$cl_name) {
|
||||
alert('UI 라벨 타입은 한국어 라벨명을 필수로 입력해야 합니다.');
|
||||
}
|
||||
|
||||
// 1. g5_ui_manager 테이블 정보 업데이트
|
||||
$sql = "UPDATE {$g5['ui_manager_table']}
|
||||
SET screen_code = '{$screen_code}',
|
||||
group_code = '{$group_code}',
|
||||
resource_code = '{$resource_code}',
|
||||
resource_type = '{$resource_type}',
|
||||
resource_desc = '{$resource_desc}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'
|
||||
WHERE um_id = '{$um_id}'";
|
||||
sql_query($sql);
|
||||
|
||||
// 2. 리소스 타입이 'LABEL'인 경우, g5_common_lang 테이블 정보 업데이트 (없으면 생성)
|
||||
if ($resource_type == 'LABEL') {
|
||||
$sql = "SELECT cl_id FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['ui_manager_table']}' AND target_id = '{$um_id}' AND lang_code = 'ko'";
|
||||
$lang_row = sql_fetch($sql);
|
||||
|
||||
if (isset($lang_row['cl_id'])) { // 기존 데이터가 있으면 UPDATE
|
||||
$sql = "UPDATE {$g5['common_lang_table']}
|
||||
SET cl_name = '{$cl_name}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'
|
||||
WHERE cl_id = '{$lang_row['cl_id']}'";
|
||||
} else { // 기존 데이터가 없으면 INSERT
|
||||
$sql = "INSERT INTO {$g5['common_lang_table']}
|
||||
SET target_table = '{$g5['ui_manager_table']}',
|
||||
target_id = '{$um_id}',
|
||||
lang_code = 'ko',
|
||||
cl_name = '{$cl_name}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'";
|
||||
}
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
goto_url('./ui_manager_list.php');
|
||||
}
|
||||
// ==================================================================
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
// 수정할 리소스 데이터 조회
|
||||
if ($w == 'u') {
|
||||
if (!$um_id) {
|
||||
alert('um_id 값이 없습니다.', './ui_manager_list.php');
|
||||
}
|
||||
|
||||
$sql = "SELECT A.*, B.cl_name
|
||||
FROM {$g5['ui_manager_table']} AS A
|
||||
LEFT JOIN {$g5['common_lang_table']} AS B
|
||||
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = 'ko')
|
||||
WHERE A.um_id = '{$um_id}'";
|
||||
$ui_resource = sql_fetch($sql);
|
||||
|
||||
if (!isset($ui_resource['um_id'])) {
|
||||
alert('존재하지 않는 리소스입니다.', './ui_manager_list.php');
|
||||
}
|
||||
} else {
|
||||
alert('w 값이 올바르지 않습니다.', './ui_manager_list.php');
|
||||
}
|
||||
|
||||
$g5['title'] = 'UI 리소스 수정';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.1">', 0);
|
||||
?>
|
||||
|
||||
<section id="code_manager_form">
|
||||
<h2 class="h2_frm">UI 리소스 수정</h2>
|
||||
|
||||
<form name="fresourceform" id="fresourceform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
|
||||
<input type="hidden" name="w" value="u">
|
||||
<input type="hidden" name="um_id" value="<?php echo $um_id; ?>">
|
||||
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
|
||||
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>UI 리소스 수정 폼</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="screen_code">화면 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="screen_code" id="screen_code" required class="required frm_input" size="30" value="<?php echo get_text($ui_resource['screen_code']); ?>">
|
||||
<span class="frm_info">리소스가 사용될 화면의 고유 코드 (예: order_form, member_join)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="group_code">그룹 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="group_code" id="group_code" required class="required frm_input" size="30" value="<?php echo get_text($ui_resource['group_code']); ?>">
|
||||
<span class="frm_info">화면 내에서 리소스를 묶어줄 그룹 코드 (예: address_info, common_options)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">리소스 타입</th>
|
||||
<td>
|
||||
<label><input type="radio" name="resource_type" value="LABEL" <?php echo get_checked($ui_resource['resource_type'], 'LABEL'); ?>> UI 라벨 (단일 텍스트)</label>
|
||||
<label><input type="radio" name="resource_type" value="DATA" <?php echo get_checked($ui_resource['resource_type'], 'DATA'); ?>> 데이터 (선택 옵션)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="resource-type-field" id="label-field">
|
||||
<th scope="row"><label for="cl_name">한국어 라벨명</label></th>
|
||||
<td>
|
||||
<input type="text" name="cl_name" id="cl_name" class="frm_input" size="50" value="<?php echo get_text($ui_resource['cl_name']); ?>">
|
||||
<span class="frm_info">화면에 표시될 실제 텍스트 (예: 집의 유형, 창호 재질)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="resource_code">리소스 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="resource_code" id="resource_code" required class="required frm_input" size="30" value="<?php echo get_text($ui_resource['resource_code']); ?>">
|
||||
<span class="frm_info">개발자가 이 리소스를 호출할 때 사용할 고유 코드 (예: house_type_label, house_type_data)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="resource_desc">설명</label></th>
|
||||
<td>
|
||||
<input type="text" name="resource_desc" id="resource_desc" class="frm_input" size="80" value="<?php echo get_text($ui_resource['resource_desc']); ?>">
|
||||
<span class="frm_info">이 리소스의 용도에 대한 설명 (관리자 참고용)</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<a href="./ui_manager_list.php" class="btn_cancel btn">목록으로</a>
|
||||
<input type="submit" value="수정" class="btn_submit btn" accesskey="s">
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/ui_manager.js?ver=1.2"></script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
$sub_menu = '700100';
|
||||
include_once('./_common.php');
|
||||
|
||||
// ==================================================================
|
||||
// 삭제 처리 로직
|
||||
// ==================================================================
|
||||
if (isset($_GET['mode']) && $_GET['mode'] == 'delete') {
|
||||
auth_check_menu($auth, $sub_menu, 'd');
|
||||
check_admin_token();
|
||||
$um_id = isset($_GET['um_id']) ? (int)$_GET['um_id'] : 0;
|
||||
if (!$um_id) {
|
||||
alert('um_id 값이 없습니다.');
|
||||
}
|
||||
$sql = "SELECT resource_type FROM {$g5['ui_manager_table']} WHERE um_id = '{$um_id}'";
|
||||
$row = sql_fetch($sql);
|
||||
if (isset($row['resource_type'])) {
|
||||
if ($row['resource_type'] == 'DATA') {
|
||||
$sql_delete_lang = "DELETE FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['form_category_table']}' AND target_id IN (SELECT fc_id FROM {$g5['form_category_table']} WHERE um_id = '{$um_id}')";
|
||||
sql_query($sql_delete_lang);
|
||||
$sql_delete_cat = "DELETE FROM {$g5['form_category_table']} WHERE um_id = '{$um_id}'";
|
||||
sql_query($sql_delete_cat);
|
||||
}
|
||||
$sql_delete_main_lang = "DELETE FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['ui_manager_table']}' AND target_id = '{$um_id}'";
|
||||
sql_query($sql_delete_main_lang);
|
||||
$sql_delete_main = "DELETE FROM {$g5['ui_manager_table']} WHERE um_id = '{$um_id}'";
|
||||
sql_query($sql_delete_main);
|
||||
}
|
||||
// 삭제 후 현재 페이지와 검색 조건을 유지하도록 $qstr 사용
|
||||
goto_url('./ui_manager_list.php?'.$qstr);
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// 💡 [핵심 수정 1] 폼 제출 처리 로직 (신규 등록)
|
||||
// ==================================================================
|
||||
// GnuBoard 표준에 따라 'w' 값이 비어있을 때를 신규 등록으로 처리합니다.
|
||||
if (empty($w) && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
check_admin_token();
|
||||
|
||||
// 입력값 정리
|
||||
$screen_code = trim($_POST['screen_code']);
|
||||
$group_code = trim($_POST['group_code']);
|
||||
$resource_code = trim($_POST['resource_code']);
|
||||
$resource_type = trim($_POST['resource_type']);
|
||||
$resource_desc = trim($_POST['resource_desc']);
|
||||
$cl_name = isset($_POST['cl_name']) ? trim($_POST['cl_name']) : '';
|
||||
|
||||
// 유효성 검사
|
||||
if (!$screen_code || !$group_code || !$resource_code || !$resource_type) {
|
||||
alert('필수 항목을 모두 입력해주세요.');
|
||||
}
|
||||
if ($resource_type == 'LABEL' && !$cl_name) {
|
||||
alert('UI 라벨 타입은 한국어 라벨명을 필수로 입력해야 합니다.');
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
$sql = "SELECT COUNT(*) as cnt FROM {$g5['ui_manager_table']} WHERE screen_code = '{$screen_code}' AND group_code = '{$group_code}' AND resource_code = '{$resource_code}'";
|
||||
$row = sql_fetch($sql);
|
||||
if ($row['cnt']) {
|
||||
alert('이미 동일한 화면/그룹/리소스 코드로 등록된 리소스가 존재합니다.');
|
||||
}
|
||||
|
||||
// 1. g5_ui_manager 테이블에 정보 INSERT
|
||||
$sql = "INSERT INTO {$g5['ui_manager_table']}
|
||||
SET screen_code = '{$screen_code}',
|
||||
group_code = '{$group_code}',
|
||||
resource_code = '{$resource_code}',
|
||||
resource_type = '{$resource_type}',
|
||||
resource_desc = '{$resource_desc}',
|
||||
is_used = '1',
|
||||
created_at = '".G5_TIME_YMDHIS."',
|
||||
created_by = '{$member['mb_id']}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'";
|
||||
sql_query($sql);
|
||||
$um_id = sql_insert_id();
|
||||
|
||||
// 2. 리소스 타입이 'LABEL'인 경우, g5_common_lang 테이블에 정보 INSERT
|
||||
if ($resource_type == 'LABEL' && $cl_name) {
|
||||
$sql = "INSERT INTO {$g5['common_lang_table']}
|
||||
SET target_table = '{$g5['ui_manager_table']}',
|
||||
target_id = '{$um_id}',
|
||||
lang_code = 'ko',
|
||||
cl_name = '{$cl_name}',
|
||||
updated_at = '".G5_TIME_YMDHIS."',
|
||||
updated_by = '{$member['mb_id']}'";
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
goto_url('./ui_manager_list.php');
|
||||
}
|
||||
// ==================================================================
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
// 검색 및 페이징 변수 처리
|
||||
$sfl = isset($_GET['sfl']) ? trim($_GET['sfl']) : '';
|
||||
$stx = isset($_GET['stx']) ? trim($_GET['stx']) : '';
|
||||
$page_rows = isset($_GET['page_rows']) && (int)$_GET['page_rows'] > 0 ? (int)$_GET['page_rows'] : 15;
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
|
||||
// 페이지당 목록 수($page_rows)를 $qstr에 추가하여 다른 링크에서도 유지되도록 함
|
||||
$qstr .= ($qstr ? '&' : '') . 'page_rows=' . urlencode($page_rows);
|
||||
|
||||
$sql_search = "";
|
||||
if ($sfl && $stx) {
|
||||
$stx_escaped = sql_real_escape_string($stx);
|
||||
$sql_search = " WHERE ";
|
||||
|
||||
switch ($sfl) {
|
||||
case 'screen_code':
|
||||
case 'group_code':
|
||||
case 'resource_code':
|
||||
case 'resource_desc':
|
||||
$sql_search .= "A.{$sfl} LIKE '%{$stx_escaped}%'";
|
||||
break;
|
||||
case 'cl_name':
|
||||
$sql_search .= "B.cl_name LIKE '%{$stx_escaped}%'";
|
||||
break;
|
||||
default: // '전체' 검색
|
||||
$sql_search .= " ( A.screen_code LIKE '%{$stx_escaped}%' OR A.group_code LIKE '%{$stx_escaped}%' OR A.resource_code LIKE '%{$stx_escaped}%' OR A.resource_desc LIKE '%{$stx_escaped}%' OR B.cl_name LIKE '%{$stx_escaped}%' ) ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 검색 조건에 맞는 모든 리소스를 한 번에 가져옴
|
||||
$sql = "SELECT A.*, B.cl_name
|
||||
FROM {$g5['ui_manager_table']} AS A
|
||||
LEFT JOIN {$g5['common_lang_table']} AS B
|
||||
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = 'ko')
|
||||
{$sql_search}
|
||||
ORDER BY A.screen_code, A.group_code, A.resource_code";
|
||||
$result = sql_query($sql);
|
||||
|
||||
// 2. PHP에서 화면별로 그룹화
|
||||
$all_grouped_resources = [];
|
||||
$total_resource_count = 0;
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$all_grouped_resources[$row['screen_code']][] = $row;
|
||||
$total_resource_count++;
|
||||
}
|
||||
|
||||
// 3. 그룹화된 결과를 기준으로 페이징 처리
|
||||
$total_count = count($all_grouped_resources); // 전체 '화면' 수
|
||||
$total_page = ceil($total_count / $page_rows);
|
||||
$from_record = ($page - 1) * $page_rows;
|
||||
|
||||
// 현재 페이지에 해당하는 그룹만 잘라냄
|
||||
$paged_groups = array_slice($all_grouped_resources, $from_record, $page_rows, true);
|
||||
|
||||
$g5['title'] = 'UI 리소스 관리';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.3">', 0);
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
웹사이트의 모든 화면에 사용되는 텍스트(라벨)와 선택 옵션(데이터)을 체계적으로 관리합니다.<br>
|
||||
'화면 코드' 별로 그룹화되어 표시되며, 각 그룹을 클릭하여 내용을 확인하거나 수정할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 검색 폼 -->
|
||||
<section id="resource_search_form">
|
||||
<h2 class="h2_frm">리소스 검색</h2>
|
||||
<form name="fsearch" id="fsearch" method="get">
|
||||
<div class="search-form-inner">
|
||||
<label for="page_rows" class="sound_only">페이지당 개수</label>
|
||||
<select name="page_rows" id="page_rows" class="frm_input">
|
||||
<option value="15" <?php echo get_selected($page_rows, 15); ?>>15개씩</option>
|
||||
<option value="30" <?php echo get_selected($page_rows, 30); ?>>30개씩</option>
|
||||
<option value="50" <?php echo get_selected($page_rows, 50); ?>>50개씩</option>
|
||||
<option value="100" <?php echo get_selected($page_rows, 100); ?>>100개씩</option>
|
||||
</select>
|
||||
|
||||
<label for="sfl" class="sound_only">검색대상</label>
|
||||
<select name="sfl" id="sfl">
|
||||
<option value="all" <?php echo get_selected($sfl, 'all'); ?>>전체</option>
|
||||
<option value="screen_code" <?php echo get_selected($sfl, 'screen_code'); ?>>화면 코드</option>
|
||||
<option value="group_code" <?php echo get_selected($sfl, 'group_code'); ?>>그룹 코드</option>
|
||||
<option value="resource_code" <?php echo get_selected($sfl, 'resource_code'); ?>>리소스 코드</option>
|
||||
<option value="resource_desc" <?php echo get_selected($sfl, 'resource_desc'); ?>>설명</option>
|
||||
<option value="cl_name" <?php echo get_selected($sfl, 'cl_name'); ?>>라벨명</option>
|
||||
</select>
|
||||
|
||||
<label for="stx" class="sound_only">검색어</label>
|
||||
<input type="text" name="stx" value="<?php echo get_text($stx) ?>" id="stx" class="frm_input" size="30">
|
||||
<input type="submit" value="검색" class="btn_submit">
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="code_manager">
|
||||
<div class="code-manager-header">
|
||||
<h2 class="code-manager-title">UI 리소스 목록 (총 <?php echo number_format($total_count); ?>개 화면)</h2>
|
||||
<div class="code-manager-actions">
|
||||
<button type="button" id="add-resource-btn" class="btn btn_01">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i> 새 리소스 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 새 리소스 추가 폼 -->
|
||||
<div id="resource-form-container" style="display: none;">
|
||||
<form name="fresourceform" id="fresourceform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
|
||||
<!-- 💡 [핵심 수정 2] 신규 등록이므로 w 값을 비워둡니다. -->
|
||||
<input type="hidden" name="w" value="">
|
||||
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
|
||||
<div class="tbl_frm01 tbl_wrap">
|
||||
<table>
|
||||
<caption>UI 리소스 추가 폼</caption>
|
||||
<colgroup>
|
||||
<col class="grid_4">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><label for="screen_code">화면 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="screen_code" id="screen_code" required class="required frm_input" size="30">
|
||||
<span class="frm_info">리소스가 사용될 화면의 고유 코드 (예: order_form, member_join)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="group_code">그룹 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="group_code" id="group_code" required class="required frm_input" size="30">
|
||||
<span class="frm_info">화면 내에서 리소스를 묶어줄 그룹 코드 (예: address_info, common_options)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">리소스 타입</th>
|
||||
<td>
|
||||
<label><input type="radio" name="resource_type" value="LABEL" checked> UI 라벨 (단일 텍스트)</label>
|
||||
<label><input type="radio" name="resource_type" value="DATA"> 데이터 (선택 옵션)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="resource-type-field" id="label-field">
|
||||
<th scope="row"><label for="cl_name">한국어 라벨명</label></th>
|
||||
<td>
|
||||
<input type="text" name="cl_name" id="cl_name" class="frm_input" size="50">
|
||||
<span class="frm_info">화면에 표시될 실제 텍스트 (예: 집의 유형, 창호 재질)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="resource_code">리소스 코드</label></th>
|
||||
<td>
|
||||
<input type="text" name="resource_code" id="resource_code" required class="required frm_input" size="30">
|
||||
<span class="frm_info">개발자가 이 리소스를 호출할 때 사용할 고유 코드 (예: house_type_label, house_type_data)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="resource_desc">설명</label></th>
|
||||
<td>
|
||||
<input type="text" name="resource_desc" id="resource_desc" class="frm_input" size="80">
|
||||
<span class="frm_info">이 리소스의 용도에 대한 설명 (관리자 참고용)</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn_confirm01 btn_confirm">
|
||||
<button type="button" id="cancel-resource-btn" class="btn_cancel btn">취소</button>
|
||||
<input type="submit" value="리소스 등록" class="btn_submit btn" accesskey="s">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 아코디언 형태의 리소스 목록 -->
|
||||
<div id="resource-list-accordion">
|
||||
<?php if (count($paged_groups) == 0) : ?>
|
||||
<div class="empty_list">표시할 리소스가 없습니다.</div>
|
||||
<?php else : ?>
|
||||
<?php foreach ($paged_groups as $screen_code => $resources) : ?>
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header">
|
||||
<span class="screen-title"><i class="fa fa-desktop"></i> 화면: <strong><?php echo get_text($screen_code); ?></strong></span>
|
||||
<span class="resource-count"><?php echo count($resources); ?>개 리소스</span>
|
||||
<i class="fa fa-chevron-down accordion-icon"></i>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 15%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 10%;">
|
||||
<col>
|
||||
<col style="width: 240px;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">그룹 코드</th>
|
||||
<th scope="col">리소스 코드</th>
|
||||
<th scope="col">타입</th>
|
||||
<th scope="col">설명</th>
|
||||
<th scope="col">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($resources as $res) : ?>
|
||||
<tr>
|
||||
<td><?php echo get_text($res['group_code']); ?></td>
|
||||
<td><?php echo get_text($res['resource_code']); ?></td>
|
||||
<td>
|
||||
<?php if ($res['resource_type'] == 'LABEL'): ?>
|
||||
<span class="res-type-label">라벨</span>
|
||||
<?php else: ?>
|
||||
<span class="res-type-data">데이터</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td_left"><?php echo get_text($res['resource_desc']); ?></td>
|
||||
<td class="td_mng td_mng_s">
|
||||
<?php if ($res['resource_type'] == 'DATA') : ?>
|
||||
<a href="./category_list.php?um_id=<?php echo $res['um_id']; ?>" class="btn btn_03">옵션 관리</a>
|
||||
<?php endif; ?>
|
||||
<a href="./ui_manager_form.php?w=u&um_id=<?php echo $res['um_id']; ?>&<?php echo $qstr; ?>" class="btn btn_02">수정</a>
|
||||
<a href="./lang_manager.php?target_table=<?php echo $g5['ui_manager_table']; ?>&target_id=<?php echo $res['um_id']; ?>" class="btn btn_01">다국어</a>
|
||||
<button type="button" class="btn btn_delete btn_delete_resource" data-um_id="<?php echo $res['um_id']; ?>">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 페이징 링크 출력 -->
|
||||
<div class="pagination_wrap">
|
||||
<?php
|
||||
$paging_url = $_SERVER['SCRIPT_NAME'].'?'.$qstr;
|
||||
echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $paging_url);
|
||||
?>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<script>
|
||||
var g5_admin_token = "<?php echo get_admin_token(); ?>";
|
||||
</script>
|
||||
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/ui_manager.js?ver=1.3"></script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
+1823
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
$sub_menu = "100100";
|
||||
require_once './_common.php';
|
||||
|
||||
check_demo();
|
||||
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
if ($is_admin != 'super') {
|
||||
alert('최고관리자만 접근 가능합니다.');
|
||||
}
|
||||
|
||||
$sql = " select * from {$g5['config_table']} limit 1";
|
||||
$ori_config = sql_fetch($sql);
|
||||
|
||||
$cf_title = isset($_POST['cf_title']) ? strip_tags(clean_xss_attributes($_POST['cf_title'])) : '';
|
||||
$cf_admin = isset($_POST['cf_admin']) ? clean_xss_tags($_POST['cf_admin'], 1, 1) : '';
|
||||
|
||||
$mb = get_member($cf_admin);
|
||||
|
||||
if (!(isset($mb['mb_id']) && $mb['mb_id'])) {
|
||||
alert('최고관리자 회원아이디가 존재하지 않습니다.');
|
||||
}
|
||||
|
||||
check_admin_token();
|
||||
|
||||
$cf_social_servicelist = !empty($_POST['cf_social_servicelist']) ? implode(',', $_POST['cf_social_servicelist']) : '';
|
||||
|
||||
$check_keys = array('cf_cert_kcb_cd', 'cf_cert_kcp_cd', 'cf_cert_kcp_enckey', 'cf_editor', 'cf_recaptcha_site_key', 'cf_recaptcha_secret_key', 'cf_naver_clientid', 'cf_naver_secret', 'cf_facebook_appid', 'cf_facebook_secret', 'cf_twitter_key', 'cf_twitter_secret', 'cf_google_clientid', 'cf_google_secret', 'cf_googl_shorturl_apikey', 'cf_kakao_rest_key', 'cf_kakao_client_secret', 'cf_kakao_js_apikey', 'cf_payco_clientid', 'cf_payco_secret', 'cf_cert_kg_cd', 'cf_cert_kg_mid');
|
||||
|
||||
foreach ($check_keys as $key) {
|
||||
if (isset($_POST[$key]) && $_POST[$key]) {
|
||||
$_POST[$key] = preg_replace('/[^a-z0-9_\-\.]/i', '', $_POST[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$_POST['cf_icode_server_port'] = isset($_POST['cf_icode_server_port']) ? preg_replace('/[^0-9]/', '', $_POST['cf_icode_server_port']) : '7295';
|
||||
|
||||
if (isset($_POST['cf_intercept_ip']) && $_POST['cf_intercept_ip']) {
|
||||
$pattern = explode("\n", trim($_POST['cf_intercept_ip']));
|
||||
for ($i = 0; $i < count($pattern); $i++) {
|
||||
$pattern[$i] = trim($pattern[$i]);
|
||||
if (empty($pattern[$i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern[$i] = str_replace(".", "\.", $pattern[$i]);
|
||||
$pattern[$i] = str_replace("+", "[0-9\.]+", $pattern[$i]);
|
||||
$pat = "/^{$pattern[$i]}$/";
|
||||
|
||||
if (preg_match($pat, $_SERVER['REMOTE_ADDR'])) {
|
||||
alert("현재 접속 IP : " . $_SERVER['REMOTE_ADDR'] . " 가 차단될수 있기 때문에, 다른 IP를 입력해 주세요.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$check_keys = array(
|
||||
'cf_use_email_certify' => 'int',
|
||||
'cf_use_homepage' => 'int',
|
||||
'cf_req_homepage' => 'int',
|
||||
'cf_use_tel' => 'int',
|
||||
'cf_req_tel' => 'int',
|
||||
'cf_use_hp' => 'int',
|
||||
'cf_req_hp' => 'int',
|
||||
'cf_use_addr' => 'int',
|
||||
'cf_req_addr' => 'int',
|
||||
'cf_use_signature' => 'int',
|
||||
'cf_req_signature' => 'int',
|
||||
'cf_use_profile' => 'int',
|
||||
'cf_req_profile' => 'int',
|
||||
'cf_register_level' => 'int',
|
||||
'cf_register_point' => 'int',
|
||||
'cf_icon_level' => 'int',
|
||||
'cf_use_recommend' => 'int',
|
||||
'cf_leave_day' => 'int',
|
||||
'cf_search_part' => 'int',
|
||||
'cf_email_use' => 'int',
|
||||
'cf_email_wr_super_admin' => 'int',
|
||||
'cf_email_wr_group_admin' => 'int',
|
||||
'cf_email_wr_board_admin' => 'int',
|
||||
'cf_email_wr_write' => 'int',
|
||||
'cf_email_wr_comment_all' => 'int',
|
||||
'cf_email_mb_super_admin' => 'int',
|
||||
'cf_email_mb_member' => 'int',
|
||||
'cf_email_po_super_admin' => 'int',
|
||||
'cf_prohibit_id' => 'text',
|
||||
'cf_prohibit_email' => 'text',
|
||||
'cf_new_del' => 'int',
|
||||
'cf_memo_del' => 'int',
|
||||
'cf_visit_del' => 'int',
|
||||
'cf_popular_del' => 'int',
|
||||
'cf_use_member_icon' => 'int',
|
||||
'cf_member_icon_size' => 'int',
|
||||
'cf_member_icon_width' => 'int',
|
||||
'cf_member_icon_height' => 'int',
|
||||
'cf_member_img_size' => 'int',
|
||||
'cf_member_img_width' => 'int',
|
||||
'cf_member_img_height' => 'int',
|
||||
'cf_login_minutes' => 'int',
|
||||
'cf_formmail_is_member' => 'int',
|
||||
'cf_page_rows' => 'int',
|
||||
'cf_mobile_page_rows' => 'int',
|
||||
'cf_social_login_use' => 'int',
|
||||
'cf_cert_req' => 'int',
|
||||
'cf_cert_use' => 'int',
|
||||
'cf_cert_find' => 'int',
|
||||
'cf_cert_ipin' => 'char',
|
||||
'cf_cert_hp' => 'char',
|
||||
'cf_cert_simple' => 'char',
|
||||
'cf_cert_use_seed' => 'int',
|
||||
'cf_admin_email' => 'char',
|
||||
'cf_admin_email_name' => 'char',
|
||||
'cf_add_script' => 'text',
|
||||
'cf_use_point' => 'int',
|
||||
'cf_point_term' => 'int',
|
||||
'cf_use_copy_log' => 'int',
|
||||
'cf_login_point' => 'int',
|
||||
'cf_cut_name' => 'int',
|
||||
'cf_nick_modify' => 'int',
|
||||
'cf_new_skin' => 'char',
|
||||
'cf_new_rows' => 'int',
|
||||
'cf_search_skin' => 'char',
|
||||
'cf_connect_skin' => 'char',
|
||||
'cf_faq_skin' => 'char',
|
||||
'cf_read_point' => 'int',
|
||||
'cf_write_point' => 'int',
|
||||
'cf_comment_point' => 'int',
|
||||
'cf_download_point' => 'int',
|
||||
'cf_write_pages' => 'int',
|
||||
'cf_mobile_pages' => 'int',
|
||||
'cf_link_target' => 'char',
|
||||
'cf_delay_sec' => 'int',
|
||||
'cf_filter' => 'text',
|
||||
'cf_possible_ip' => 'text',
|
||||
'cf_analytics' => 'text',
|
||||
'cf_add_meta' => 'text',
|
||||
'cf_member_skin' => 'char',
|
||||
'cf_image_extension' => 'char',
|
||||
'cf_flash_extension' => 'char',
|
||||
'cf_movie_extension' => 'char',
|
||||
'cf_visit' => 'char',
|
||||
'cf_stipulation' => 'text',
|
||||
'cf_privacy' => 'text',
|
||||
'cf_open_modify' => 'int',
|
||||
'cf_memo_send_point' => 'int',
|
||||
'cf_mobile_new_skin' => 'char',
|
||||
'cf_mobile_search_skin' => 'char',
|
||||
'cf_mobile_connect_skin' => 'char',
|
||||
'cf_mobile_faq_skin' => 'char',
|
||||
'cf_mobile_member_skin' => 'char',
|
||||
'cf_captcha_mp3' => 'char',
|
||||
'cf_cert_limit' => 'int',
|
||||
'cf_sms_use' => 'char',
|
||||
'cf_sms_type' => 'char',
|
||||
'cf_icode_id' => 'char',
|
||||
'cf_icode_pw' => 'char',
|
||||
'cf_icode_server_ip' => 'char',
|
||||
'cf_captcha' => 'char',
|
||||
'cf_syndi_token' => '',
|
||||
'cf_syndi_except' => ''
|
||||
);
|
||||
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$check_keys['cf_' . $i . '_subj'] = isset($_POST['cf_' . $i . '_subj']) ? $_POST['cf_' . $i . '_subj'] : '';
|
||||
$check_keys['cf_' . $i] = isset($_POST['cf_' . $i]) ? $_POST['cf_' . $i] : '';
|
||||
}
|
||||
|
||||
foreach ($check_keys as $k => $v) {
|
||||
if ($v === 'int') {
|
||||
$_POST[$k] = isset($_POST[$k]) ? (int) $_POST[$k] : 0;
|
||||
} else {
|
||||
if (in_array($k, array('cf_analytics', 'cf_add_meta', 'cf_add_script', 'cf_stipulation', 'cf_privacy'))) {
|
||||
$_POST[$k] = isset($_POST[$k]) ? $_POST[$k] : '';
|
||||
} else {
|
||||
$_POST[$k] = isset($_POST[$k]) ? strip_tags(clean_xss_attributes($_POST[$k])) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 본인확인을 사용할 경우 아이핀, 휴대폰인증 중 하나는 선택되어야 함
|
||||
if ($_POST['cf_cert_use'] && !$_POST['cf_cert_ipin'] && !$_POST['cf_cert_hp'] && !$_POST['cf_cert_simple']) {
|
||||
alert('본인확인을 위해 아이핀, 휴대폰 본인확인, KG이니시스 간편인증 서비스 중 하나 이상 선택해 주십시오.');
|
||||
}
|
||||
|
||||
if (!$_POST['cf_cert_use']) {
|
||||
$_POST['cf_cert_ipin'] = '';
|
||||
$_POST['cf_cert_hp'] = '';
|
||||
$_POST['cf_cert_simple'] = '';
|
||||
}
|
||||
|
||||
// 관리자가 자동등록방지를 사용해야 할 경우 ( 기본환경설정에서 최고관리자, 방문자분석 스크립트, 추가 메타태그, 추가 script, css 변경시 )
|
||||
$check_captcha = 0;
|
||||
|
||||
if ($cf_admin && $ori_config['cf_admin'] !== $cf_admin) {
|
||||
$check_captcha = 1;
|
||||
}
|
||||
|
||||
if ($_POST['cf_analytics'] && $ori_config['cf_analytics'] !== stripslashes($_POST['cf_analytics'])) {
|
||||
$check_captcha = 1;
|
||||
}
|
||||
|
||||
if ($_POST['cf_add_meta'] && $ori_config['cf_add_meta'] !== stripslashes($_POST['cf_add_meta'])) {
|
||||
$check_captcha = 1;
|
||||
}
|
||||
|
||||
if ($_POST['cf_add_script'] && $ori_config['cf_add_script'] !== stripslashes($_POST['cf_add_script'])) {
|
||||
$check_captcha = 1;
|
||||
}
|
||||
|
||||
if ($check_captcha) {
|
||||
include_once(G5_CAPTCHA_PATH . '/captcha.lib.php');
|
||||
|
||||
if (!chk_captcha()) {
|
||||
alert('자동등록방지 숫자가 틀렸습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
$sql = " update {$g5['config_table']}
|
||||
set cf_title = '{$cf_title}',
|
||||
cf_admin = '{$cf_admin}',
|
||||
cf_admin_email = '{$_POST['cf_admin_email']}',
|
||||
cf_admin_email_name = '{$_POST['cf_admin_email_name']}',
|
||||
cf_add_script = '{$_POST['cf_add_script']}',
|
||||
cf_use_point = '{$_POST['cf_use_point']}',
|
||||
cf_point_term = '{$_POST['cf_point_term']}',
|
||||
cf_use_copy_log = '{$_POST['cf_use_copy_log']}',
|
||||
cf_use_email_certify = '{$_POST['cf_use_email_certify']}',
|
||||
cf_login_point = '{$_POST['cf_login_point']}',
|
||||
cf_cut_name = '{$_POST['cf_cut_name']}',
|
||||
cf_nick_modify = '{$_POST['cf_nick_modify']}',
|
||||
cf_new_skin = '{$_POST['cf_new_skin']}',
|
||||
cf_new_rows = '{$_POST['cf_new_rows']}',
|
||||
cf_search_skin = '{$_POST['cf_search_skin']}',
|
||||
cf_connect_skin = '{$_POST['cf_connect_skin']}',
|
||||
cf_faq_skin = '{$_POST['cf_faq_skin']}',
|
||||
cf_read_point = '{$_POST['cf_read_point']}',
|
||||
cf_write_point = '{$_POST['cf_write_point']}',
|
||||
cf_comment_point = '{$_POST['cf_comment_point']}',
|
||||
cf_download_point = '{$_POST['cf_download_point']}',
|
||||
cf_write_pages = '{$_POST['cf_write_pages']}',
|
||||
cf_mobile_pages = '{$_POST['cf_mobile_pages']}',
|
||||
cf_link_target = '{$_POST['cf_link_target']}',
|
||||
cf_delay_sec = '{$_POST['cf_delay_sec']}',
|
||||
cf_filter = '{$_POST['cf_filter']}',
|
||||
cf_possible_ip = '" . trim($_POST['cf_possible_ip']) . "',
|
||||
cf_intercept_ip = '" . trim($_POST['cf_intercept_ip']) . "',
|
||||
cf_analytics = '{$_POST['cf_analytics']}',
|
||||
cf_add_meta = '{$_POST['cf_add_meta']}',
|
||||
cf_syndi_token = '{$_POST['cf_syndi_token']}',
|
||||
cf_syndi_except = '{$_POST['cf_syndi_except']}',
|
||||
cf_bbs_rewrite = '{$_POST['cf_bbs_rewrite']}',
|
||||
cf_member_skin = '{$_POST['cf_member_skin']}',
|
||||
cf_use_homepage = '{$_POST['cf_use_homepage']}',
|
||||
cf_req_homepage = '{$_POST['cf_req_homepage']}',
|
||||
cf_use_tel = '{$_POST['cf_use_tel']}',
|
||||
cf_req_tel = '{$_POST['cf_req_tel']}',
|
||||
cf_use_hp = '{$_POST['cf_use_hp']}',
|
||||
cf_req_hp = '{$_POST['cf_req_hp']}',
|
||||
cf_use_addr = '{$_POST['cf_use_addr']}',
|
||||
cf_req_addr = '{$_POST['cf_req_addr']}',
|
||||
cf_use_signature = '{$_POST['cf_use_signature']}',
|
||||
cf_req_signature = '{$_POST['cf_req_signature']}',
|
||||
cf_use_profile = '{$_POST['cf_use_profile']}',
|
||||
cf_req_profile = '{$_POST['cf_req_profile']}',
|
||||
cf_register_level = '{$_POST['cf_register_level']}',
|
||||
cf_register_point = '{$_POST['cf_register_point']}',
|
||||
cf_icon_level = '{$_POST['cf_icon_level']}',
|
||||
cf_use_recommend = '{$_POST['cf_use_recommend']}',
|
||||
cf_recommend_point = '{$_POST['cf_recommend_point']}',
|
||||
cf_leave_day = '{$_POST['cf_leave_day']}',
|
||||
cf_search_part = '{$_POST['cf_search_part']}',
|
||||
cf_email_use = '{$_POST['cf_email_use']}',
|
||||
cf_email_wr_super_admin = '{$_POST['cf_email_wr_super_admin']}',
|
||||
cf_email_wr_group_admin = '{$_POST['cf_email_wr_group_admin']}',
|
||||
cf_email_wr_board_admin = '{$_POST['cf_email_wr_board_admin']}',
|
||||
cf_email_wr_write = '{$_POST['cf_email_wr_write']}',
|
||||
cf_email_wr_comment_all = '{$_POST['cf_email_wr_comment_all']}',
|
||||
cf_email_mb_super_admin = '{$_POST['cf_email_mb_super_admin']}',
|
||||
cf_email_mb_member = '{$_POST['cf_email_mb_member']}',
|
||||
cf_email_po_super_admin = '{$_POST['cf_email_po_super_admin']}',
|
||||
cf_prohibit_id = '{$_POST['cf_prohibit_id']}',
|
||||
cf_prohibit_email = '{$_POST['cf_prohibit_email']}',
|
||||
cf_new_del = '{$_POST['cf_new_del']}',
|
||||
cf_memo_del = '{$_POST['cf_memo_del']}',
|
||||
cf_visit_del = '{$_POST['cf_visit_del']}',
|
||||
cf_popular_del = '{$_POST['cf_popular_del']}',
|
||||
cf_use_member_icon = '{$_POST['cf_use_member_icon']}',
|
||||
cf_member_icon_size = '{$_POST['cf_member_icon_size']}',
|
||||
cf_member_icon_width = '{$_POST['cf_member_icon_width']}',
|
||||
cf_member_icon_height = '{$_POST['cf_member_icon_height']}',
|
||||
cf_member_img_size = '{$_POST['cf_member_img_size']}',
|
||||
cf_member_img_width = '{$_POST['cf_member_img_width']}',
|
||||
cf_member_img_height = '{$_POST['cf_member_img_height']}',
|
||||
cf_login_minutes = '{$_POST['cf_login_minutes']}',
|
||||
cf_image_extension = '{$_POST['cf_image_extension']}',
|
||||
cf_flash_extension = '{$_POST['cf_flash_extension']}',
|
||||
cf_movie_extension = '{$_POST['cf_movie_extension']}',
|
||||
cf_formmail_is_member = '{$_POST['cf_formmail_is_member']}',
|
||||
cf_page_rows = '{$_POST['cf_page_rows']}',
|
||||
cf_mobile_page_rows = '{$_POST['cf_mobile_page_rows']}',
|
||||
cf_stipulation = '{$_POST['cf_stipulation']}',
|
||||
cf_privacy = '{$_POST['cf_privacy']}',
|
||||
cf_open_modify = '{$_POST['cf_open_modify']}',
|
||||
cf_memo_send_point = '{$_POST['cf_memo_send_point']}',
|
||||
cf_mobile_new_skin = '{$_POST['cf_mobile_new_skin']}',
|
||||
cf_mobile_search_skin = '{$_POST['cf_mobile_search_skin']}',
|
||||
cf_mobile_connect_skin = '{$_POST['cf_mobile_connect_skin']}',
|
||||
cf_mobile_faq_skin = '{$_POST['cf_mobile_faq_skin']}',
|
||||
cf_mobile_member_skin = '{$_POST['cf_mobile_member_skin']}',
|
||||
cf_captcha_mp3 = '{$_POST['cf_captcha_mp3']}',
|
||||
cf_editor = '{$_POST['cf_editor']}',
|
||||
cf_cert_use = '{$_POST['cf_cert_use']}',
|
||||
cf_cert_find = '{$_POST['cf_cert_find']}',
|
||||
cf_cert_ipin = '{$_POST['cf_cert_ipin']}',
|
||||
cf_cert_hp = '{$_POST['cf_cert_hp']}',
|
||||
cf_cert_simple = '{$_POST['cf_cert_simple']}',
|
||||
cf_cert_use_seed = '".(int)$_POST['cf_cert_use_seed']."',
|
||||
cf_cert_kg_cd = '{$_POST['cf_cert_kg_cd']}',
|
||||
cf_cert_kg_mid = '" . trim($_POST['cf_cert_kg_mid']) . "',
|
||||
cf_cert_kcb_cd = '{$_POST['cf_cert_kcb_cd']}',
|
||||
cf_cert_kcp_cd = '{$_POST['cf_cert_kcp_cd']}',
|
||||
cf_cert_kcp_enckey = '{$_POST['cf_cert_kcp_enckey']}',
|
||||
cf_cert_limit = '{$_POST['cf_cert_limit']}',
|
||||
cf_cert_req = '{$_POST['cf_cert_req']}',
|
||||
cf_sms_use = '{$_POST['cf_sms_use']}',
|
||||
cf_sms_type = '{$_POST['cf_sms_type']}',
|
||||
cf_icode_id = '{$_POST['cf_icode_id']}',
|
||||
cf_icode_pw = '{$_POST['cf_icode_pw']}',
|
||||
cf_icode_token_key = '{$_POST['cf_icode_token_key']}',
|
||||
cf_icode_server_ip = '{$_POST['cf_icode_server_ip']}',
|
||||
cf_icode_server_port = '{$_POST['cf_icode_server_port']}',
|
||||
cf_googl_shorturl_apikey = '{$_POST['cf_googl_shorturl_apikey']}',
|
||||
cf_kakao_js_apikey = '{$_POST['cf_kakao_js_apikey']}',
|
||||
cf_facebook_appid = '{$_POST['cf_facebook_appid']}',
|
||||
cf_facebook_secret = '{$_POST['cf_facebook_secret']}',
|
||||
cf_twitter_key = '{$_POST['cf_twitter_key']}',
|
||||
cf_twitter_secret = '{$_POST['cf_twitter_secret']}',
|
||||
cf_social_login_use = '{$_POST['cf_social_login_use']}',
|
||||
cf_naver_clientid = '{$_POST['cf_naver_clientid']}',
|
||||
cf_naver_secret = '{$_POST['cf_naver_secret']}',
|
||||
cf_google_clientid = '{$_POST['cf_google_clientid']}',
|
||||
cf_google_secret = '{$_POST['cf_google_secret']}',
|
||||
cf_kakao_rest_key = '{$_POST['cf_kakao_rest_key']}',
|
||||
cf_kakao_client_secret = '{$_POST['cf_kakao_client_secret']}',
|
||||
cf_social_servicelist = '{$cf_social_servicelist}',
|
||||
cf_captcha = '{$_POST['cf_captcha']}',
|
||||
cf_recaptcha_site_key = '{$_POST['cf_recaptcha_site_key']}',
|
||||
cf_recaptcha_secret_key = '{$_POST['cf_recaptcha_secret_key']}',
|
||||
cf_payco_clientid = '{$_POST['cf_payco_clientid']}',
|
||||
cf_payco_secret = '{$_POST['cf_payco_secret']}',
|
||||
cf_1_subj = '{$_POST['cf_1_subj']}',
|
||||
cf_2_subj = '{$_POST['cf_2_subj']}',
|
||||
cf_3_subj = '{$_POST['cf_3_subj']}',
|
||||
cf_4_subj = '{$_POST['cf_4_subj']}',
|
||||
cf_5_subj = '{$_POST['cf_5_subj']}',
|
||||
cf_6_subj = '{$_POST['cf_6_subj']}',
|
||||
cf_7_subj = '{$_POST['cf_7_subj']}',
|
||||
cf_8_subj = '{$_POST['cf_8_subj']}',
|
||||
cf_9_subj = '{$_POST['cf_9_subj']}',
|
||||
cf_10_subj = '{$_POST['cf_10_subj']}',
|
||||
cf_1 = '{$_POST['cf_1']}',
|
||||
cf_2 = '{$_POST['cf_2']}',
|
||||
cf_3 = '{$_POST['cf_3']}',
|
||||
cf_4 = '{$_POST['cf_4']}',
|
||||
cf_5 = '{$_POST['cf_5']}',
|
||||
cf_6 = '{$_POST['cf_6']}',
|
||||
cf_7 = '{$_POST['cf_7']}',
|
||||
cf_8 = '{$_POST['cf_8']}',
|
||||
cf_9 = '{$_POST['cf_9']}',
|
||||
cf_10 = '{$_POST['cf_10']}' ";
|
||||
sql_query($sql);
|
||||
|
||||
//sql_query(" OPTIMIZE TABLE `$g5[config_table]` ");
|
||||
|
||||
if (isset($_POST['cf_bbs_rewrite'])) {
|
||||
g5_delete_all_cache();
|
||||
}
|
||||
|
||||
if (function_exists('get_admin_captcha_by')) {
|
||||
get_admin_captcha_by('remove');
|
||||
}
|
||||
|
||||
run_event('admin_config_form_update');
|
||||
|
||||
update_rewrite_rules();
|
||||
|
||||
goto_url('./config_form.php', false);
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 공통 파일 (관리자용)
|
||||
*/
|
||||
|
||||
define('G5_IS_ADMIN', true);
|
||||
include_once('../../common.php');
|
||||
include_once(G5_ADMIN_PATH . '/admin.lib.php');
|
||||
|
||||
// 💡 [개선] 공통 함수를 별도 파일로 분리하여 관리합니다.
|
||||
include_once(__DIR__ . '/functions.php');
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) {
|
||||
// AJAX 요청 등으로 직접 접근했을 때, 그누보드 환경을 로드합니다.
|
||||
include_once('../../../common.php');
|
||||
}
|
||||
|
||||
// 💡 [개선] 공통 함수를 별도 파일로 분리하여 관리합니다.
|
||||
include_once(__DIR__ . '/functions.php');
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 메뉴
|
||||
*/
|
||||
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// 메뉴 구조: array('메뉴코드', '메뉴명', '링크', '메뉴 ID', '아이콘 클래스 (옵션)')
|
||||
$menu['menu850'] = array(
|
||||
// 💡 [개선] 메뉴 그룹 대표 아이콘 추가 (예: fa-comments)
|
||||
array('850000', '상담관리', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_manage', 'fa-comments'),
|
||||
array('850100', '대시보드', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_dashboard'),
|
||||
array('850200', '예약 현황', G5_ADMIN_URL . '/consultant_manage/reservations.php', 'consultant_reservations'),
|
||||
array('850300', '빠른 스케줄 관리', G5_ADMIN_URL . '/consultant_manage/schedule_generate.php', 'consultant_schedule_quick'),
|
||||
array('850400', '통계 분석', G5_ADMIN_URL . '/consultant_manage/statistics.php', 'consultant_statistics'),
|
||||
// 💡 [개선] 설정 관련 메뉴를 '환경설정' 그룹으로 통합
|
||||
array('850500', '팝업 샘플', G5_ADMIN_URL . '/consultant_manage/sample_page.php', 'consultant_sample'),
|
||||
array('850600', '환경설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings_group', 'fa-cogs'),
|
||||
array('850610', '기본/운영 설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings'),
|
||||
array('850615', '리소스(상담사) 관리', G5_ADMIN_URL . '/consultant_manage/resources.php', 'consultant_resources'), // 💡 [추가] 리소스 관리 메뉴
|
||||
array('850620', '알림 템플릿', G5_ADMIN_URL . '/consultant_manage/templates.php', 'consultant_templates'),
|
||||
array('850630', '시스템 로그', G5_ADMIN_URL . '/consultant_manage/log_view.php', 'consultant_log_view'),
|
||||
array('850640', '설치/업데이트', G5_ADMIN_URL . '/consultant_manage/install.php', 'consultant_install'),
|
||||
// array('850900', '시스템 테스트', G5_ADMIN_URL . '/consultant_manage/test_system.php', 'consultant_test') // 개발용 메뉴는 주석 처리
|
||||
);
|
||||
?>
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* 스케줄 생성 AJAX 처리
|
||||
*/
|
||||
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
die(json_encode(['success' => false, 'message' => '권한이 없습니다.']));
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
die(json_encode(['success' => false, 'message' => '상담 예약 시스템이 설치되지 않았습니다.']));
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$year = (int) ($_POST['year'] ?? date('Y'));
|
||||
$month = (int) ($_POST['month'] ?? date('n'));
|
||||
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
|
||||
try {
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
switch ($action) {
|
||||
case 'generate_month':
|
||||
$result = $generator->generateMonth($year, $month);
|
||||
if ($result) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "{$year}년 {$month}월 스케줄이 생성되었습니다."
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '스케줄 생성에 실패했습니다.'
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'check_conflicts':
|
||||
$conflicts = $generator->checkScheduleConflicts($year, $month);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'conflicts' => $conflicts,
|
||||
'message' => count($conflicts) > 0 ?
|
||||
count($conflicts) . '개의 충돌이 발견되었습니다.' :
|
||||
'충돌이 없습니다.'
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'regenerate_next_month':
|
||||
$nextMonth = date('n') == 12 ? 1 : date('n') + 1;
|
||||
$nextYear = date('n') == 12 ? date('Y') + 1 : date('Y');
|
||||
|
||||
$result = $generator->generateMonth($nextYear, $nextMonth);
|
||||
if ($result) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "다음 달({$nextYear}년 {$nextMonth}월) 스케줄이 재생성되었습니다."
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '다음 달 스케줄 재생성에 실패했습니다.'
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['success' => false, 'message' => '잘못된 요청입니다.']);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => '오류가 발생했습니다: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 클래스
|
||||
*/
|
||||
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
class ReservationManager
|
||||
{
|
||||
|
||||
private $table_name = 'consultant_reservations';
|
||||
|
||||
/**
|
||||
* 예약 목록 조회
|
||||
*/
|
||||
public function getReservationList($status = '', $date = '', $page = 1, $per_page = 20, $search_type = '', $search_keyword = '')
|
||||
{
|
||||
try {
|
||||
$where_conditions = ["is_deleted = 0"];
|
||||
|
||||
// 상태 필터
|
||||
if (!empty($status)) {
|
||||
$where_conditions[] = "status = '" . sql_real_escape_string($status) . "'";
|
||||
}
|
||||
|
||||
// 날짜 필터
|
||||
if (!empty($date)) {
|
||||
$where_conditions[] = "reservation_date = '" . sql_real_escape_string($date) . "'";
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
if (!empty($search_keyword) && !empty($search_type)) {
|
||||
$search_keyword = sql_real_escape_string($search_keyword);
|
||||
switch ($search_type) {
|
||||
case 'customer_name':
|
||||
$where_conditions[] = "customer_name LIKE '%{$search_keyword}%'";
|
||||
break;
|
||||
case 'customer_phone':
|
||||
$where_conditions[] = "customer_phone LIKE '%{$search_keyword}%'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$where_clause = implode(' AND ', $where_conditions);
|
||||
|
||||
// 전체 개수 조회
|
||||
$count_sql = "SELECT COUNT(*) as total FROM {$this->table_name} WHERE {$where_clause}";
|
||||
$count_result = sql_fetch($count_sql);
|
||||
$total = $count_result['total'];
|
||||
|
||||
// 페이징 계산
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$total_pages = ceil($total / $per_page);
|
||||
|
||||
// 목록 조회
|
||||
$sql = "SELECT * FROM {$this->table_name}
|
||||
WHERE {$where_clause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT {$offset}, {$per_page}";
|
||||
|
||||
$reservations = [];
|
||||
$result = sql_query($sql);
|
||||
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$reservations[] = $row;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'reservations' => $reservations,
|
||||
'pagination' => [
|
||||
'current_page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total' => $total,
|
||||
'total_pages' => $total_pages
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("예약 목록 조회 실패: " . $e->getMessage(), 'error');
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '예약 목록 조회 중 오류가 발생했습니다.'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 생성
|
||||
*/
|
||||
public function createReservation($data)
|
||||
{
|
||||
try {
|
||||
// 필수 필드 검증
|
||||
$required_fields = ['customer_name', 'customer_phone', 'reservation_date', 'reservation_time'];
|
||||
foreach ($required_fields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
throw new Exception("필수 필드가 누락되었습니다: {$field}");
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 예약 확인
|
||||
$check_sql = "SELECT COUNT(*) as count FROM {$this->table_name}
|
||||
WHERE reservation_date = '" . sql_real_escape_string($data['reservation_date']) . "'
|
||||
AND reservation_time = '" . sql_real_escape_string($data['reservation_time']) . "'
|
||||
AND status IN ('payment_pending', 'reserved')
|
||||
AND is_deleted = 0";
|
||||
|
||||
$check_result = sql_fetch($check_sql);
|
||||
$current_count = $check_result['count'];
|
||||
|
||||
// 최대 인원 확인 (기본값 2명)
|
||||
$max_persons = consultant_get_config('default_max_persons', 2);
|
||||
if ($current_count >= $max_persons) {
|
||||
throw new Exception('해당 시간대는 예약이 마감되었습니다.');
|
||||
}
|
||||
|
||||
// 데이터 준비
|
||||
$insert_data = [
|
||||
'customer_name' => sql_real_escape_string($data['customer_name']),
|
||||
'customer_phone' => sql_real_escape_string($data['customer_phone']),
|
||||
'customer_email' => sql_real_escape_string($data['customer_email'] ?? ''),
|
||||
'reservation_date' => sql_real_escape_string($data['reservation_date']),
|
||||
'reservation_time' => sql_real_escape_string($data['reservation_time']),
|
||||
'consultation_type' => sql_real_escape_string($data['consultation_type'] ?? 'onsite'),
|
||||
'status' => 'payment_pending',
|
||||
'payment_amount' => (int) ($data['payment_amount'] ?? consultant_get_config('consultation_fee', 50000)),
|
||||
'payment_status' => 'pending',
|
||||
'request_memo' => sql_real_escape_string($data['request_memo'] ?? ''),
|
||||
'wr_id' => (int) ($data['wr_id'] ?? 0),
|
||||
'temp_1' => sql_real_escape_string($data['temp_1'] ?? ''),
|
||||
'created_at' => 'NOW()',
|
||||
'updated_at' => 'NOW()'
|
||||
];
|
||||
|
||||
// SQL 생성
|
||||
$fields = implode(', ', array_keys($insert_data));
|
||||
$values = "'" . implode("', '", array_values($insert_data)) . "'";
|
||||
$values = str_replace("'NOW()'", "NOW()", $values); // NOW() 함수 처리
|
||||
|
||||
$sql = "INSERT INTO {$this->table_name} ({$fields}) VALUES ({$values})";
|
||||
|
||||
if (!sql_query($sql)) {
|
||||
throw new Exception('데이터베이스 저장 실패: ' . sql_error());
|
||||
}
|
||||
|
||||
$reservation_id = sql_insert_id();
|
||||
|
||||
// 알림 발송
|
||||
$this->sendReservationNotification($reservation_id, 'created');
|
||||
|
||||
consultant_log("새 예약 생성: ID {$reservation_id}, 고객: {$data['customer_name']}");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '예약이 성공적으로 접수되었습니다.',
|
||||
'data' => [
|
||||
'reservation_id' => $reservation_id
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("예약 생성 실패: " . $e->getMessage(), 'error');
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 상태 변경
|
||||
*/
|
||||
public function updateReservationStatus($reservation_id, $new_status, $admin_memo = '')
|
||||
{
|
||||
try {
|
||||
// 예약 정보 조회
|
||||
$reservation = $this->getReservationById($reservation_id);
|
||||
if (!$reservation) {
|
||||
throw new Exception('예약 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 상태 변경 가능 여부 확인
|
||||
$valid_transitions = [
|
||||
'payment_pending' => ['reserved', 'cancelled'],
|
||||
'reserved' => ['completed', 'cancelled'],
|
||||
'completed' => [],
|
||||
'cancelled' => []
|
||||
];
|
||||
|
||||
$current_status = $reservation['status'];
|
||||
if (!in_array($new_status, $valid_transitions[$current_status])) {
|
||||
throw new Exception('현재 상태에서 해당 상태로 변경할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
$sql = "UPDATE {$this->table_name}
|
||||
SET status = '" . sql_real_escape_string($new_status) . "',
|
||||
admin_memo = '" . sql_real_escape_string($admin_memo) . "',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$reservation_id}";
|
||||
|
||||
if (!sql_query($sql)) {
|
||||
throw new Exception('상태 변경 실패: ' . sql_error());
|
||||
}
|
||||
|
||||
// 알림 발송
|
||||
$this->sendReservationNotification($reservation_id, 'status_changed', $new_status);
|
||||
|
||||
consultant_log("예약 상태 변경: ID {$reservation_id}, {$current_status} -> {$new_status}");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '예약 상태가 성공적으로 변경되었습니다.'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("예약 상태 변경 실패: " . $e->getMessage(), 'error');
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 입금 확인
|
||||
*/
|
||||
public function confirmPayment($reservation_id, $admin_id)
|
||||
{
|
||||
try {
|
||||
// 예약 정보 조회
|
||||
$reservation = $this->getReservationById($reservation_id);
|
||||
if (!$reservation) {
|
||||
throw new Exception('예약 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if ($reservation['status'] !== 'payment_pending') {
|
||||
throw new Exception('입금 대기 상태의 예약만 확인할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 입금 확인 처리
|
||||
$sql = "UPDATE {$this->table_name}
|
||||
SET status = 'reserved',
|
||||
payment_status = 'paid',
|
||||
payment_confirmed_at = NOW(),
|
||||
payment_confirmed_by = '" . sql_real_escape_string($admin_id) . "',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$reservation_id}";
|
||||
|
||||
if (!sql_query($sql)) {
|
||||
throw new Exception('입금 확인 처리 실패: ' . sql_error());
|
||||
}
|
||||
|
||||
// 알림 발송
|
||||
$this->sendReservationNotification($reservation_id, 'payment_confirmed');
|
||||
|
||||
consultant_log("입금 확인: ID {$reservation_id}, 확인자: {$admin_id}");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '입금이 확인되어 예약이 확정되었습니다.'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("입금 확인 실패: " . $e->getMessage(), 'error');
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 취소
|
||||
*/
|
||||
public function cancelReservation($reservation_id, $reason)
|
||||
{
|
||||
try {
|
||||
// 예약 정보 조회
|
||||
$reservation = $this->getReservationById($reservation_id);
|
||||
if (!$reservation) {
|
||||
throw new Exception('예약 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if ($reservation['status'] === 'cancelled') {
|
||||
throw new Exception('이미 취소된 예약입니다.');
|
||||
}
|
||||
|
||||
if ($reservation['status'] === 'completed') {
|
||||
throw new Exception('완료된 예약은 취소할 수 없습니다.');
|
||||
}
|
||||
|
||||
// 예약 취소 처리
|
||||
$sql = "UPDATE {$this->table_name}
|
||||
SET status = 'cancelled',
|
||||
admin_memo = '" . sql_real_escape_string($reason) . "',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$reservation_id}";
|
||||
|
||||
if (!sql_query($sql)) {
|
||||
throw new Exception('예약 취소 처리 실패: ' . sql_error());
|
||||
}
|
||||
|
||||
// 알림 발송
|
||||
$this->sendReservationNotification($reservation_id, 'cancelled', $reason);
|
||||
|
||||
consultant_log("예약 취소: ID {$reservation_id}, 사유: {$reason}");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '예약이 성공적으로 취소되었습니다.'
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("예약 취소 실패: " . $e->getMessage(), 'error');
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 정보 조회
|
||||
*/
|
||||
public function getReservationById($reservation_id)
|
||||
{
|
||||
$sql = "SELECT * FROM {$this->table_name} WHERE id = {$reservation_id} AND is_deleted = 0";
|
||||
return sql_fetch($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 알림 발송
|
||||
*/
|
||||
private function sendReservationNotification($reservation_id, $type, $extra_data = null)
|
||||
{
|
||||
try {
|
||||
$reservation = $this->getReservationById($reservation_id);
|
||||
if (!$reservation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 알림 템플릿 키 결정
|
||||
$template_keys = [
|
||||
'created' => 'consultant_reservation_customer',
|
||||
'payment_confirmed' => 'consultant_confirmed_customer',
|
||||
'cancelled' => 'consultant_cancelled_customer',
|
||||
'status_changed' => 'consultant_status_changed_customer'
|
||||
];
|
||||
|
||||
$template_key = $template_keys[$type] ?? null;
|
||||
if (!$template_key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 템플릿 변수 준비
|
||||
$variables = [
|
||||
'customer_name' => $reservation['customer_name'],
|
||||
'customer_phone' => $reservation['customer_phone'],
|
||||
'customer_email' => $reservation['customer_email'],
|
||||
'reservation_date' => $reservation['reservation_date'],
|
||||
'reservation_time' => $reservation['reservation_time'],
|
||||
'payment_amount' => number_format($reservation['payment_amount']),
|
||||
'account_info' => consultant_get_config('account_info', ''),
|
||||
'cancel_reason' => $extra_data ?? ''
|
||||
];
|
||||
|
||||
// 이메일 발송
|
||||
if (!empty($reservation['customer_email'])) {
|
||||
$this->sendEmailNotification($reservation['customer_email'], $template_key, $variables);
|
||||
}
|
||||
|
||||
// SMS 발송
|
||||
if (!empty($reservation['customer_phone'])) {
|
||||
$this->sendSmsNotification($reservation['customer_phone'], $template_key, $variables);
|
||||
}
|
||||
|
||||
// 관리자 알림 (새 예약 시)
|
||||
if ($type === 'created') {
|
||||
$this->sendAdminNotification($reservation, $variables);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("알림 발송 실패: " . $e->getMessage(), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 알림 발송
|
||||
*/
|
||||
private function sendEmailNotification($email, $template_key, $variables)
|
||||
{
|
||||
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
|
||||
return consultant_send_notification('email', $template_key, array_merge($variables, ['customer_email' => $email]));
|
||||
}
|
||||
|
||||
/**
|
||||
* SMS 알림 발송
|
||||
*/
|
||||
private function sendSmsNotification($phone, $template_key, $variables)
|
||||
{
|
||||
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
|
||||
return consultant_send_notification('sms', $template_key, array_merge($variables, ['customer_phone' => $phone]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 알림 발송
|
||||
*/
|
||||
private function sendAdminNotification($reservation, $variables)
|
||||
{
|
||||
// 관리자 이메일 알림
|
||||
$admin_template_key = 'consultant_reservation_admin';
|
||||
|
||||
// 관리자 이메일 주소 (설정에서 가져오거나 기본값 사용)
|
||||
$admin_email = consultant_get_config('admin_email', get_admin_email());
|
||||
|
||||
if ($admin_email) {
|
||||
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
|
||||
consultant_send_notification('email', $admin_template_key, array_merge($variables, ['customer_email' => $admin_email]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 변수 치환
|
||||
*/
|
||||
private function replaceTemplateVariables($template, $variables)
|
||||
{
|
||||
foreach ($variables as $key => $value) {
|
||||
$template = str_replace('{' . $key . '}', $value, $template);
|
||||
}
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 예약 통계 조회
|
||||
*/
|
||||
public function getReservationStats($start_date = null, $end_date = null)
|
||||
{
|
||||
try {
|
||||
if (!$start_date)
|
||||
$start_date = date('Y-m-01'); // 이번 달 첫날
|
||||
if (!$end_date)
|
||||
$end_date = date('Y-m-d'); // 오늘
|
||||
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
|
||||
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
|
||||
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as total_revenue
|
||||
FROM {$this->table_name}
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0";
|
||||
|
||||
return sql_fetch($sql);
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("통계 조회 실패: " . $e->getMessage(), 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 - 월별 스케줄 자동 생성 클래스
|
||||
*
|
||||
* 요일별 설정을 기반으로 월별 상세 스케줄을 자동 생성하고 관리합니다.
|
||||
*/
|
||||
|
||||
class ScheduleGenerator
|
||||
{
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $connect_db;
|
||||
$this->db = $connect_db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 월의 전체 스케줄 생성
|
||||
*
|
||||
* @param int $year 년도
|
||||
* @param int $month 월
|
||||
* @return bool 성공 여부
|
||||
*/
|
||||
public function generateMonth($year, $month)
|
||||
{
|
||||
try {
|
||||
// 기본 설정 조회
|
||||
$basicSettings = $this->getBasicSettings();
|
||||
|
||||
// 요일별 설정 조회
|
||||
$weeklySettings = $this->getWeeklySettings();
|
||||
|
||||
// 해당 월의 모든 날짜 생성
|
||||
$dates = self::getMonthDates($year, $month);
|
||||
|
||||
// 기존 스케줄 중 예약이 없는 것들 삭제 (재생성을 위해)
|
||||
$this->clearAutoGeneratedSchedules($year, $month);
|
||||
|
||||
// 각 날짜별로 스케줄 생성
|
||||
foreach ($dates as $date) {
|
||||
$dayOfWeek = date('N', strtotime($date)); // 1=월요일, 7=일요일
|
||||
$this->generateDay($date, $dayOfWeek, $weeklySettings, $basicSettings);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("스케줄 생성 실패: " . $e->getMessage(), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 스케줄 생성
|
||||
*/
|
||||
private function generateDay($date, $dayOfWeek, $weeklySettings, $basicSettings)
|
||||
{
|
||||
$dayNames = ['', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$dayName = $dayNames[$dayOfWeek];
|
||||
|
||||
// 해당 요일이 운영일인지 확인
|
||||
$isEnabled = $weeklySettings[$dayName . '_enabled'] ?? '0';
|
||||
|
||||
if ($isEnabled == '0') {
|
||||
// 휴무일 처리
|
||||
$this->insertHolidaySchedule($date);
|
||||
return;
|
||||
}
|
||||
|
||||
// 운영시간 정보
|
||||
$startTime = $weeklySettings[$dayName . '_start'] ?? '09:00';
|
||||
$endTime = $weeklySettings[$dayName . '_end'] ?? '18:00';
|
||||
$lunchStart = $weeklySettings[$dayName . '_lunch_start'] ?? '12:00';
|
||||
$lunchEnd = $weeklySettings[$dayName . '_lunch_end'] ?? '13:00';
|
||||
|
||||
// 시간 슬롯 생성
|
||||
$this->createTimeSlots($date, $startTime, $endTime, $basicSettings['consultation_duration'], $basicSettings['max_persons_per_slot']);
|
||||
|
||||
// 점심시간 블록 처리
|
||||
// 💡 [수정] 시작 시간과 종료 시간이 다를 때만 점심시간으로 처리합니다.
|
||||
if ($lunchStart && $lunchEnd && strtotime($lunchStart) < strtotime($lunchEnd)) {
|
||||
$this->blockLunchTime($date, $lunchStart, $lunchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 슬롯 생성
|
||||
*/
|
||||
private function createTimeSlots($date, $startTime, $endTime, $slotDuration, $maxPersons)
|
||||
{
|
||||
$currentTime = strtotime($startTime);
|
||||
$endTimeStamp = strtotime($endTime);
|
||||
$slotMinutes = (int) $slotDuration;
|
||||
|
||||
while ($currentTime < $endTimeStamp) {
|
||||
$slotStart = date('H:i', $currentTime);
|
||||
$slotEnd = date('H:i', $currentTime + ($slotMinutes * 60));
|
||||
|
||||
// 종료시간을 넘지 않는 경우만 생성
|
||||
if (strtotime($slotEnd) <= $endTimeStamp) {
|
||||
// 재생성 시 중복 생성을 방지하기 위해 기존 슬롯이 있는지 확인
|
||||
$check_sql = "SELECT id FROM consultant_schedule
|
||||
WHERE specific_date = '" . sql_real_escape_string($date) . "'
|
||||
AND start_time = '" . sql_real_escape_string($slotStart) . "'";
|
||||
|
||||
$existing_slot = sql_fetch($check_sql);
|
||||
|
||||
// 기존에 수동으로 추가되었거나 예약으로 보호된 슬롯이 없으면 새로 생성
|
||||
if (!$existing_slot) {
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, created_at)
|
||||
VALUES (
|
||||
'" . sql_real_escape_string($date) . "',
|
||||
'" . sql_real_escape_string($slotStart) . "',
|
||||
'" . sql_real_escape_string($slotEnd) . "',
|
||||
" . (int) $slotMinutes . ",
|
||||
" . (int) $maxPersons . ",
|
||||
1,
|
||||
'auto_generated',
|
||||
NOW()
|
||||
)";
|
||||
|
||||
if (!sql_query($sql)) {
|
||||
// sql_query가 false를 반환하면 오류를 던짐
|
||||
throw new Exception("시간 슬롯 생성 실패: " . sql_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
$currentTime += ($slotMinutes * 60);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 점심시간 블록 처리
|
||||
*/
|
||||
private function blockLunchTime($date, $lunchStart, $lunchEnd)
|
||||
{
|
||||
$sql = "UPDATE consultant_schedule
|
||||
SET is_available = 0, temp_1 = 'lunch_time', temp_2 = '점심시간', updated_at = NOW()
|
||||
WHERE specific_date = '" . sql_real_escape_string($date) . "'
|
||||
-- 💡 [수정] start_time이 휴게시간 범위에 포함되는 모든 슬롯을 대상으로 하도록 변경합니다.
|
||||
-- 이렇게 하면 상담 시간 단위(30분, 60분 등)에 상관없이 정확하게 동작합니다.
|
||||
AND start_time >= '" . sql_real_escape_string($lunchStart) . "' AND start_time < '" . sql_real_escape_string($lunchEnd) . "'";
|
||||
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴무일 스케줄 삽입
|
||||
*/
|
||||
private function insertHolidaySchedule($date)
|
||||
{
|
||||
// 💡 [수정] 휴무일 데이터 중복 생성을 방지하기 위해, 해당 날짜에 이미 스케줄이 있는지 확인합니다.
|
||||
$check_sql = "SELECT id FROM consultant_schedule WHERE specific_date = '" . sql_real_escape_string($date) . "'";
|
||||
$existing_schedule = sql_fetch($check_sql);
|
||||
|
||||
// 해당 날짜에 아무 스케줄도 없을 때만 휴무일 데이터를 삽입합니다.
|
||||
if (!$existing_schedule) {
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, temp_2, created_at)
|
||||
VALUES (
|
||||
'" . sql_real_escape_string($date) . "',
|
||||
'00:00',
|
||||
'23:59',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
'holiday',
|
||||
'휴무일',
|
||||
NOW()
|
||||
)";
|
||||
|
||||
sql_query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 예약 보호 로직 - 예약이 있는 시간대는 삭제하지 않음
|
||||
*/
|
||||
private function protectExistingReservations($year, $month)
|
||||
{
|
||||
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
// 해당 월에 예약이 있는 스케줄 ID 조회
|
||||
$sql = "SELECT DISTINCT s.id
|
||||
FROM consultant_schedule s
|
||||
INNER JOIN consultant_reservations r ON (
|
||||
s.specific_date = r.reservation_date
|
||||
AND s.start_time = r.reservation_time
|
||||
)
|
||||
WHERE s.specific_date BETWEEN '{$startDate}' AND '{$endDate}'
|
||||
AND r.status NOT IN ('cancelled')
|
||||
AND r.is_deleted = 0";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$protectedIds = [];
|
||||
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$protectedIds[] = $row['id'];
|
||||
}
|
||||
|
||||
return $protectedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 변경으로 인한 기존 예약과의 충돌 감지 (향상된 로직)
|
||||
* - 휴무일로 변경된 경우
|
||||
* - 운영 시간 밖으로 밀려난 경우
|
||||
* - 점심시간과 겹치는 경우
|
||||
*/
|
||||
public function findConflictsWithNewSettings($year, $month)
|
||||
{
|
||||
$conflicts = [];
|
||||
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
// 해당 월의 모든 예약 조회
|
||||
$sql = "SELECT r.reservation_date, r.reservation_time, r.customer_name, r.customer_phone
|
||||
FROM consultant_reservations r
|
||||
WHERE r.reservation_date BETWEEN '{$startDate}' AND '{$endDate}'
|
||||
AND r.status NOT IN ('cancelled')
|
||||
AND r.is_deleted = 0";
|
||||
|
||||
$weeklySettings = $this->getWeeklySettings();
|
||||
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$dayOfWeek = date('N', strtotime($row['reservation_date']));
|
||||
$dayNames = ['', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$dayName = $dayNames[$dayOfWeek];
|
||||
|
||||
$isEnabled = $weeklySettings[$dayName . '_enabled'] ?? '0';
|
||||
$startTime = $weeklySettings[$dayName . '_start'] ?? '09:00';
|
||||
$endTime = $weeklySettings[$dayName . '_end'] ?? '18:00';
|
||||
$lunchStart = $weeklySettings[$dayName . '_lunch_start'] ?? '12:00';
|
||||
$lunchEnd = $weeklySettings[$dayName . '_lunch_end'] ?? '13:00';
|
||||
|
||||
$reservationTime = $row['reservation_time'];
|
||||
$conflictReason = '';
|
||||
|
||||
if ($isEnabled == '0') {
|
||||
$conflictReason = '휴무일로 변경됨';
|
||||
} elseif (strtotime($reservationTime) < strtotime($startTime) || strtotime($reservationTime) >= strtotime($endTime)) {
|
||||
$conflictReason = '운영 시간 벗어남';
|
||||
} elseif (strtotime($reservationTime) >= strtotime($lunchStart) && strtotime($reservationTime) < strtotime($lunchEnd)) {
|
||||
$conflictReason = '점심시간과 겹침';
|
||||
}
|
||||
|
||||
if ($conflictReason) {
|
||||
$conflicts[] = [
|
||||
'date' => $row['reservation_date'],
|
||||
'time' => $reservationTime,
|
||||
'customer' => $row['customer_name'],
|
||||
'phone' => $row['customer_phone'],
|
||||
'reason' => $conflictReason
|
||||
];
|
||||
}
|
||||
}
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 생성된 스케줄 삭제 (예약이 없는 것만)
|
||||
*/
|
||||
private function clearAutoGeneratedSchedules($year, $month)
|
||||
{
|
||||
$startDate = sprintf('%04d-%02d-01', (int)$year, (int)$month);
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
// 예약이 있는 '시간 슬롯'의 ID를 보호
|
||||
$protectedIds = $this->protectExistingReservations($year, $month);
|
||||
|
||||
// 💡 [수정] 예약이 있는 날짜라도 'holiday' 타입의 스케줄은 삭제될 수 있도록 temp_1 조건만 사용합니다.
|
||||
$whereClause = "specific_date BETWEEN '{$startDate}' AND '{$endDate}'
|
||||
AND temp_1 IN ('auto_generated', 'lunch_time', 'holiday', 'manual_block')";
|
||||
|
||||
if (!empty($protectedIds)) {
|
||||
$whereClause .= " AND id NOT IN (" . implode(',', $protectedIds) . ")";
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM consultant_schedule WHERE " . $whereClause;
|
||||
sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 월의 모든 날짜 배열 생성
|
||||
*/
|
||||
private static function getMonthDates($year, $month)
|
||||
{
|
||||
$dates = [];
|
||||
$daysInMonth = date('t', mktime(0, 0, 0, $month, 1, $year));
|
||||
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$dates[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 설정 조회
|
||||
*/
|
||||
private function getBasicSettings()
|
||||
{
|
||||
return [
|
||||
'consultation_duration' => (int) consultant_get_config('consultation_duration', 60),
|
||||
'max_persons_per_slot' => (int) consultant_get_config('max_persons_per_slot', 2)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일별 설정 조회
|
||||
*/
|
||||
private function getWeeklySettings()
|
||||
{
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$settings = [];
|
||||
|
||||
foreach ($days as $day) {
|
||||
$settings[$day . '_enabled'] = consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1');
|
||||
$settings[$day . '_start'] = consultant_get_config($day . '_start', '09:00');
|
||||
$settings[$day . '_end'] = consultant_get_config($day . '_end', '18:00');
|
||||
$settings[$day . '_lunch_start'] = consultant_get_config($day . '_lunch_start', '12:00');
|
||||
$settings[$day . '_lunch_end'] = consultant_get_config($day . '_lunch_end', '13:00');
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
// ❗ [핵심] 필요한 클래스와 라이브러리를 포함합니다.
|
||||
if (file_exists(G5_ADMIN_PATH . '/mail_manage/classes/MailSender.php')) {
|
||||
require_once(G5_ADMIN_PATH . '/mail_manage/classes/MailSender.php');
|
||||
}
|
||||
if (file_exists(G5_PLUGIN_PATH . '/sms5/sms5.lib.php')) {
|
||||
include_once(G5_PLUGIN_PATH . '/sms5/sms5.lib.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 메일/SMS 발송 클래스
|
||||
* 기존 mail_manage, sms_admin 시스템을 활용하여 발송 및 이력 기록
|
||||
*/
|
||||
class NotificationSender
|
||||
{
|
||||
private $g5;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $g5;
|
||||
$this->g5 = $g5;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [핵심 수정] 템플릿 기반 메인 발송 함수
|
||||
* @param array $params 발송 파라미터
|
||||
* @return array 발송 결과
|
||||
*/
|
||||
public function send($params)
|
||||
{
|
||||
// 파라미터 검증
|
||||
$validated = $this->validateParams($params);
|
||||
if (!$validated['success']) {
|
||||
return $validated;
|
||||
}
|
||||
|
||||
// 대상 회원 조회
|
||||
$members = $this->getTargetMembers($params);
|
||||
if (empty($members)) {
|
||||
return ['success' => false, 'message' => '발송 대상 회원이 없습니다.'];
|
||||
}
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'total_targets' => count($members),
|
||||
'sms_success' => 0,
|
||||
'sms_fail' => 0,
|
||||
'email_success' => 0,
|
||||
'email_fail' => 0,
|
||||
'message' => ''
|
||||
];
|
||||
|
||||
// SMS 발송
|
||||
if (!empty($params['sms_template_key'])) {
|
||||
$sms_result = $this->sendSMS($members, $params['sms_template_key'], $params['vars'] ?? []);
|
||||
$results['sms_success'] = $sms_result['success'];
|
||||
$results['sms_fail'] = $sms_result['fail'];
|
||||
}
|
||||
|
||||
// 이메일 발송
|
||||
if (!empty($params['email_template_key'])) {
|
||||
$email_result = $this->sendEmail($members, $params['email_template_key'], $params['vars'] ?? []);
|
||||
$results['email_success'] = $email_result['success'];
|
||||
$results['email_fail'] = $email_result['fail'];
|
||||
}
|
||||
|
||||
$results['message'] = $this->generateResultMessage($results);
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [핵심 수정] 템플릿 기반 파라미터 검증
|
||||
*/
|
||||
private function validateParams($params)
|
||||
{
|
||||
if (empty($params['target_type'])) {
|
||||
return ['success' => false, 'message' => "필수 파라미터 'target_type'이 누락되었습니다."];
|
||||
}
|
||||
if ($params['target_type'] === 'single' && empty($params['member_id'])) {
|
||||
return ['success' => false, 'message' => '단일 발송 시 회원 ID(member_id)가 필요합니다.'];
|
||||
}
|
||||
if ($params['target_type'] === 'bulk' && empty($params['member_levels'])) {
|
||||
return ['success' => false, 'message' => '대량 발송 시 회원 레벨(member_levels)이 필요합니다.'];
|
||||
}
|
||||
if (empty($params['sms_template_key']) && empty($params['email_template_key'])) {
|
||||
return ['success' => false, 'message' => 'SMS 또는 이메일 템플릿 키 중 하나는 반드시 필요합니다.'];
|
||||
}
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상 회원 조회
|
||||
*/
|
||||
private function getTargetMembers($params)
|
||||
{
|
||||
$members = [];
|
||||
$member_table = $this->g5['member_table'];
|
||||
|
||||
if ($params['target_type'] === 'single') {
|
||||
$sql = "SELECT mb_id, mb_name, mb_hp, mb_email, mb_sms, mb_mailling FROM `{$member_table}` WHERE mb_id = '" . sql_real_escape_string($params['member_id']) . "' AND mb_leave_date = '' AND mb_intercept_date = ''";
|
||||
} else {
|
||||
$levels = array_map('intval', $params['member_levels']);
|
||||
$level_condition = implode(',', $levels);
|
||||
$sql = "SELECT mb_id, mb_name, mb_hp, mb_email, mb_sms, mb_mailling FROM `{$member_table}` WHERE mb_level IN ({$level_condition}) AND mb_leave_date = '' AND mb_intercept_date = ''";
|
||||
}
|
||||
$this->write_debug_log("[SMS 발송 시작] sql '{$sql}'");
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$members[] = $row;
|
||||
}
|
||||
return $members;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [핵심 수정] SMS 발송 (템플릿 및 변수 처리, 이력 기록 포함)
|
||||
*/
|
||||
private function sendSMS($members, $template_key, $common_vars)
|
||||
{
|
||||
$success = 0;
|
||||
$fail = 0;
|
||||
$notification_mode = get_order_config('notification_mode', 'log');
|
||||
$is_test_mode = ($notification_mode !== 'send');
|
||||
|
||||
$sizeof_members = count($members);
|
||||
|
||||
// 💡 [수정] 템플릿 조회 로직 변경: 지정 테이블(consultant_sms_templates) -> 기본 테이블(sms_templates) 순서로 조회
|
||||
$template = null;
|
||||
|
||||
// 1. 지정 테이블 조회
|
||||
$check_table = sql_query("SHOW TABLES LIKE 'consultant_sms_templates'", false);
|
||||
if (sql_num_rows($check_table) > 0) {
|
||||
$template = sql_fetch("SELECT * FROM `consultant_sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
|
||||
if ($template) {
|
||||
// 필드명 통일 (기본 테이블과 필드명이 다를 수 있음)
|
||||
$template['content'] = $template['template_content'];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 기본 테이블 조회 (지정 테이블에 없을 경우)
|
||||
if (!$template) {
|
||||
$template = sql_fetch("SELECT * FROM `sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
|
||||
}
|
||||
|
||||
// 3. 둘 다 없으면 에러 처리
|
||||
if (!$template) {
|
||||
$this->write_debug_log("[SMS 발송 오류] 템플릿 '{$template_key}'을(를) 찾을 수 없습니다.");
|
||||
return ['success' => 0, 'fail' => count($members)];
|
||||
}
|
||||
|
||||
$count= count($members);
|
||||
if ($is_test_mode) {
|
||||
// --- 개발 모드: 로그 파일에만 기록 ---
|
||||
foreach ($members as $member) {
|
||||
if ($member['mb_sms'] && !empty($member['mb_hp'])) {
|
||||
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
|
||||
$personal_message = $template['content'];
|
||||
foreach ($personal_vars as $key => $value) {
|
||||
$personal_message = str_replace('{' . $key . '}', $value, $personal_message);
|
||||
}
|
||||
$this->write_debug_log("[SMS LOG] To: {$member['mb_hp']}, Content: {$personal_message}");
|
||||
$success++;
|
||||
} else {
|
||||
$empty = !empty($member['mb_hp']);
|
||||
$this->write_debug_log("[SMS 발송오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// --- 실제 발송 모드: DB 기록 및 실제 발송 ---
|
||||
$sms_config = sql_fetch("SELECT * FROM {$this->g5['sms5_config_table']}");
|
||||
$send_phone = $sms_config['cf_phone'];
|
||||
|
||||
$wr_message = sql_real_escape_string($template['content']);
|
||||
$wr_reply = sql_real_escape_string($send_phone);
|
||||
sql_query("INSERT INTO {$this->g5['sms5_write_table']} (wr_message, wr_reply, wr_total, wr_datetime) VALUES ('{$wr_message}', '{$wr_reply}', '" . count($members) . "', '" . G5_TIME_YMDHIS . "')");
|
||||
$wr_no = sql_insert_id();
|
||||
|
||||
$SMS = null;
|
||||
if (class_exists('SMS5')) {
|
||||
$SMS = new SMS5;
|
||||
$SMS->SMS_con($sms_config['cf_sms_ip'], $sms_config['cf_sms_id'], $sms_config['cf_sms_pw'], $sms_config['cf_sms_port']);
|
||||
} else {
|
||||
$this->write_debug_log("[SMS 발송 오류] SMS5 클래스를 찾을 수 없습니다. 실제 발송을 건너뜁니다.");
|
||||
}
|
||||
|
||||
foreach ($members as $member) {
|
||||
if ($member['mb_sms'] && !empty($member['mb_hp'])) {
|
||||
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
|
||||
$personal_message = $template['content'];
|
||||
foreach ($personal_vars as $key => $value) {
|
||||
$personal_message = str_replace('{' . $key . '}', $value, $personal_message);
|
||||
}
|
||||
|
||||
$result_code = 'Fail';
|
||||
$result_msg = 'SMS5 클래스가 없어 발송할 수 없습니다.';
|
||||
$hs_status = '0';
|
||||
|
||||
if ($SMS) {
|
||||
$SMS->Add($member['mb_hp'], $send_phone, '', $personal_message);
|
||||
$SMS->Send();
|
||||
$result_arr = $SMS->Result;
|
||||
$result_code = 'Fail';
|
||||
$result_msg = '서버로부터 응답이 없습니다.';
|
||||
|
||||
if(!empty($result_arr)){
|
||||
$result_parts = explode(':', $result_arr[0]);
|
||||
if(count($result_parts) > 1 && strpos($result_parts[1], 'Error') === false) {
|
||||
$result_code = 'Success';
|
||||
$result_msg = $result_parts[1];
|
||||
} else {
|
||||
$result_msg = $result_arr[0];
|
||||
}
|
||||
}
|
||||
$hs_status = ($result_code == 'Success') ? '1' : '0';
|
||||
$SMS->Init();
|
||||
}
|
||||
|
||||
sql_query("INSERT INTO {$this->g5['sms5_history_table']} (wr_no, mb_id, hs_name, hs_hp, hs_datetime, hs_status, hs_message) VALUES ('{$wr_no}', '{$member['mb_id']}', '{$member['mb_name']}', '{$member['mb_hp']}', '" . G5_TIME_YMDHIS . "', '{$hs_status}', '{$result_msg}')");
|
||||
|
||||
if ($hs_status == '1') $success++;
|
||||
else $fail++;
|
||||
} else {
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['success' => $success, 'fail' => $fail];
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [핵심 수정] 이메일 발송 (MailSender 클래스 활용)
|
||||
*/
|
||||
private function sendEmail($members, $template_key, $common_vars)
|
||||
{
|
||||
$success = 0;
|
||||
$fail = 0;
|
||||
|
||||
$notification_mode = get_order_config('notification_mode', 'log');
|
||||
$is_test_mode = ($notification_mode !== 'send');
|
||||
$sizeof_members = count($members);
|
||||
|
||||
// 💡 [수정] 템플릿 조회 로직 변경: 지정 테이블(consultant_mail_templates) -> 기본 테이블(mail_templates) 순서로 조회
|
||||
$template = null;
|
||||
|
||||
// 1. 지정 테이블 조회
|
||||
$check_table = sql_query("SHOW TABLES LIKE 'consultant_mail_templates'", false);
|
||||
if (sql_num_rows($check_table) > 0) {
|
||||
$template = sql_fetch("SELECT * FROM `consultant_mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
|
||||
if ($template) {
|
||||
// 필드명 통일
|
||||
$template['subject'] = $template['template_subject'];
|
||||
$template['content'] = $template['template_content'];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 기본 테이블 조회 (지정 테이블에 없을 경우)
|
||||
if (!$template) {
|
||||
$template = sql_fetch("SELECT * FROM `mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
|
||||
}
|
||||
|
||||
// 3. 둘 다 없으면 에러 처리
|
||||
if (!$template) {
|
||||
$this->write_debug_log("[EMAIL 발송 오류] 템플릿 '{$template_key}'을(를) 찾을 수 없습니다.");
|
||||
return ['success' => 0, 'fail' => count($members)];
|
||||
}
|
||||
|
||||
if ($is_test_mode) {
|
||||
// --- 개발 모드: 로그 파일에만 기록 ---
|
||||
foreach ($members as $member) {
|
||||
if ($member['mb_mailling'] && !empty($member['mb_email'])) {
|
||||
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
|
||||
$subject = $template['subject'];
|
||||
$content = $template['content'];
|
||||
foreach ($personal_vars as $key => $value) {
|
||||
$search = '{' . $key . '}';
|
||||
$subject = str_replace($search, $value, $subject);
|
||||
$content = str_replace($search, $value, $content);
|
||||
}
|
||||
$this->write_debug_log("[EMAIL LOG] To: {$member['mb_email']}, Subject: {$subject}, Content: {$content}");
|
||||
$success++;
|
||||
} else {
|
||||
$fail++;
|
||||
$empty = !empty($member['mb_email']);
|
||||
$this->write_debug_log("[EMAIL 발송 오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// --- 실제 발송 모드: MailSender 호출 ---
|
||||
if (!class_exists('MailSender')) {
|
||||
$this->write_debug_log("[EMAIL 발송 오류] MailSender 클래스를 찾을 수 없습니다.");
|
||||
return ['success' => 0, 'fail' => count($members)];
|
||||
}
|
||||
$mailSender = new MailSender();
|
||||
|
||||
foreach ($members as $member) {
|
||||
if ($member['mb_mailling'] && !empty($member['mb_email'])) {
|
||||
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
|
||||
|
||||
// 💡 [수정] MailSender가 템플릿 키로 조회하는 방식일 수 있으므로,
|
||||
// 커스텀 템플릿 내용을 직접 전달하거나 MailSender를 수정해야 할 수 있음.
|
||||
// 여기서는 MailSender가 템플릿 키를 받아서 내부적으로 처리한다고 가정하고,
|
||||
// 만약 커스텀 템플릿을 사용해야 한다면 MailSender의 동작 방식에 따라 수정이 필요함.
|
||||
// 현재 구조상 MailSender::send()는 템플릿 키를 받으므로,
|
||||
// MailSender 내부에서도 동일한 우선순위 로직이 필요하거나,
|
||||
// 여기서 내용을 다 만들어서 보내는 방식(sendDirect 등)이 있다면 그걸 써야 함.
|
||||
// 일단 기존 로직 유지하되, MailSender가 커스텀 테이블을 인지하지 못할 수 있음을 주석으로 남김.
|
||||
|
||||
// 만약 MailSender가 내용을 직접 받는 메소드가 없다면,
|
||||
// 여기서 내용을 치환해서 보내는 로직을 직접 구현해야 할 수도 있음.
|
||||
// 하지만 요청사항은 "지정 테이블을 읽고 없으면 기본 테이블을 읽어서 발송"이므로,
|
||||
// 위에서 $template을 구했으니, 내용을 직접 치환해서 메일 발송 함수(mailer)를 직접 호출하는 것이 안전함.
|
||||
|
||||
$subject = $template['subject'];
|
||||
$content = $template['content'];
|
||||
foreach ($personal_vars as $key => $value) {
|
||||
$search = '{' . $key . '}';
|
||||
$subject = str_replace($search, $value, $subject);
|
||||
$content = str_replace($search, $value, $content);
|
||||
}
|
||||
|
||||
// G5 기본 mailer 함수 사용 (MailSender 의존성 제거 또는 우회)
|
||||
include_once(G5_LIB_PATH.'/mailer.lib.php');
|
||||
mailer($this->g5['title'], $this->g5['admin_email'], $member['mb_email'], $subject, $content, 1);
|
||||
|
||||
$success++;
|
||||
} else {
|
||||
$fail++;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['success' => $success, 'fail' => $fail];
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 메시지 생성
|
||||
*/
|
||||
private function generateResultMessage($results)
|
||||
{
|
||||
$message = "발송이 완료되었습니다.\n\n";
|
||||
$message .= "전체 대상: " . $results['total_targets'] . "명\n\n";
|
||||
if (isset($results['sms_success'])) {
|
||||
$message .= "SMS 발송 결과: 성공 " . $results['sms_success'] . "건, 실패 " . $results['sms_fail'] . "건\n";
|
||||
}
|
||||
if (isset($results['email_success'])) {
|
||||
$message .= "이메일 발송 결과: 성공 " . $results['email_success'] . "건, 실패 " . $results['email_fail'] . "건\n";
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* ❗ [핵심 수정] 디버그 로그 기록 함수 (권한 문제 해결)
|
||||
*/
|
||||
private function write_debug_log($message)
|
||||
{
|
||||
$log_dir = G5_PATH . '/log';
|
||||
|
||||
// 1. 디렉토리 존재 여부 확인 및 생성
|
||||
if (!is_dir($log_dir)) {
|
||||
if (!@mkdir($log_dir, 0755, true) && !is_dir($log_dir)) {
|
||||
error_log("--- NotificationSender ERROR: 디버그 로그 디렉토리 생성 실패. '{$log_dir}' 경로를 확인하거나 수동으로 생성 후 웹서버 쓰기 권한을 부여해주세요.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 디렉토리 쓰기 권한 확인
|
||||
if (!is_writable($log_dir)) {
|
||||
error_log("--- NotificationSender ERROR: 디버그 로그 쓰기 오류. '{$log_dir}' 디렉토리에 쓰기 권한이 없습니다. 웹서버의 폴더 권한을 확인해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 로그 파일에 내용 기록
|
||||
$log_file = $log_dir . '/notification_debug.log';
|
||||
$log_message = date("[Y-m-d H:i:s]") . " " . $message . "\n";
|
||||
|
||||
if (file_put_contents($log_file, $log_message, FILE_APPEND | LOCK_EX) === false) {
|
||||
error_log("--- NotificationSender ERROR: 디버그 로그 파일 쓰기 실패. '{$log_file}' 파일에 내용을 쓸 수 없습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 이 파일은 상담 예약 관련 팝업들을 한번에 쉽게 포함하기 위해 사용됩니다.
|
||||
|
||||
// 현재 파일의 경로를 기준으로 팝업 파일들의 경로를 정의합니다.
|
||||
$consultant_components_path = dirname(__FILE__);
|
||||
|
||||
// 상담 예약 신청 팝업 포함
|
||||
include_once($consultant_components_path . '/reservation_popup.php');
|
||||
|
||||
// 예약 확인/취소 팝업 포함
|
||||
include_once($consultant_components_path . '/reservation_check.php');
|
||||
?>
|
||||
@@ -0,0 +1,401 @@
|
||||
<?php
|
||||
if (isset($_POST['action'])) {
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
// 예약 조회
|
||||
if ($action === 'find_reservations') {
|
||||
$customer_name = trim($_POST['customer_name'] ?? '');
|
||||
$customer_phone = trim($_POST['customer_phone'] ?? '');
|
||||
|
||||
if (!$customer_name || !$customer_phone) {
|
||||
echo json_encode(['success' => false, 'message' => '이름과 연락처를 모두 입력해주세요.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 💡 [수정] 리소스 정보를 함께 조회하기 위해 LEFT JOIN 추가
|
||||
$sql = " SELECT r.*, res.name as resource_name
|
||||
FROM consultant_reservations r
|
||||
LEFT JOIN consultant_resources res ON r.resource_id = res.id
|
||||
WHERE r.customer_name = '" . sql_real_escape_string($customer_name) . "'
|
||||
AND r.customer_phone = '" . sql_real_escape_string($customer_phone) . "'
|
||||
AND r.is_deleted = 0
|
||||
ORDER BY r.reservation_date DESC, r.reservation_time DESC ";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
$cancel_deadline_hours = (int)consultant_get_config('cancel_deadline_hours', 24);
|
||||
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$reservation_timestamp = strtotime($row['reservation_date'] . ' ' . $row['reservation_time']);
|
||||
$cancellable_until = $reservation_timestamp - ($cancel_deadline_hours * 3600);
|
||||
|
||||
$row['is_cancellable'] = (time() < $cancellable_until && in_array($row['status'], ['payment_pending', 'reserved']));
|
||||
$reservations[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $reservations]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 예약 취소
|
||||
if ($action === 'cancel_reservation') {
|
||||
$reservation_id = (int)($_POST['reservation_id'] ?? 0);
|
||||
$customer_name = trim($_POST['customer_name'] ?? '');
|
||||
$customer_phone = trim($_POST['customer_phone'] ?? '');
|
||||
|
||||
if (!$reservation_id || !$customer_name || !$customer_phone) {
|
||||
echo json_encode(['success' => false, 'message' => '필수 정보가 누락되었습니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = " SELECT * FROM consultant_reservations
|
||||
WHERE id = '{$reservation_id}'
|
||||
AND customer_name = '" . sql_real_escape_string($customer_name) . "'
|
||||
AND customer_phone = '" . sql_real_escape_string($customer_phone) . "'
|
||||
AND is_deleted = 0 ";
|
||||
$reservation = sql_fetch($sql);
|
||||
|
||||
if (!$reservation) {
|
||||
echo json_encode(['success' => false, 'message' => '예약 정보를 찾을 수 없거나, 입력하신 정보와 일치하지 않습니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cancel_deadline_hours = (int)consultant_get_config('cancel_deadline_hours', 24);
|
||||
$reservation_timestamp = strtotime($reservation['reservation_date'] . ' ' . $reservation['reservation_time']);
|
||||
$cancellable_until = $reservation_timestamp - ($cancel_deadline_hours * 3600);
|
||||
|
||||
if (time() >= $cancellable_until) {
|
||||
echo json_encode(['success' => false, 'message' => '예약 취소 가능 시간이 지났습니다. 관리자에게 문의해주세요.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!in_array($reservation['status'], ['payment_pending', 'reserved'])) {
|
||||
echo json_encode(['success' => false, 'message' => '이미 처리되었거나 취소된 예약입니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql_update = " UPDATE consultant_reservations
|
||||
SET status = 'cancelled', updated_at = NOW()
|
||||
WHERE id = '{$reservation_id}' ";
|
||||
|
||||
if (sql_query($sql_update)) {
|
||||
consultant_log("고객 예약 취소: ID {$reservation_id} (고객: {$customer_name})");
|
||||
echo json_encode(['success' => true, 'message' => '예약이 성공적으로 취소되었습니다.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => '예약 취소 중 오류가 발생했습니다. 다시 시도해주세요.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
|
||||
// 💡 [추가] 상담 유형 한글 이름을 JavaScript에서 사용하기 위해 배열을 정의합니다.
|
||||
$consultant_types_map = json_decode(consultant_get_config('consultation_types', '{"onsite":"현장 상담"}'), true);
|
||||
if (!is_array($consultant_types_map)) {
|
||||
$consultant_types_map = ['onsite' => '현장 상담']; // 파싱 실패 시 기본값
|
||||
}
|
||||
|
||||
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_check.php';
|
||||
?>
|
||||
|
||||
<!-- 예약 확인/취소 팝업 -->
|
||||
<div id="reservation-check-popup" class="reservation-modal-overlay">
|
||||
<div class="reservation-modal-content">
|
||||
<div class="reservation-modal-header">
|
||||
<h2>예약 확인 및 취소</h2>
|
||||
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">×</button>
|
||||
</div>
|
||||
<div class="reservation-modal-body">
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [개선] 조회 폼과 결과 화면을 분리합니다. -->
|
||||
<div id="reservation-check-view-form">
|
||||
<div class="check-form-wrap">
|
||||
<p>예약 시 입력하신 이름과 연락처로 예약 내역을 조회할 수 있습니다.</p>
|
||||
<form id="reservation-check-form">
|
||||
<div class="form-group">
|
||||
<label for="check_customer_name">예약자명</label>
|
||||
<input type="text" id="check_customer_name" name="customer_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="check_customer_phone">연락처</label>
|
||||
<input type="tel" id="check_customer_phone" name="customer_phone" class="form-control" placeholder="010-1234-5678" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">예약 조회</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reservation-check-view-results" style="display: none;">
|
||||
<div class="results-header">
|
||||
<h3>조회된 예약 내역</h3>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="back-to-search-btn">새로 조회</button>
|
||||
</div>
|
||||
<div id="reservation-results">
|
||||
<!-- 검색 결과가 여기에 표시됩니다. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 💡 [개선] 예약 신청 팝업과 스타일을 통일합니다. */
|
||||
.check-form-wrap { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; font-weight: 500; color: #495057; margin-bottom: 8px; }
|
||||
.form-control { width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 16px; box-sizing: border-box; } /* iOS 줌 방지 */
|
||||
.form-control:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); }
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s ease;
|
||||
/* 💡 [수정] 버튼 내 텍스트가 잘려보이는 현상을 해결합니다. */
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-primary:hover { background: #0056b3; }
|
||||
#reservation-results { margin-top: 20px; }
|
||||
.reservation-item { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
||||
.reservation-item-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 10px; }
|
||||
.reservation-status { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
|
||||
.status-reserved { background: #d4edda; color: #155724; }
|
||||
/* 💡 [개선] 예약 상세 정보 스타일 */
|
||||
.reservation-details-grid {
|
||||
margin: 15px 0;
|
||||
font-size: 15px; /* 글씨 크기 키움 */
|
||||
line-height: 1.6; /* 줄 간격 확보 */
|
||||
}
|
||||
.detail-item { display: flex; margin-bottom: 10px; } /* 간격 조정 */
|
||||
.detail-item .label { font-weight: 600; color: #555; width: 90px; flex-shrink: 0; } /* 너비 조정 */
|
||||
.detail-item .value { color: #333; }
|
||||
/* 💡 [추가] 예약 취소 버튼/안내문 영역 스타일 */
|
||||
.reservation-actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.reservation-actions .btn-danger {
|
||||
width: auto; /* 버튼이 전체 너비를 차지하지 않도록 */
|
||||
display: inline-block;
|
||||
}
|
||||
.cancel-notice { font-size: 13px !important; color: #666 !important; margin-top: 15px; text-align: right; padding: 10px; background-color: #fff; border-radius: 4px;}
|
||||
/* 💡 [개선] 예약 조회 결과 헤더 스타일 */
|
||||
.results-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #eee; }
|
||||
.results-header h3 { margin: 0; font-size: 18px; flex-grow: 1; } /* 제목이 남는 공간을 모두 차지하도록 설정 */
|
||||
.results-header .btn { width: auto; flex-shrink: 0; } /* 헤더 안의 버튼은 내용만큼만 너비를 갖도록 설정 */
|
||||
.btn-sm { padding: 5px 10px; font-size: 12px; }
|
||||
.status-payment_pending { background: #fff3cd; color: #856404; }
|
||||
.status-cancelled { background: #f8d7da; color: #721c24; }
|
||||
.status-completed { background: #cce5ff; color: #004085; }
|
||||
.btn-danger { background: #dc3545; color: white; }
|
||||
.no-results { text-align: center; color: #666; padding: 40px 0; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const ReservationCheckPopup = {
|
||||
elements: {},
|
||||
// 💡 [추가] PHP에서 전달받은 상담 유형 맵
|
||||
consultantTypes: <?php echo json_encode($consultant_types_map, JSON_UNESCAPED_UNICODE); ?>,
|
||||
state: {
|
||||
name: '',
|
||||
phone: ''
|
||||
},
|
||||
config: {
|
||||
ajaxUrl: '<?php echo $ajax_url; ?>'
|
||||
},
|
||||
|
||||
init() {
|
||||
this.elements.popup = document.getElementById('reservation-check-popup');
|
||||
if (!this.elements.popup) return;
|
||||
|
||||
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
|
||||
this.elements.form = this.elements.popup.querySelector('#reservation-check-form');
|
||||
this.elements.resultsContainer = this.elements.popup.querySelector('#reservation-results');
|
||||
|
||||
// 💡 [추가] 화면 전환용 요소
|
||||
this.elements.formView = this.elements.popup.querySelector('#reservation-check-view-form');
|
||||
this.elements.resultsView = this.elements.popup.querySelector('#reservation-check-view-results');
|
||||
this.elements.backBtn = this.elements.popup.querySelector('#back-to-search-btn');
|
||||
|
||||
this.addEventListeners();
|
||||
},
|
||||
|
||||
addEventListeners() {
|
||||
const closeBtn = this.elements.popup.querySelector('.reservation-modal-close');
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
|
||||
|
||||
this.elements.popup.addEventListener('click', e => {
|
||||
if (e.target === this.elements.popup) this.close();
|
||||
});
|
||||
|
||||
if (this.elements.form) {
|
||||
this.elements.form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
// 💡 [개선] 조회 버튼을 누를 때만 state를 업데이트하고, 그 state를 기반으로 조회합니다.
|
||||
this.state.name = this.elements.form.querySelector('#check_customer_name').value;
|
||||
this.state.phone = this.elements.form.querySelector('#check_customer_phone').value;
|
||||
this.findReservations(); // state에 저장된 정보로 조회
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.backBtn) {
|
||||
this.elements.backBtn.addEventListener('click', () => {
|
||||
this.elements.resultsView.style.display = 'none';
|
||||
this.elements.formView.style.display = 'block';
|
||||
this.elements.resultsContainer.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elements.resultsContainer) {
|
||||
this.elements.resultsContainer.addEventListener('click', e => {
|
||||
if (e.target.classList.contains('btn-cancel-reservation')) {
|
||||
const reservationId = e.target.dataset.id;
|
||||
if (confirm('정말 이 예약을 취소하시겠습니까?')) {
|
||||
this.cancelReservation(reservationId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
open() {
|
||||
if (!this.elements.popup) return;
|
||||
this.elements.popup.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
close() {
|
||||
if (!this.elements.popup) return;
|
||||
this.elements.popup.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
if (this.elements.form) this.elements.form.reset();
|
||||
|
||||
// 💡 [추가] 팝업을 닫을 때 화면 상태를 초기화합니다.
|
||||
if (this.elements.resultsContainer) this.elements.resultsContainer.innerHTML = '';
|
||||
if (this.elements.resultsView) this.elements.resultsView.style.display = 'none';
|
||||
if (this.elements.formView) this.elements.formView.style.display = 'block';
|
||||
},
|
||||
|
||||
async findReservations() {
|
||||
this.showLoading();
|
||||
|
||||
// 💡 [개선] state에 저장된 정보로 FormData를 생성하여 일관성을 유지합니다.
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'find_reservations');
|
||||
formData.append('customer_name', this.state.name);
|
||||
formData.append('customer_phone', this.state.phone);
|
||||
|
||||
try {
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// 💡 [추가] 조회 성공 시 화면을 전환합니다.
|
||||
this.elements.formView.style.display = 'none';
|
||||
this.elements.resultsView.style.display = 'block';
|
||||
this.renderResults(result.data);
|
||||
} else {
|
||||
this.elements.resultsContainer.innerHTML = `<p class="no-results">${result.message}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("조회 오류:", error);
|
||||
this.elements.resultsContainer.innerHTML = '<p class="no-results">조회 중 오류가 발생했습니다.</p>';
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
renderResults(reservations) {
|
||||
if (reservations.length === 0) {
|
||||
this.elements.resultsContainer.innerHTML = '<p class="no-results">조회된 예약 내역이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 💡 [수정] h3 태그는 정적 HTML로 이동했으므로 제거합니다.
|
||||
let html = '';
|
||||
const statusLabels = {
|
||||
'payment_pending': '입금대기', 'reserved': '예약확정',
|
||||
'completed': '상담완료', 'cancelled': '예약취소'
|
||||
};
|
||||
|
||||
reservations.forEach(res => {
|
||||
const date = new Date(res.reservation_date + ' ' + res.reservation_time);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit' };
|
||||
const formattedDate = date.toLocaleDateString('ko-KR', options);
|
||||
|
||||
// 💡 [개선] 상담 유형의 한글 이름을 맵에서 찾아옵니다.
|
||||
const consultationTypeName = this.consultantTypes[res.consultation_type] || res.consultation_type;
|
||||
|
||||
// 💡 [추가] 리소스 이름 표시
|
||||
const resourceName = res.resource_name ? res.resource_name : '미지정 (빠른 배정)';
|
||||
|
||||
html += `
|
||||
<div class="reservation-item" id="reservation-${res.id}">
|
||||
<div class="reservation-item-header">
|
||||
<strong>예약 번호: #${res.id}</strong>
|
||||
<span class="reservation-status status-${res.status}">${statusLabels[res.status] || res.status}</span>
|
||||
</div>
|
||||
<div class="reservation-details-grid">
|
||||
<div class="detail-item"><span class="label">상담 일시:</span><span class="value">${formattedDate}</span></div>
|
||||
<div class="detail-item"><span class="label">상담 유형:</span><span class="value">${consultationTypeName}</span></div>
|
||||
<div class="detail-item"><span class="label">담당/공간:</span><span class="value">${resourceName}</span></div>
|
||||
</div>
|
||||
<div class="reservation-actions">`;
|
||||
|
||||
if (res.is_cancellable) {
|
||||
html += `<button type="button" class="btn btn-danger btn-cancel-reservation" data-id="${res.id}">예약 취소</button>`;
|
||||
} else if (res.status !== 'cancelled' && res.status !== 'completed') {
|
||||
html += `<p class="cancel-notice">예약 취소 가능 시간이 지났습니다. 변경/취소는 관리자에게 문의해주세요.</p>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
});
|
||||
this.elements.resultsContainer.innerHTML = html;
|
||||
},
|
||||
|
||||
async cancelReservation(reservationId) {
|
||||
this.showLoading();
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'cancel_reservation');
|
||||
formData.append('reservation_id', reservationId);
|
||||
formData.append('customer_name', this.state.name);
|
||||
formData.append('customer_phone', this.state.phone);
|
||||
|
||||
try {
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
if (result.success) {
|
||||
await this.findReservations(); // 💡 [수정] await를 추가하여 목록 새로고침이 완료될 때까지 기다립니다.
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("취소 오류:", error);
|
||||
alert('예약 취소 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
// 💡 [제거] findReservations()가 자체적으로 로딩을 숨기므로 중복 호출을 제거합니다.
|
||||
}
|
||||
},
|
||||
|
||||
showLoading() { if (this.elements.loading) this.elements.loading.style.display = 'flex'; },
|
||||
hideLoading() { if (this.elements.loading) this.elements.loading.style.display = 'none'; },
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => ReservationCheckPopup.init());
|
||||
|
||||
function openReservationCheckPopup() {
|
||||
ReservationCheckPopup.open();
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,868 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 팝업 UI 및 데이터 처리
|
||||
*/
|
||||
|
||||
// AJAX 요청 처리
|
||||
if (isset($_POST['action'])) {
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
|
||||
|
||||
// 월별 예약 가능일 조회
|
||||
if ($action === 'get_month_availability') {
|
||||
$year = (int) ($_POST['year'] ?? 0);
|
||||
$month = (int) ($_POST['month'] ?? 0);
|
||||
|
||||
if ($year && $month) {
|
||||
$start_date = date('Y-m-d', mktime(0, 0, 0, $month, 1, $year));
|
||||
$end_date = date('Y-m-t', strtotime($start_date));
|
||||
|
||||
$max_advance_days = consultant_get_config('max_advance_days', 30);
|
||||
$max_date = date('Y-m-d', strtotime("+" . $max_advance_days . " days"));
|
||||
|
||||
$sql = "SELECT
|
||||
specific_date,
|
||||
SUM(CASE WHEN is_available = 1 AND max_persons > (SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') THEN 1 ELSE 0 END) as available_slots,
|
||||
MAX(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as is_holiday
|
||||
FROM consultant_schedule s
|
||||
WHERE specific_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
GROUP BY specific_date";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$availability = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$is_bookable = true;
|
||||
$reason = '';
|
||||
|
||||
if ($row['is_holiday']) {
|
||||
$is_bookable = false;
|
||||
$reason = 'holiday';
|
||||
} elseif ($row['specific_date'] < date('Y-m-d')) {
|
||||
$is_bookable = false;
|
||||
$reason = 'past_date';
|
||||
} elseif ($row['specific_date'] > $max_date) {
|
||||
$is_bookable = false;
|
||||
$reason = 'too_far';
|
||||
} elseif ($row['available_slots'] == 0) {
|
||||
$is_bookable = false;
|
||||
$reason = 'full';
|
||||
}
|
||||
|
||||
$availability[date('j', strtotime($row['specific_date']))] = [
|
||||
'available' => $is_bookable,
|
||||
'reason' => $reason
|
||||
];
|
||||
}
|
||||
$response = ['success' => true, 'data' => $availability];
|
||||
}
|
||||
}
|
||||
|
||||
// 특정일의 예약 가능 시간 조회
|
||||
if ($action === 'get_time_slots') {
|
||||
$date = preg_replace('/[^0-9\-]/', '', $_POST['date'] ?? '');
|
||||
|
||||
if ($date) {
|
||||
$min_advance_hours = consultant_get_config('min_advance_hours', 24);
|
||||
$min_datetime = date('Y-m-d H:i:s', strtotime("+" . $min_advance_hours . " hours"));
|
||||
|
||||
$sql = "SELECT
|
||||
s.start_time,
|
||||
s.max_persons,
|
||||
(SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') as reserved_count
|
||||
FROM consultant_schedule s
|
||||
WHERE s.specific_date = '{$date}' AND s.is_available = 1
|
||||
ORDER BY s.start_time";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$slots = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$slot_datetime = $date . ' ' . $row['start_time'];
|
||||
$is_too_soon = ($slot_datetime < $min_datetime);
|
||||
|
||||
$available_count = $row['max_persons'] - $row['reserved_count'];
|
||||
$is_full = ($available_count <= 0);
|
||||
|
||||
$slots[] = [
|
||||
'time' => substr($row['start_time'], 0, 5),
|
||||
'available' => !$is_too_soon && !$is_full,
|
||||
'reason' => $is_too_soon ? 'too_soon' : ($is_full ? 'full' : ''),
|
||||
'reserved_count' => (int)$row['reserved_count'],
|
||||
'max_persons' => (int)$row['max_persons']
|
||||
];
|
||||
}
|
||||
$response = ['success' => true, 'data' => $slots];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 💡 [수정] 아래는 팝업의 HTML/CSS/JS 부분입니다.
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
$consultant_installed = is_consultant_installed();
|
||||
|
||||
if($consultant_installed) { // 💡 [추가] 시스템이 설치된 경우에만 팝업을 렌더링합니다.
|
||||
// 현재 날짜 정보
|
||||
$current_year = date('Y');
|
||||
$current_month_num = date('n');
|
||||
|
||||
// 💡 [개선] 설정 값을 DB에서 직접 가져와 일관성을 유지합니다.
|
||||
$consultation_fee = consultant_get_config('consultation_fee', 50000);
|
||||
$account_info = consultant_get_config('account_info', '');
|
||||
$max_advance_days = consultant_get_config('max_advance_days', 30);
|
||||
|
||||
// 💡 [수정] 스킨 URL 및 AJAX 엔드포인트 설정
|
||||
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_popup.php';
|
||||
$form_action_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_submit.php';
|
||||
|
||||
// 💡 [추가] 리소스 목록 조회
|
||||
$resources = [];
|
||||
$res_result = sql_query("SELECT id, name FROM consultant_resources WHERE is_active = 1 ORDER BY group_id, name");
|
||||
while ($row = sql_fetch_array($res_result)) {
|
||||
$resources[] = $row;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- 예약 팝업 오버레이 -->
|
||||
<div id="reservation-popup" class="reservation-modal-overlay">
|
||||
<div class="reservation-modal-content">
|
||||
<!-- 팝업 헤더 -->
|
||||
<div class="reservation-modal-header">
|
||||
<h2>상담 예약 신청</h2>
|
||||
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 팝업 본문 -->
|
||||
<div class="reservation-modal-body">
|
||||
<!-- 💡 [개선] 로딩 스피너 추가 -->
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- 단계 표시 -->
|
||||
<div class="reservation-steps">
|
||||
<div class="step active" data-step="1">
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-text">날짜 선택</span>
|
||||
</div>
|
||||
<div class="step" data-step="2">
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-text">시간 선택</span>
|
||||
</div>
|
||||
<div class="step" data-step="3">
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-text">정보 입력</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 예약 폼 -->
|
||||
<form id="reservation-form" method="post"
|
||||
action="<?php echo $form_action_url; ?>">
|
||||
<!-- 1단계: 달력 -->
|
||||
<div class="reservation-step-content" data-step="1">
|
||||
<div class="step-description">
|
||||
<h4>📅 상담 날짜를 선택해주세요</h4>
|
||||
<p>최대 <?php echo $max_advance_days; ?>일 후까지 예약 가능합니다.</p>
|
||||
</div>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<button type="button" class="calendar-nav prev-month" aria-label="이전 달">
|
||||
<span>‹</span>
|
||||
</button>
|
||||
<h3 class="calendar-title">
|
||||
<span id="calendar-year"><?php echo $current_year; ?></span>년
|
||||
<span id="calendar-month"><?php echo $current_month_num; ?></span>월
|
||||
</h3>
|
||||
<button type="button" class="calendar-nav next-month" aria-label="다음 달">
|
||||
<span>›</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-weekdays">
|
||||
<div class="weekday">일</div>
|
||||
<div class="weekday">월</div>
|
||||
<div class="weekday">화</div>
|
||||
<div class="weekday">수</div>
|
||||
<div class="weekday">목</div>
|
||||
<div class="weekday">금</div>
|
||||
<div class="weekday">토</div>
|
||||
</div>
|
||||
<div class="calendar-days" id="calendar-days">
|
||||
<!-- 달력 날짜들이 JavaScript로 동적 생성됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<div class="legend-item"><span class="legend-color available"></span><span>예약 가능</span></div>
|
||||
<!-- 💡 [추가] 휴일/마감 상태를 구분하는 범례 추가 -->
|
||||
<div class="legend-item"><span class="legend-color holiday"></span><span>휴일</span></div>
|
||||
<div class="legend-item"><span class="legend-color full"></span><span>예약 마감</span></div>
|
||||
<div class="legend-item"><span class="legend-color unavailable"></span><span>지난날짜</span></div>
|
||||
<div class="legend-item"><span class="legend-color selected"></span><span>선택</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2단계: 시간 선택 -->
|
||||
<div class="reservation-step-content" data-step="2" style="display: none;">
|
||||
<div class="step-description">
|
||||
<h4>🕐 상담 시간을 선택해주세요</h4>
|
||||
<div class="selected-date-info"><strong>선택된 날짜: <span id="selected-date-display"></span></strong></div>
|
||||
</div>
|
||||
<div class="time-slots-container">
|
||||
<div class="time-slots-grid" id="time-slots-grid">
|
||||
<!-- 시간대들이 JavaScript로 동적 생성됩니다 -->
|
||||
</div>
|
||||
<div class="time-legend">
|
||||
<div class="legend-item"><span class="legend-color time-available"></span><span>예약 가능</span></div>
|
||||
<div class="legend-item"><span class="legend-color time-full"></span><span>예약 마감</span></div>
|
||||
<div class="legend-item"><span class="legend-color time-too-soon"></span><span>예약 임박</span></div>
|
||||
<div class="legend-item"><span class="legend-color time-selected"></span><span>선택</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3단계: 고객 정보 입력 -->
|
||||
<div class="reservation-step-content" data-step="3" style="display: none;">
|
||||
<div class="step-description">
|
||||
<h4>📝 고객 정보를 입력해주세요</h4>
|
||||
</div>
|
||||
|
||||
<div class="reservation-summary">
|
||||
<h5>📋 예약 정보 확인</h5>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-item">
|
||||
<span class="label">📅 예약 날짜:</span>
|
||||
<span id="summary-date">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">🕐 예약 시간:</span>
|
||||
<span id="summary-time">-</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">💰 상담 비용:</span>
|
||||
<span><?php echo number_format($consultation_fee); ?>원</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="customer-info-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="customer-name">👤 이름 <span class="required">*</span></label>
|
||||
<input type="text" id="customer-name" name="customer_name" required placeholder="홍길동">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer-phone">📱 연락처 <span class="required">*</span></label>
|
||||
<input type="tel" id="customer-phone" name="customer_phone" required placeholder="010-1234-5678">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer-email">📧 이메일 <span class="required">*</span></label>
|
||||
<input type="email" id="customer-email" name="customer_email" required placeholder="example@email.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="consultation-type">🏠 상담 유형</label>
|
||||
<select id="consultation-type" name="consultation_type">
|
||||
<option value="onsite">현장 상담</option>
|
||||
<option value="online">온라인 상담</option>
|
||||
<option value="phone">전화 상담</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 💡 [추가] 담당자/공간 선택 드롭다운 -->
|
||||
<div class="form-group">
|
||||
<label for="consultation-resource">👨⚕️ 담당자/공간 선택</label>
|
||||
<select id="consultation-resource" name="resource_id">
|
||||
<option value="">선택 안 함 (빠른 배정)</option>
|
||||
<?php foreach ($resources as $res): ?>
|
||||
<option value="<?php echo $res['id']; ?>"><?php echo htmlspecialchars($res['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="customer-request">📝 요청사항</label>
|
||||
<textarea id="customer-request" name="customer_request" rows="4" placeholder="상담 관련 요청사항이나 문의사항을 입력해주세요."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 숨겨진 필드들 -->
|
||||
<input type="hidden" id="selected-date" name="reservation_date">
|
||||
<input type="hidden" id="selected-time" name="reservation_time">
|
||||
<input type="hidden" name="payment_amount" value="<?php echo $consultation_fee; ?>">
|
||||
<input type="hidden" name="status" value="payment_pending">
|
||||
</div>
|
||||
|
||||
<!-- 네비게이션 버튼 -->
|
||||
<div class="reservation-nav-buttons">
|
||||
<button type="button" class="btn-prev" style="display: none;">← 이전</button>
|
||||
<button type="button" class="btn-next">다음 →</button>
|
||||
<button type="submit" class="btn-submit" style="display: none;">예약 신청</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 예약 팝업 스타일 -->
|
||||
<style>
|
||||
/* 💡 [추가] 로딩 오버레이 스타일 */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.loading-spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
|
||||
/* 팝업 오버레이 */
|
||||
.reservation-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.reservation-modal-overlay.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.reservation-modal-content {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.reservation-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.reservation-modal-header h2 { margin: 0; font-size: 20px; font-weight: 600; color: #333; }
|
||||
.reservation-modal-close {
|
||||
background: none; border: none; font-size: 24px; cursor: pointer; color: #666;
|
||||
padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 50%; transition: all 0.2s ease;
|
||||
}
|
||||
.reservation-modal-close:hover { background: #e9ecef; color: #333; }
|
||||
.reservation-modal-body { padding: 25px; }
|
||||
|
||||
/* 단계 표시 */
|
||||
.reservation-steps { display: flex; justify-content: center; margin-bottom: 30px; position: relative; }
|
||||
.reservation-steps::before {
|
||||
content: ''; position: absolute; top: 15px; left: 25%; right: 25%;
|
||||
height: 2px; background: #e9ecef; z-index: 1;
|
||||
}
|
||||
.step { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 2; background: #fff; padding: 0 15px; }
|
||||
.step-number {
|
||||
width: 30px; height: 30px; border-radius: 50%; background: #e9ecef; color: #6c757d;
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 8px; transition: all 0.3s ease;
|
||||
}
|
||||
.step.active .step-number { background: #007bff; color: #fff; }
|
||||
.step.completed .step-number { background: #28a745; color: #fff; }
|
||||
.step-text { font-size: 12px; color: #6c757d; font-weight: 500; }
|
||||
.step.active .step-text { color: #007bff; font-weight: 600; }
|
||||
|
||||
/* 달력 */
|
||||
.calendar-container { max-width: 100%; }
|
||||
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.calendar-nav {
|
||||
background: none; border: 1px solid #ddd; width: 35px; height: 35px; border-radius: 50%;
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease;
|
||||
}
|
||||
.calendar-nav:hover { background: #f8f9fa; border-color: #007bff; }
|
||||
.calendar-title { margin: 0; font-size: 18px; font-weight: 600; color: #333; }
|
||||
.calendar-grid { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }
|
||||
.calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); background: #f8f9fa; }
|
||||
.weekday { padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; font-size: 14px; border-right: 1px solid #e9ecef; }
|
||||
.weekday:last-child { border-right: none; }
|
||||
.calendar-days { display: grid; grid-template-columns: repeat(7, 1fr); }
|
||||
.calendar-day {
|
||||
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
|
||||
border-right: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef; cursor: pointer;
|
||||
transition: all 0.2s ease; font-size: 14px; position: relative;
|
||||
}
|
||||
.calendar-day:nth-child(7n) { border-right: none; }
|
||||
.calendar-day.other-month { color: #ccc; background: #f8f9fa; cursor: default; }
|
||||
.calendar-day.available { background: #fff; color: #333; }
|
||||
.calendar-day.available:hover { background: #e3f2fd; color: #1976d2; }
|
||||
.calendar-day.unavailable { background: #f5f5f5; color: #999; cursor: not-allowed; }
|
||||
.calendar-day.full { background: #fbe9e7; color: #c62828; cursor: not-allowed; } /* 예약 마감 */
|
||||
.calendar-day.holiday { background: #e8eaf6; color: #3f51b5; cursor: not-allowed; } /* 휴일 */
|
||||
.calendar-day.selected { background: #007bff; color: #fff; font-weight: 600; }
|
||||
.calendar-day.today { font-weight: 600; color: #dc3545; }
|
||||
.calendar-loading, .loading, .error, .no-slots { grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: #666; font-style: italic; }
|
||||
.error { color: #dc3545; }
|
||||
|
||||
/* 범례 */
|
||||
.calendar-legend, .time-legend { display: flex; justify-content: center; gap: 20px; margin-top: 15px; flex-wrap: wrap; }
|
||||
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #666; }
|
||||
.legend-color { width: 12px; height: 12px; border-radius: 2px; border: 1px solid #ddd; }
|
||||
.legend-color.available { background: #fff; }
|
||||
.legend-color.unavailable { background: #f5f5f5; }
|
||||
.legend-color.full { background: #fbe9e7; }
|
||||
.legend-color.holiday { background: #e8eaf6; }
|
||||
.legend-color.selected { background: #007bff; }
|
||||
.legend-color.time-available { background: #e8f5e8; border-color: #28a745; }
|
||||
.legend-color.time-full { background: #f8d7da; border-color: #dc3545; }
|
||||
.legend-color.time-too-soon { background: #f1f3f5; border-color: #dee2e6; }
|
||||
.legend-color.time-selected { background: #007bff; border-color: #007bff; }
|
||||
|
||||
/* 시간 선택 */
|
||||
.selected-date-info { margin-bottom: 10px; }
|
||||
.step-description { margin-bottom: 20px; }
|
||||
.step-description h4 { margin: 0 0 10px 0; font-size: 18px; }
|
||||
.step-description p { margin: 0; font-size: 14px; color: #666; }
|
||||
.selected-date-info h4 { margin: 0; color: #333; font-size: 16px; }
|
||||
.time-slots-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px; }
|
||||
.time-slot {
|
||||
padding: 12px 8px; border: 2px solid #e9ecef; border-radius: 6px; text-align: center; cursor: pointer;
|
||||
transition: all 0.2s ease; font-size: 14px; font-weight: 500; display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.time-text { font-weight: 600; }
|
||||
.slot-info { font-size: 11px; opacity: 0.8; }
|
||||
/* 💡 [수정] 예약 가능 슬롯 스타일 변경 */
|
||||
.time-slot.available { background: #e7f3ff; border-color: #007bff; color: #004085; }
|
||||
.time-slot.available:hover { background: #d4edda; transform: translateY(-1px); }
|
||||
.time-slot.full { background: #f8d7da; border-color: #dc3545; color: #721c24; cursor: not-allowed; }
|
||||
.time-slot.too-soon { background: #f1f3f5; color: #868e96; cursor: not-allowed; border-color: #dee2e6; }
|
||||
.time-slot.selected { background: #007bff; border-color: #007bff; color: #fff; }
|
||||
|
||||
/* 고객 정보 폼 */
|
||||
.reservation-summary { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.reservation-summary h5 { margin: 0 0 10px 0; font-size: 16px; }
|
||||
.summary-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
|
||||
.summary-item { display: flex; justify-content: space-between; font-size: 14px; }
|
||||
.summary-item .label { font-weight: 500; color: #495057; }
|
||||
.summary-item span { color: #333; }
|
||||
.customer-info-form h4 { margin: 0 0 15px 0; color: #333; font-size: 16px; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #495057; font-size: 14px; }
|
||||
.required { color: #dc3545; font-weight: bold; }
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px;
|
||||
font-size: 14px; transition: border-color 0.2s ease; box-sizing: border-box;
|
||||
}
|
||||
.form-group textarea { resize: vertical; min-height: 80px; }
|
||||
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
|
||||
outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 네비게이션 버튼 */
|
||||
.reservation-nav-buttons {
|
||||
display: flex; justify-content: space-between; gap: 10px; margin-top: 25px;
|
||||
padding-top: 20px; border-top: 1px solid #e9ecef;
|
||||
}
|
||||
.reservation-nav-buttons button {
|
||||
padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px;
|
||||
font-weight: 600; cursor: pointer; transition: all 0.2s ease; min-width: 100px;
|
||||
}
|
||||
.btn-prev { background: #6c757d; color: #fff; }
|
||||
.btn-prev:hover { background: #5a6268; }
|
||||
.btn-next, .btn-submit { background: #007bff; color: #fff; }
|
||||
.btn-next:hover, .btn-submit:hover { background: #0056b3; }
|
||||
|
||||
/* 반응형 디자인 - 모바일 최적화 */
|
||||
@media (max-width: 768px) {
|
||||
.reservation-modal-content { margin: 10px; max-height: 95vh; border-radius: 8px; }
|
||||
.reservation-modal-header, .reservation-modal-body { padding: 15px; }
|
||||
.reservation-modal-header h2 { font-size: 18px; }
|
||||
.reservation-steps { margin-bottom: 20px; }
|
||||
.step { padding: 0 10px; }
|
||||
.step-number { width: 28px; height: 28px; font-size: 13px; }
|
||||
.step-text { font-size: 11px; }
|
||||
.calendar-day { font-size: 13px; }
|
||||
.time-slots-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }
|
||||
.form-row { grid-template-columns: 1fr; gap: 0; }
|
||||
.form-group input, .form-group textarea, .form-group select { font-size: 16px; /* iOS 줌 방지 */ }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.reservation-modal-overlay { padding: 0; align-items: flex-end; }
|
||||
.reservation-modal-content { margin: 0; width: 100%; max-height: 90vh; border-radius: 10px 10px 0 0; }
|
||||
.reservation-nav-buttons { flex-direction: column-reverse; gap: 10px; }
|
||||
.reservation-nav-buttons button { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 💡 [개선] JavaScript 로직 전면 수정 -->
|
||||
<script>
|
||||
const ReservationPopup = {
|
||||
elements: {},
|
||||
state: {
|
||||
currentStep: 1,
|
||||
selectedDate: null,
|
||||
selectedTime: null,
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
},
|
||||
config: {
|
||||
ajaxUrl: '<?php echo $ajax_url; ?>',
|
||||
maxAdvanceDays: <?php echo (int)$max_advance_days; ?>,
|
||||
},
|
||||
|
||||
init() {
|
||||
this.elements.popup = document.getElementById('reservation-popup');
|
||||
if (!this.elements.popup) return;
|
||||
|
||||
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
|
||||
this.elements.form = this.elements.popup.querySelector('#reservation-form');
|
||||
this.elements.calendar = {
|
||||
year: this.elements.popup.querySelector('#calendar-year'),
|
||||
month: this.elements.popup.querySelector('#calendar-month'),
|
||||
days: this.elements.popup.querySelector('#calendar-days'),
|
||||
prevBtn: this.elements.popup.querySelector('.prev-month'),
|
||||
nextBtn: this.elements.popup.querySelector('.next-month'),
|
||||
};
|
||||
this.elements.timeSlotsGrid = this.elements.popup.querySelector('#time-slots-grid');
|
||||
this.elements.nav = {
|
||||
prevBtn: this.elements.popup.querySelector('.btn-prev'),
|
||||
nextBtn: this.elements.popup.querySelector('.btn-next'),
|
||||
submitBtn: this.elements.popup.querySelector('.btn-submit'),
|
||||
};
|
||||
|
||||
this.addEventListeners();
|
||||
},
|
||||
|
||||
addEventListeners() {
|
||||
// 💡 [수정] 닫기 버튼을 더 안정적으로 찾아 이벤트를 추가하고, null 체크를 추가합니다.
|
||||
const closeBtn = this.elements.popup.querySelector('.reservation-modal-close');
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
|
||||
|
||||
this.elements.popup.addEventListener('click', e => {
|
||||
if (e.target === this.elements.popup) this.close();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && this.elements.popup.classList.contains('active')) this.close();
|
||||
});
|
||||
|
||||
// 💡 [수정] 모든 이벤트 리스너에 null 체크를 추가하여 스크립트 오류를 방지합니다.
|
||||
if (this.elements.calendar.prevBtn) this.elements.calendar.prevBtn.addEventListener('click', () => this.changeMonth(-1));
|
||||
if (this.elements.calendar.nextBtn) this.elements.calendar.nextBtn.addEventListener('click', () => this.changeMonth(1));
|
||||
|
||||
if (this.elements.nav.prevBtn) this.elements.nav.prevBtn.addEventListener('click', () => this.goToStep(this.state.currentStep - 1));
|
||||
if (this.elements.nav.nextBtn) this.elements.nav.nextBtn.addEventListener('click', () => this.goToNextStep());
|
||||
if (this.elements.form) {
|
||||
this.elements.form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.submitForm();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
open() {
|
||||
this.state.currentYear = new Date().getFullYear();
|
||||
this.state.currentMonth = new Date().getMonth() + 1;
|
||||
this.goToStep(1);
|
||||
this.renderCalendar();
|
||||
this.elements.popup.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
close() {
|
||||
this.elements.popup.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
this.elements.form.reset();
|
||||
},
|
||||
|
||||
changeMonth(delta) {
|
||||
this.state.currentMonth += delta;
|
||||
if (this.state.currentMonth < 1) {
|
||||
this.state.currentMonth = 12;
|
||||
this.state.currentYear--;
|
||||
} else if (this.state.currentMonth > 12) {
|
||||
this.state.currentMonth = 1;
|
||||
this.state.currentYear++;
|
||||
}
|
||||
this.renderCalendar();
|
||||
},
|
||||
|
||||
async renderCalendar() {
|
||||
this.elements.calendar.year.textContent = this.state.currentYear;
|
||||
this.elements.calendar.month.textContent = this.state.currentMonth;
|
||||
this.elements.calendar.days.innerHTML = '<div class="loading">달력 정보를 불러오는 중...</div>';
|
||||
|
||||
const availability = await this.fetchMonthAvailability();
|
||||
if (!availability) {
|
||||
this.elements.calendar.days.innerHTML = '<div class="error">달력 정보를 불러올 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.calendar.days.innerHTML = '';
|
||||
const firstDay = new Date(this.state.currentYear, this.state.currentMonth - 1, 1);
|
||||
const daysInMonth = new Date(this.state.currentYear, this.state.currentMonth, 0).getDate();
|
||||
const startDayOfWeek = firstDay.getDay();
|
||||
|
||||
for (let i = 0; i < startDayOfWeek; i++) {
|
||||
this.elements.calendar.days.appendChild(this.createDayElement(0, true));
|
||||
}
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
this.elements.calendar.days.appendChild(this.createDayElement(day, false, availability[day]));
|
||||
}
|
||||
},
|
||||
|
||||
createDayElement(day, isOtherMonth, availability = null) {
|
||||
const dayElement = document.createElement('div');
|
||||
dayElement.className = 'calendar-day';
|
||||
if (isOtherMonth) {
|
||||
dayElement.classList.add('other-month');
|
||||
return dayElement;
|
||||
}
|
||||
|
||||
|
||||
dayElement.textContent = day;
|
||||
|
||||
const dateStr = `${this.state.currentYear}-${String(this.state.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const currentDate = new Date(dateStr);
|
||||
currentDate.setHours(0, 0, 0, 0);
|
||||
if (currentDate.getTime() === today.getTime()) {
|
||||
dayElement.classList.add('today');
|
||||
} else if (currentDate.getTime() < today.getTime()){
|
||||
dayElement.classList.add('unavailable');
|
||||
} else if (availability && availability.available) {
|
||||
dayElement.classList.add('available');
|
||||
dayElement.addEventListener('click', () => this.selectDate(dateStr, dayElement));
|
||||
} else {
|
||||
// 💡 [개선] 예약 불가 사유에 따라 다른 스타일을 적용합니다.
|
||||
if (availability && availability.reason === 'holiday') {
|
||||
dayElement.classList.add('holiday');
|
||||
} else if (availability && availability.reason === 'full') {
|
||||
dayElement.classList.add('full');
|
||||
} else {
|
||||
dayElement.classList.add('unavailable');
|
||||
}
|
||||
}
|
||||
return dayElement;
|
||||
},
|
||||
|
||||
selectDate(dateStr, element) {
|
||||
const prevSelected = this.elements.calendar.days.querySelector('.selected');
|
||||
if (prevSelected) prevSelected.classList.remove('selected');
|
||||
element.classList.add('selected');
|
||||
this.state.selectedDate = dateStr;
|
||||
},
|
||||
|
||||
async fetchMonthAvailability() {
|
||||
this.showLoading();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'get_month_availability');
|
||||
formData.append('year', this.state.currentYear);
|
||||
formData.append('month', this.state.currentMonth);
|
||||
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
return result.success ? result.data : null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching month availability:', error);
|
||||
return null;
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTimeSlots() {
|
||||
this.showLoading();
|
||||
this.elements.timeSlotsGrid.innerHTML = '';
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'get_time_slots');
|
||||
formData.append('date', this.state.selectedDate);
|
||||
|
||||
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderTimeSlots(result.data);
|
||||
} else {
|
||||
this.elements.timeSlotsGrid.innerHTML = `<div class="error">${result.message || '시간 정보를 불러올 수 없습니다.'}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching time slots:', error);
|
||||
this.elements.timeSlotsGrid.innerHTML = '<div class="error">오류가 발생했습니다. 다시 시도해주세요.</div>';
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
renderTimeSlots(slots) {
|
||||
// 💡 [수정] 슬롯이 하나도 없는 경우와, 예약 가능한 슬롯이 없는 경우를 구분하여 메시지를 표시합니다.
|
||||
if (slots.length === 0) {
|
||||
this.elements.timeSlotsGrid.innerHTML = '<div class="no-slots">해당 날짜에 운영되는 상담 시간이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let hasAvailableSlots = false;
|
||||
this.elements.timeSlotsGrid.innerHTML = ''; // 그리드 초기화
|
||||
|
||||
slots.forEach(slot => {
|
||||
const slotElement = document.createElement('div');
|
||||
slotElement.className = 'time-slot';
|
||||
|
||||
let slotInfoText = '';
|
||||
const reservedCount = slot.reserved_count;
|
||||
|
||||
if (slot.available) {
|
||||
hasAvailableSlots = true;
|
||||
slotElement.classList.add('available');
|
||||
slotElement.addEventListener('click', () => this.selectTime(slot.time, slotElement));
|
||||
slotInfoText = `예약 ${reservedCount} / ${slot.max_persons}`;
|
||||
} else {
|
||||
if (slot.reason === 'full') {
|
||||
slotElement.classList.add('full');
|
||||
slotInfoText = '마감';
|
||||
} else if (slot.reason === 'too_soon') {
|
||||
slotElement.classList.add('too-soon');
|
||||
slotInfoText = '예약 임박';
|
||||
} else {
|
||||
slotElement.classList.add('full'); // Fallback
|
||||
slotInfoText = '마감';
|
||||
}
|
||||
}
|
||||
slotElement.innerHTML = `<span class="time-text">${slot.time}</span> <span class="slot-info">${slotInfoText}</span>`;
|
||||
this.elements.timeSlotsGrid.appendChild(slotElement);
|
||||
});
|
||||
|
||||
// 예약 가능한 슬롯이 하나도 없는 경우 안내 메시지 추가
|
||||
if (!hasAvailableSlots) {
|
||||
const noSlotsMessage = document.createElement('div');
|
||||
noSlotsMessage.className = 'no-slots';
|
||||
noSlotsMessage.textContent = '현재 예약 가능한 시간이 없습니다. 다른 날짜를 선택해주세요.';
|
||||
this.elements.timeSlotsGrid.prepend(noSlotsMessage);
|
||||
}
|
||||
},
|
||||
|
||||
selectTime(time, element) {
|
||||
const prevSelected = this.elements.timeSlotsGrid.querySelector('.selected');
|
||||
if (prevSelected) prevSelected.classList.remove('selected');
|
||||
element.classList.add('selected');
|
||||
this.state.selectedTime = time;
|
||||
},
|
||||
|
||||
goToStep(step) {
|
||||
this.state.currentStep = step;
|
||||
this.elements.popup.querySelectorAll('.step').forEach((el, i) => {
|
||||
el.classList.toggle('active', i + 1 === step);
|
||||
el.classList.toggle('completed', i + 1 < step);
|
||||
});
|
||||
this.elements.popup.querySelectorAll('.reservation-step-content').forEach(el => {
|
||||
el.style.display = parseInt(el.dataset.step) === step ? 'block' : 'none';
|
||||
});
|
||||
|
||||
this.elements.nav.prevBtn.style.display = step > 1 ? 'inline-block' : 'none';
|
||||
this.elements.nav.nextBtn.style.display = step < 3 ? 'inline-block' : 'none';
|
||||
this.elements.nav.submitBtn.style.display = step === 3 ? 'inline-block' : 'none';
|
||||
},
|
||||
|
||||
goToNextStep() {
|
||||
if (this.state.currentStep === 1 && !this.state.selectedDate) {
|
||||
alert('날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (this.state.currentStep === 2 && !this.state.selectedTime) {
|
||||
alert('시간을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.currentStep < 3) {
|
||||
this.goToStep(this.state.currentStep + 1);
|
||||
if (this.state.currentStep === 2) this.fetchTimeSlots();
|
||||
if (this.state.currentStep === 3) this.updateSummary();
|
||||
}
|
||||
},
|
||||
|
||||
updateSummary() {
|
||||
const date = new Date(this.state.selectedDate);
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
|
||||
this.elements.popup.querySelector('#summary-date').textContent = date.toLocaleDateString('ko-KR', options);
|
||||
this.elements.popup.querySelector('#summary-time').textContent = this.state.selectedTime;
|
||||
this.elements.popup.querySelector('#selected-date-display').textContent = date.toLocaleDateString('ko-KR', options);
|
||||
this.elements.form.querySelector('#selected-date').value = this.state.selectedDate;
|
||||
this.elements.form.querySelector('#selected-time').value = this.state.selectedTime;
|
||||
},
|
||||
|
||||
async submitForm() {
|
||||
if (!this.elements.form.checkValidity()) {
|
||||
alert('필수 입력 항목을 모두 채워주세요.');
|
||||
this.elements.form.reportValidity();
|
||||
return;
|
||||
}
|
||||
this.showLoading();
|
||||
this.elements.nav.submitBtn.disabled = true;
|
||||
this.elements.nav.submitBtn.textContent = '처리 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(this.elements.form);
|
||||
const response = await fetch('<?php echo $form_action_url; ?>', { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('예약 신청이 완료되었습니다. 입금 확인 후 예약이 확정됩니다.');
|
||||
this.close();
|
||||
// 필요시 페이지 새로고침 또는 다른 동작 수행
|
||||
// location.reload();
|
||||
} else {
|
||||
alert(result.message || '예약 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
this.elements.nav.submitBtn.disabled = false;
|
||||
this.elements.nav.submitBtn.textContent = '예약 신청';
|
||||
}
|
||||
},
|
||||
|
||||
showLoading() { if (this.elements.loading) this.elements.loading.style.display = 'flex'; },
|
||||
hideLoading() { if (this.elements.loading) this.elements.loading.style.display = 'none'; },
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => ReservationPopup.init());
|
||||
|
||||
// 외부에서 팝업을 열기 위한 전역 함수
|
||||
function openReservationPopup() {
|
||||
ReservationPopup.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php } // end if($consultant_installed) ?>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 입력 데이터 정리
|
||||
$reservation_data = [
|
||||
'customer_name' => trim($_POST['customer_name'] ?? ''),
|
||||
'customer_phone' => trim($_POST['customer_phone'] ?? ''),
|
||||
'customer_email' => trim($_POST['customer_email'] ?? ''),
|
||||
'reservation_date' => trim($_POST['reservation_date'] ?? ''),
|
||||
'reservation_time' => trim($_POST['reservation_time'] ?? ''),
|
||||
'consultation_type' => trim($_POST['consultation_type'] ?? 'onsite'),
|
||||
'request_memo' => trim($_POST['customer_request'] ?? ''),
|
||||
'payment_amount' => (int)($_POST['payment_amount'] ?? 0),
|
||||
'status' => 'payment_pending'
|
||||
];
|
||||
|
||||
// 필수 항목 유효성 검사
|
||||
if (empty($reservation_data['customer_name']) || empty($reservation_data['customer_phone']) || empty($reservation_data['reservation_date']) || empty($reservation_data['reservation_time'])) {
|
||||
throw new Exception('필수 예약 정보가 누락되었습니다.');
|
||||
}
|
||||
|
||||
// 예약 가능 여부 재확인 (서버 측 검증)
|
||||
$sql_check = "SELECT COUNT(*) as cnt FROM consultant_schedule
|
||||
WHERE specific_date = '{$reservation_data['reservation_date']}'
|
||||
AND start_time = '{$reservation_data['reservation_time']}'
|
||||
AND is_available = 1";
|
||||
$schedule = sql_fetch($sql_check);
|
||||
|
||||
if (!$schedule || $schedule['cnt'] == 0) {
|
||||
throw new Exception('선택하신 시간은 예약이 불가능합니다. 다른 시간을 선택해주세요.');
|
||||
}
|
||||
|
||||
// 예약 생성
|
||||
$sql = "INSERT INTO consultant_reservations
|
||||
(customer_name, customer_phone, customer_email, reservation_date, reservation_time, consultation_type, request_memo, payment_amount, status, created_at, updated_at)
|
||||
VALUES
|
||||
(
|
||||
'" . sql_real_escape_string($reservation_data['customer_name']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['customer_phone']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['customer_email']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['reservation_date']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['reservation_time']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['consultation_type']) . "',
|
||||
'" . sql_real_escape_string($reservation_data['request_memo']) . "',
|
||||
'{$reservation_data['payment_amount']}',
|
||||
'{$reservation_data['status']}',
|
||||
NOW(),
|
||||
NOW()
|
||||
)";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
$reservation_id = sql_insert_id();
|
||||
consultant_log("새 예약 신청: ID {$reservation_id} (고객: {$reservation_data['customer_name']})");
|
||||
|
||||
// TODO: 고객 및 관리자에게 알림 발송 로직 추가
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '예약 신청이 완료되었습니다.']);
|
||||
} else {
|
||||
throw new Exception('데이터베이스 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("예약 신청 오류: " . $e->getMessage(), 'error');
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
$sub_menu = '850100'; // 대시보드 메뉴와 동일하게 설정
|
||||
include_once('./_common.php');
|
||||
|
||||
$g5['title'] = '상담 예약 팝업 샘플';
|
||||
include_once(G5_ADMIN_PATH.'/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이 페이지는 사이트의 어떤 페이지에서든 상담 예약 기능을 쉽게 추가하는 방법을 보여주는 예제입니다.<br>
|
||||
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
|
||||
</p>
|
||||
<pre><code><?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?></code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [시작] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
|
||||
<h3 style="margin-bottom:15px;">상담이 필요하신가요?</h3>
|
||||
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 상담을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
|
||||
|
||||
<!-- 1. 팝업을 여는 버튼들 -->
|
||||
<div class="consultant-buttons">
|
||||
<button type="button" class="reservation-btn-main" onclick="openReservationPopup()">
|
||||
<i class="fa fa-calendar"></i> 상담 예약 신청
|
||||
</button>
|
||||
<button type="button" class="reservation-check-btn" onclick="openReservationCheckPopup()">
|
||||
예약 확인/취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php');
|
||||
?>
|
||||
|
||||
<!-- 버튼 디자인을 위한 CSS (사이트의 공통 CSS 파일에 추가하는 것을 권장합니다) -->
|
||||
<style>
|
||||
.consultant-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.reservation-btn-main, .reservation-check-btn {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.reservation-btn-main {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
.reservation-btn-main:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
.reservation-btn-main i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.reservation-check-btn {
|
||||
background: #6c757d;
|
||||
}
|
||||
.reservation-check-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [끝] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH.'/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 관리 시스템 대시보드
|
||||
*/
|
||||
$sub_menu = '850100';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 예약 대시보드';
|
||||
|
||||
// 오늘 날짜 기준 통계
|
||||
$today = date('Y-m-d');
|
||||
$this_week_start = date('Y-m-d', strtotime('monday this week'));
|
||||
$this_month_start = date('Y-m-01');
|
||||
|
||||
// 오늘 예약 현황
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
|
||||
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date = '{$today}' AND is_deleted = 0";
|
||||
$today_stats = sql_fetch($sql);
|
||||
|
||||
// 이번 주 예약 현황
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$this_week_start}'
|
||||
AND reservation_date <= '{$today}'
|
||||
AND is_deleted = 0";
|
||||
$week_stats = sql_fetch($sql);
|
||||
|
||||
// 이번 달 예약 현황
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$this_month_start}'
|
||||
AND is_deleted = 0";
|
||||
$month_stats = sql_fetch($sql);
|
||||
|
||||
// 최근 예약 목록 (5개)
|
||||
$sql = "SELECT * FROM consultant_reservations
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5";
|
||||
$recent_reservations = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$recent_reservations[] = $row;
|
||||
}
|
||||
|
||||
// 오늘 예약 목록
|
||||
$sql = "SELECT * FROM consultant_reservations
|
||||
WHERE reservation_date = '{$today}'
|
||||
AND is_deleted = 0
|
||||
ORDER BY reservation_time";
|
||||
$today_reservations = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$today_reservations[] = $row;
|
||||
}
|
||||
|
||||
// 입금 대기 예약 수
|
||||
$sql = "SELECT COUNT(*) as count FROM consultant_reservations
|
||||
WHERE status = 'payment_pending' AND is_deleted = 0";
|
||||
$pending_count = sql_fetch($sql)['count'];
|
||||
|
||||
// 시간대별 예약 현황 (이번 주)
|
||||
$sql = "SELECT
|
||||
reservation_time,
|
||||
COUNT(*) as count
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$this_week_start}'
|
||||
AND reservation_date <= '{$today}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_time
|
||||
ORDER BY reservation_time";
|
||||
$time_stats = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$time_stats[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.dashboard-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-sublabel {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-card.today .stat-number {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.stat-card.week .stat-number {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.stat-card.month .stat-number {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.stat-card.pending .stat-number {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.reservation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.reservation-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.reservation-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reservation-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reservation-details {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.reservation-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.status-payment_pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-reserved {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
background: #007bff;
|
||||
width: 30px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.chart-bar:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-value {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 10px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #856404;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<div>
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
<p>상담 예약 현황을 한눈에 확인하세요</p>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<a href="reservations.php" class="btn btn-primary">예약 관리</a>
|
||||
<a href="schedule_generate.php" class="btn btn-success">일정 설정</a>
|
||||
<a href="statistics.php" class="btn btn-info">통계 보기</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card today">
|
||||
<div class="stat-number"><?php echo number_format($today_stats['total']); ?></div>
|
||||
<div class="stat-label">오늘 예약</div>
|
||||
<div class="stat-sublabel">
|
||||
확정 <?php echo $today_stats['confirmed']; ?>건 |
|
||||
대기 <?php echo $today_stats['pending']; ?>건
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card week">
|
||||
<div class="stat-number"><?php echo number_format($week_stats['total']); ?></div>
|
||||
<div class="stat-label">이번 주 예약</div>
|
||||
<div class="stat-sublabel">
|
||||
매출 <?php echo number_format($week_stats['revenue']); ?>원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card month">
|
||||
<div class="stat-number"><?php echo number_format($month_stats['total']); ?></div>
|
||||
<div class="stat-label">이번 달 예약</div>
|
||||
<div class="stat-sublabel">
|
||||
매출 <?php echo number_format($month_stats['revenue']); ?>원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-number"><?php echo number_format($pending_count); ?></div>
|
||||
<div class="stat-label">입금 대기</div>
|
||||
<div class="stat-sublabel">
|
||||
<?php if ($pending_count > 0): ?>
|
||||
<a href="reservations.php?status=payment_pending" style="color: #856404;">확인 필요</a>
|
||||
<?php else: ?>
|
||||
모두 처리됨
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($pending_count > 0): ?>
|
||||
<div class="alert alert-warning">
|
||||
<strong>알림:</strong> 입금 대기 중인 예약이 <?php echo $pending_count; ?>건 있습니다.
|
||||
<a href="reservations.php?status=payment_pending">지금 확인하기</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<div class="content-grid">
|
||||
<!-- 오늘 예약 현황 -->
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<span>오늘 예약 현황 (<?php echo date('Y-m-d'); ?>)</span>
|
||||
<a href="reservations.php?date=<?php echo $today; ?>" class="btn btn-sm btn-primary">전체 보기</a>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<?php if (empty($today_reservations)): ?>
|
||||
<div class="empty-state">
|
||||
<p>오늘 예약된 상담이 없습니다.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($today_reservations as $reservation): ?>
|
||||
<div class="reservation-item">
|
||||
<div class="reservation-info">
|
||||
<div class="reservation-name">
|
||||
<?php echo htmlspecialchars($reservation['customer_name']); ?>
|
||||
</div>
|
||||
<div class="reservation-details">
|
||||
<?php echo $reservation['reservation_time']; ?> |
|
||||
<?php echo htmlspecialchars($reservation['customer_phone']); ?> |
|
||||
<?php echo number_format($reservation['payment_amount']); ?>원
|
||||
</div>
|
||||
</div>
|
||||
<div class="reservation-status status-<?php echo $reservation['status']; ?>">
|
||||
<?php
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
echo $status_labels[$reservation['status']] ?? $reservation['status'];
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 예약 -->
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<span>최근 예약</span>
|
||||
<a href="reservations.php" class="btn btn-sm btn-primary">전체 보기</a>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<?php if (empty($recent_reservations)): ?>
|
||||
<div class="empty-state">
|
||||
<p>최근 예약이 없습니다.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recent_reservations as $reservation): ?>
|
||||
<div class="reservation-item">
|
||||
<div class="reservation-info">
|
||||
<div class="reservation-name">
|
||||
<?php echo htmlspecialchars($reservation['customer_name']); ?>
|
||||
</div>
|
||||
<div class="reservation-details">
|
||||
<?php echo $reservation['reservation_date']; ?>
|
||||
<?php echo $reservation['reservation_time']; ?><br>
|
||||
<?php echo date('m-d H:i', strtotime($reservation['created_at'])); ?> 신청
|
||||
</div>
|
||||
</div>
|
||||
<div class="reservation-status status-<?php echo $reservation['status']; ?>">
|
||||
<?php
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
echo $status_labels[$reservation['status']] ?? $reservation['status'];
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시간대별 예약 현황 -->
|
||||
<?php if (!empty($time_stats)): ?>
|
||||
<div class="content-card">
|
||||
<div class="card-header">
|
||||
<span>시간대별 예약 현황 (이번 주)</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container">
|
||||
<?php
|
||||
$max_count = max(array_column($time_stats, 'count'));
|
||||
foreach ($time_stats as $stat):
|
||||
$height = $max_count > 0 ? ($stat['count'] / $max_count) * 150 : 0;
|
||||
?>
|
||||
<div class="chart-bar" style="height: <?php echo $height; ?>px;">
|
||||
<div class="chart-value"><?php echo $stat['count']; ?></div>
|
||||
<div class="chart-label"><?php echo substr($stat['reservation_time'], 0, 5); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 시스템 정보 -->
|
||||
<div class="alert alert-info">
|
||||
<strong>시스템 정보:</strong>
|
||||
상담 예약 시스템 v<?php echo G5_CONSULTANT_VERSION; ?> |
|
||||
마지막 업데이트: <?php echo date('Y-m-d H:i'); ?> |
|
||||
<a href="settings.php">시스템 설정</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 실시간 업데이트 (5분마다)
|
||||
setInterval(function () {
|
||||
location.reload();
|
||||
}, 300000);
|
||||
|
||||
// 차트 애니메이션
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const bars = document.querySelectorAll('.chart-bar');
|
||||
bars.forEach((bar, index) => {
|
||||
setTimeout(() => {
|
||||
bar.style.opacity = '0';
|
||||
bar.style.transform = 'scaleY(0)';
|
||||
bar.style.transformOrigin = 'bottom';
|
||||
|
||||
setTimeout(() => {
|
||||
bar.style.transition = 'all 0.5s ease';
|
||||
bar.style.opacity = '1';
|
||||
bar.style.transform = 'scaleY(1)';
|
||||
}, 100);
|
||||
}, index * 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
// 상담 관리 시스템 버전
|
||||
define('G5_CONSULTANT_VERSION', '1.0.0');
|
||||
|
||||
// 상담 관리 시스템 설치 확인
|
||||
function is_consultant_installed()
|
||||
{
|
||||
global $g5;
|
||||
$consultant_tables = ['consultant_schedule', 'consultant_reservations', 'consultant_config', 'consultant_log'];
|
||||
|
||||
foreach ($consultant_tables as $table) {
|
||||
if(!isset($g5[$table.'_table'])) $g5[$table.'_table'] = $table;
|
||||
$sql = "SHOW TABLES LIKE '{$g5[$table.'_table']}'";
|
||||
$result = sql_query($sql, false);
|
||||
if (!$result || sql_num_rows($result) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 상담 관리 시스템 권한 확인
|
||||
function consultant_auth_check($auth_level = 'r')
|
||||
{
|
||||
global $member, $is_admin;
|
||||
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 추가 권한 확인 로직 (필요시)
|
||||
return true;
|
||||
}
|
||||
|
||||
// 상담 관리 시스템 설정값 조회
|
||||
function consultant_get_config($key, $default = null)
|
||||
{
|
||||
global $g5;
|
||||
if(!isset($g5['consultant_config_table'])) $g5['consultant_config_table'] = 'consultant_config';
|
||||
|
||||
$sql = "SELECT config_value FROM {$g5['consultant_config_table']} WHERE config_key = '" . sql_real_escape_string($key) . "'";
|
||||
$result = sql_fetch($sql);
|
||||
|
||||
if ($result) {
|
||||
return $result['config_value'];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 상담 관리 시스템 설정값 저장
|
||||
function consultant_set_config($key, $value)
|
||||
{
|
||||
global $g5;
|
||||
if(!isset($g5['consultant_config_table'])) $g5['consultant_config_table'] = 'consultant_config';
|
||||
|
||||
$sql = "INSERT INTO {$g5['consultant_config_table']} (config_key, config_value, updated_at)
|
||||
VALUES ('" . sql_real_escape_string($key) . "', '" . sql_real_escape_string($value) . "', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_value = '" . sql_real_escape_string($value) . "',
|
||||
updated_at = NOW()";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
// 로그 함수
|
||||
function consultant_log($message, $level = 'info')
|
||||
{
|
||||
global $g5, $member;
|
||||
if(!isset($g5['consultant_log_table'])) $g5['consultant_log_table'] = 'consultant_log';
|
||||
|
||||
$sql = "INSERT INTO {$g5['consultant_log_table']} (mb_id, log_level, log_message, ip_address, log_time) VALUES ('".($member['mb_id'] ?? '')."', '".sql_real_escape_string($level)."', '".sql_real_escape_string($message)."', '".$_SERVER['REMOTE_ADDR']."', '".G5_TIME_YMDHIS."')";
|
||||
sql_query($sql, false);
|
||||
}
|
||||
|
||||
// 💡 [추가] 알림 발송 함수 (메일/SMS 통합)
|
||||
function consultant_send_notification($type, $template_key, $data) {
|
||||
global $g5;
|
||||
|
||||
// 템플릿 조회
|
||||
$table_name = ($type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
|
||||
|
||||
// 테이블 존재 확인
|
||||
$table_check = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
|
||||
if (sql_num_rows($table_check) == 0) return false;
|
||||
|
||||
$sql = "SELECT * FROM {$table_name} WHERE template_key = '" . sql_real_escape_string($template_key) . "'";
|
||||
$template = sql_fetch($sql);
|
||||
|
||||
if (!$template) return false;
|
||||
|
||||
// 변수 치환
|
||||
$subject = $template['template_subject'];
|
||||
$content = $template['template_content'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$subject = str_replace('{' . $key . '}', $value, $subject);
|
||||
$content = str_replace('{' . $key . '}', $value, $content);
|
||||
}
|
||||
|
||||
if ($type === 'email') {
|
||||
// 이메일 발송 (G5 기본 mailer 사용 가정)
|
||||
include_once(G5_LIB_PATH.'/mailer.lib.php');
|
||||
// $data['customer_email'] 등이 존재해야 함
|
||||
if (!empty($data['customer_email'])) {
|
||||
mailer($g5['title'], $g5['admin_email'], $data['customer_email'], $subject, $content, 1);
|
||||
consultant_log("이메일 발송: {$template_key} to {$data['customer_email']}");
|
||||
return true;
|
||||
}
|
||||
} elseif ($type === 'sms') {
|
||||
// SMS 발송 (G5 기본 SMS 라이브러리 사용 가정 - 실제 구현은 SMS 모듈에 따라 다름)
|
||||
// 여기서는 로그만 남기고 실제 발송 로직은 SMS 모듈에 맞게 구현 필요
|
||||
// 예:
|
||||
// include_once(G5_LIB_PATH.'/icode.sms.lib.php');
|
||||
// $SMS = new SMS;
|
||||
// $SMS->SMS_con($g5['sms_admin'], $g5['sms_id'], $g5['sms_pw'], $g5['sms_port']);
|
||||
// $SMS->Add($data['customer_phone'], $g5['admin_phone'], $g5['sms_admin'], iconv("utf-8", "euc-kr", $content), "");
|
||||
// $SMS->Send();
|
||||
|
||||
consultant_log("SMS 발송 시도: {$template_key} to {$data['customer_phone']} (내용: $content)");
|
||||
return true; // 실제 발송 성공 여부에 따라 변경
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
$sub_menu = '850640';
|
||||
include_once('./_common.php');
|
||||
include_once(__DIR__ . '/lib/SchemaManager.class.php');
|
||||
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일에서 테이블 이름을 추출하는 함수
|
||||
*/
|
||||
function get_tables_from_sql_file($filepath) {
|
||||
$tables = [];
|
||||
if (!file_exists($filepath)) return $tables;
|
||||
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `([^`]+)`/i', $line, $matches)) {
|
||||
$tables[] = $matches[1];
|
||||
}
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 예약 시스템 설치';
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
|
||||
$install_result = null;
|
||||
$delete_result = null;
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
$tables_to_check = get_tables_from_sql_file(__DIR__ . '/install.sql');
|
||||
|
||||
if ($action === 'install') {
|
||||
check_admin_token();
|
||||
try {
|
||||
$sql_file = __DIR__ . '/install.sql';
|
||||
$schemaManager = new SchemaManager($sql_file);
|
||||
$schemaManager->execute();
|
||||
$db_results = $schemaManager->get_results();
|
||||
$data_msg = insert_default_data() ? "성공" : "실패";
|
||||
$menu_msg = create_admin_menu_file();
|
||||
$install_result = ['db' => $db_results, 'data' => $data_msg, 'menu' => $menu_msg];
|
||||
} catch (Exception $e) {
|
||||
$install_result['errors'][] = '설치 중 심각한 오류 발생: ' . $e->getMessage();
|
||||
}
|
||||
} else if ($action === 'delete') {
|
||||
check_admin_token();
|
||||
$delete_result = ['tables' => [], 'menu' => ''];
|
||||
$tables_to_delete = $tables_to_check;
|
||||
foreach ($tables_to_delete as $table) {
|
||||
sql_query("DROP TABLE IF EXISTS `{$table}`", false);
|
||||
$delete_result['tables'][] = $table;
|
||||
}
|
||||
$menu_file = G5_ADMIN_PATH . '/admin.menu850.consultant_manage.php';
|
||||
if (file_exists($menu_file)) {
|
||||
if (@unlink($menu_file)) {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 성공';
|
||||
} else {
|
||||
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insert_default_data() {
|
||||
$default_configs = [
|
||||
['consultation_duration', '60', '1회 상담 시간 (분)'], ['max_persons_per_slot', '2', '동시간대 최대 예약 인원'],
|
||||
['consultation_fee', '50000', '상담 비용 (원)'], ['account_info', '국민은행 123-456-789 (주)상담센터', '상담비 입금 계좌'],
|
||||
['notification_enabled', '1', '알림 발송 사용 여부'], ['auto_confirm_enabled', '0', '자동 예약 확정 사용 여부'],
|
||||
['max_advance_days', '30', '최대 예약 가능 일수'], ['min_advance_hours', '24', '최소 예약 시간 (시간)'],
|
||||
['cancel_deadline_hours', '24', '예약 취소 마감 시간 (시간)']
|
||||
];
|
||||
foreach ($default_configs as $config) {
|
||||
sql_query("INSERT IGNORE INTO consultant_config (config_key, config_value, config_desc) VALUES ('{$config[0]}', '{$config[1]}', '{$config[2]}')");
|
||||
}
|
||||
|
||||
// 💡 [수정] 메일 템플릿 기본 데이터
|
||||
$mail_templates = [
|
||||
['consultant_reservation_customer', '고객 예약 신청 확인', '[상담예약] 예약 신청이 접수되었습니다', "안녕하세요 {customer_name}님,\n\n상담 예약 신청이 정상적으로 접수되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n- 상담비: {payment_amount}원\n\n입금 계좌: {account_info}\n\n입금 확인 후 예약이 확정됩니다.\n\n감사합니다."],
|
||||
['consultant_confirmed_customer', '고객 예약 확정 알림', '[상담예약] 예약이 확정되었습니다', "안녕하세요 {customer_name}님,\n\n입금이 확인되어 예약이 확정되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n상담 당일 시간에 맞춰 방문해주시기 바랍니다.\n\n감사합니다."],
|
||||
['consultant_cancelled_customer', '고객 예약 취소 알림', '[상담예약] 예약이 취소되었습니다', "안녕하세요 {customer_name}님,\n\n예약이 취소되었습니다.\n\n취소된 예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n취소 사유: {cancel_reason}\n\n문의사항이 있으시면 연락주시기 바랍니다.\n\n감사합니다."]
|
||||
];
|
||||
foreach ($mail_templates as $template) {
|
||||
sql_query("INSERT IGNORE INTO consultant_mail_templates (template_key, template_name, template_subject, template_content) VALUES ('{$template[0]}', '{$template[1]}', '{$template[2]}', '" . sql_real_escape_string($template[3]) . "')");
|
||||
}
|
||||
|
||||
// 💡 [추가] SMS 템플릿 기본 데이터
|
||||
$sms_templates = [
|
||||
['consultant_reservation_customer', '고객 예약 신청 확인', '[상담예약] 예약 신청 접수', "{customer_name}님, 상담 예약이 접수되었습니다.\n일시: {reservation_date} {reservation_time}\n계좌: {account_info}\n입금 확인 후 확정됩니다."],
|
||||
['consultant_confirmed_customer', '고객 예약 확정 알림', '[상담예약] 예약 확정', "{customer_name}님, 예약이 확정되었습니다.\n일시: {reservation_date} {reservation_time}\n시간 맞춰 방문 부탁드립니다."],
|
||||
['consultant_cancelled_customer', '고객 예약 취소 알림', '[상담예약] 예약 취소', "{customer_name}님, 예약이 취소되었습니다.\n일시: {reservation_date} {reservation_time}\n사유: {cancel_reason}"]
|
||||
];
|
||||
foreach ($sms_templates as $template) {
|
||||
sql_query("INSERT IGNORE INTO consultant_sms_templates (template_key, template_name, template_subject, template_content) VALUES ('{$template[0]}', '{$template[1]}', '{$template[2]}', '" . sql_real_escape_string($template[3]) . "')");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function create_admin_menu_file() {
|
||||
$source_file = __DIR__ . '/admin.menu850.consultant_manage.php';
|
||||
$target_file = G5_ADMIN_PATH . '/admin.menu850.consultant_manage.php';
|
||||
if (!file_exists($source_file)) return "실패 (메뉴 원본 파일 없음)";
|
||||
if (file_exists($target_file)) return "성공 (이미 존재함)";
|
||||
if (@copy($source_file, $target_file)) return "성공";
|
||||
return "실패 (파일 복사 오류)";
|
||||
}
|
||||
|
||||
$existing_tables = [];
|
||||
foreach ($tables_to_check as $table) {
|
||||
if (sql_query("SHOW TABLES LIKE '$table'", false) && sql_num_rows(sql_query("SHOW TABLES LIKE '$table'", false)) > 0) {
|
||||
$existing_tables[] = $table;
|
||||
}
|
||||
}
|
||||
$is_installed = count($existing_tables) == count($tables_to_check);
|
||||
?>
|
||||
|
||||
<style>
|
||||
.install-container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.install-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #AA20FF; }
|
||||
.install-header h1 { color: #AA20FF; margin-bottom: 10px; }
|
||||
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0; }
|
||||
.feature-card { padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center; }
|
||||
.feature-card i { font-size: 2em; color: #AA20FF; margin-bottom: 10px; }
|
||||
.status-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
.status-table th, .status-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
.status-table th { background-color: #fff; font-weight: bold; }
|
||||
.status-ok { color: #28a745; font-weight: bold; }
|
||||
.status-missing { color: #dc3545; font-weight: bold; }
|
||||
.install-btn { display: block; width: 200px; margin: 30px auto; padding: 15px 30px; background: #AA20FF; color: white; text-align: center; text-decoration: none; border-radius: 5px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; transition: background-color 0.3s; }
|
||||
.install-btn:hover { background: #8A1ACC; color: white; }
|
||||
.install-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
.alert { padding: 15px; margin: 20px 0; border-radius: 5px; }
|
||||
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.alert-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||||
.btn-secondary { background: #6c757d; color: white; border-color: #6c757d; padding: 5px 10px; border-radius: 4px; text-decoration: none; }
|
||||
.btn-secondary:hover { background: #5a6268; }
|
||||
.btn-danger { background: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
|
||||
.btn-danger:hover { background: #c82333; }
|
||||
.button-group { display: flex; justify-content: center; align-items: center; gap: 10px; }
|
||||
</style>
|
||||
|
||||
<div class="install-container">
|
||||
<div class="install-header">
|
||||
<h1><i class="fa fa-calendar-check"></i> 상담 예약 시스템</h1>
|
||||
<p>전문적인 상담 예약 관리 및 일정 관리 솔루션</p>
|
||||
</div>
|
||||
<?php if ($install_result): ?>
|
||||
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 작업 완료</h4>
|
||||
<p>데이터베이스 및 기본 설정 설치가 완료되었습니다.</p>
|
||||
<p><?php echo 'data : '.$install_result['data']; ?></p>
|
||||
<p><?php echo 'menu : '.$install_result['menu']; ?></p>
|
||||
<p><a href="./dashboard.php" class="btn btn-primary">상담 예약 관리로 이동</a></p></div>
|
||||
<?php elseif ($delete_result): ?>
|
||||
<div class="alert alert-danger"><h4><i class="fa fa-trash"></i> 삭제 작업 완료</h4><p>솔루션 관련 데이터와 파일이 삭제되었습니다.</p><ul><?php foreach($delete_result['tables'] as $tbl) echo "<li>{$tbl} 테이블 삭제됨</li>"; ?><li><?php echo $delete_result['menu']; ?></li></ul></div>
|
||||
<?php elseif ($is_installed): ?>
|
||||
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 완료</h4><p>상담 예약 시스템이 이미 설치되어 있습니다.</p><p><a href="./dashboard.php" class="btn btn-primary">상담 예약 관리로 이동</a></p></div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-info"><h4><i class="fa fa-info-circle"></i> 설치 필요</h4><p>상담 예약 시스템을 사용하기 위해 설치가 필요합니다.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3><i class="fa fa-database"></i> 설치 상태</h3>
|
||||
<table class="status-table">
|
||||
<thead><tr><th>테이블명</th><th>설명</th><th>상태</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($tables_to_check as $table): ?>
|
||||
<tr>
|
||||
<td><code><?php echo $table; ?></code></td>
|
||||
<td><?php echo array('consultant_config' => '환경 설정', 'consultant_schedule' => '상담 스케줄', 'consultant_reservations' => '예약 정보', 'consultant_mail_templates' => '메일 템플릿', 'consultant_sms_templates' => '문자 템플릿', 'consultant_log' => '시스템 로그')[$table] ?? '데이터 테이블'; ?></td>
|
||||
<td>
|
||||
<?php if (in_array($table, $existing_tables)): ?>
|
||||
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
|
||||
<?php else: ?>
|
||||
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if (!$is_installed): ?>
|
||||
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_installed && !$install_result && !$delete_result): ?>
|
||||
<div class="button-group" style="text-align: center; margin-top: 20px;">
|
||||
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
|
||||
<input type="hidden" name="action" value="install">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터와 파일이 영구적으로 삭제됩니다.');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
|
||||
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,159 @@
|
||||
-- 1. 상담 설정 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_config` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`config_key` varchar(100) NOT NULL COMMENT '설정 키',
|
||||
`config_value` text COMMENT '설정 값',
|
||||
`config_desc` varchar(255) DEFAULT NULL COMMENT '설정 설명',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `config_key` (`config_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 시스템 설정';
|
||||
|
||||
-- 2. 상담 일정 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_schedule` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`specific_date` date DEFAULT NULL COMMENT '특정 날짜',
|
||||
`start_time` time NOT NULL COMMENT '시작 시간',
|
||||
`end_time` time NOT NULL COMMENT '종료 시간',
|
||||
`time_slot` int(11) DEFAULT 60 COMMENT '예약 단위 시간(분)',
|
||||
`max_persons` int(11) DEFAULT 1 COMMENT '동시간대 최대 예약 인원',
|
||||
`is_available` tinyint(1) DEFAULT 1 COMMENT '예약 가능 여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_specific_date` (`specific_date`),
|
||||
KEY `idx_is_available` (`is_available`),
|
||||
KEY `idx_time_range` (`start_time`,`end_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 상세 스케줄';
|
||||
|
||||
-- 3. 상담 예약 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_reservations` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`wr_id` int(11) DEFAULT NULL COMMENT '연결된 게시글 ID',
|
||||
`customer_name` varchar(100) NOT NULL COMMENT '고객명',
|
||||
`customer_phone` varchar(20) NOT NULL COMMENT '고객 연락처',
|
||||
`customer_email` varchar(100) DEFAULT NULL COMMENT '고객 이메일',
|
||||
`reservation_date` date NOT NULL COMMENT '예약 날짜',
|
||||
`reservation_time` time NOT NULL COMMENT '예약 시간',
|
||||
`consultation_type` varchar(50) DEFAULT 'onsite' COMMENT '상담 유형',
|
||||
`resource_id` int(11) DEFAULT NULL COMMENT '배정된 리소스 ID',
|
||||
`status` varchar(50) DEFAULT 'payment_pending' COMMENT '예약 상태',
|
||||
`payment_amount` int(11) DEFAULT 0 COMMENT '상담 비용',
|
||||
`payment_status` varchar(50) DEFAULT 'pending' COMMENT '결제 상태',
|
||||
`payment_confirmed_at` datetime DEFAULT NULL COMMENT '입금 확인 시간',
|
||||
`payment_confirmed_by` varchar(50) DEFAULT NULL COMMENT '입금 확인자',
|
||||
`request_memo` text COMMENT '고객 요청사항',
|
||||
`admin_memo` text COMMENT '관리자 메모',
|
||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '삭제 여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_reservation_date` (`reservation_date`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_customer_phone` (`customer_phone`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 정보';
|
||||
|
||||
-- 4. 상담 메일 알림 템플릿 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_mail_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_type` varchar(10) NOT NULL DEFAULT 'email' COMMENT '템플릿 종류 (email)',
|
||||
`template_name` varchar(200) NOT NULL,
|
||||
`template_subject` varchar(255) NOT NULL,
|
||||
`template_content` text NOT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `template_key` (`template_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 메일 알림 템플릿';
|
||||
|
||||
-- 5. 상담 문자 알림 템플릿 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_sms_templates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_key` varchar(100) NOT NULL,
|
||||
`template_type` varchar(10) NOT NULL DEFAULT 'sms' COMMENT '템플릿 종류 (sms)',
|
||||
`template_name` varchar(200) NOT NULL,
|
||||
`template_subject` varchar(255) NOT NULL,
|
||||
`template_content` text NOT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `template_key` (`template_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 문자 알림 템플릿';
|
||||
|
||||
-- 6. 시스템 로그 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_log` (
|
||||
`log_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`log_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`mb_id` varchar(20) DEFAULT NULL,
|
||||
`log_level` varchar(20) NOT NULL DEFAULT 'info',
|
||||
`log_message` text NOT NULL,
|
||||
`ip_address` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`log_id`),
|
||||
KEY `idx_log_time` (`log_time`),
|
||||
KEY `idx_mb_id` (`mb_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 시스템 로그';
|
||||
|
||||
-- 7. 상담 그룹 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_groups` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL COMMENT '그룹명',
|
||||
`is_active` tinyint(1) DEFAULT 1 COMMENT '사용여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담/자원 그룹';
|
||||
|
||||
-- 8. 상담 리소스 테이블
|
||||
CREATE TABLE IF NOT EXISTS `consultant_resources` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`group_id` int(11) NOT NULL COMMENT '그룹 ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '리소스명(이름/호실)',
|
||||
`description` varchar(255) DEFAULT NULL COMMENT '설명',
|
||||
`is_active` tinyint(1) DEFAULT 1 COMMENT '사용여부',
|
||||
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
|
||||
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
|
||||
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
|
||||
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
|
||||
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
|
||||
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
|
||||
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
|
||||
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
|
||||
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
|
||||
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `group_id` (`group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='상담사/자원 목록';
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
class NotificationManager
|
||||
{
|
||||
/**
|
||||
* 템플릿 키를 기반으로 알림을 발송합니다.
|
||||
* @param string $template_key 템플릿 키 (e.g., 'reservation_confirmed')
|
||||
* @param array $data 치환될 데이터 배열 (e.g., ['customer_name' => '홍길동'])
|
||||
* @return bool 성공 여부
|
||||
*/
|
||||
public function sendNotification($template_key, $data)
|
||||
{
|
||||
$success = true;
|
||||
|
||||
// 1. 이메일 템플릿 조회 및 발송
|
||||
$email_sql = "SELECT * FROM consultant_mail_templates WHERE template_key = '" . sql_real_escape_string($template_key) . "' AND is_active = 1";
|
||||
$email_template = sql_fetch($email_sql);
|
||||
|
||||
if ($email_template) {
|
||||
$subject = $this->replaceVariables($email_template['template_subject'], $data);
|
||||
$content = $this->replaceVariables($email_template['template_content'], $data);
|
||||
|
||||
if (!$this->sendEmail($data['customer_email'], $data['customer_name'], $subject, $content)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. SMS 템플릿 조회 및 발송
|
||||
$sms_sql = "SELECT * FROM consultant_sms_templates WHERE template_key = '" . sql_real_escape_string($template_key) . "' AND is_active = 1";
|
||||
$sms_template = sql_fetch($sms_sql);
|
||||
|
||||
if ($sms_template) {
|
||||
$content = $this->replaceVariables($sms_template['template_content'], $data);
|
||||
|
||||
if (!$this->sendSms($data['customer_phone'], $content)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 변수를 실제 값으로 치환합니다.
|
||||
*/
|
||||
private function replaceVariables($text, $data)
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$text = str_replace('{' . $key . '}', $value, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 💡 [연동] mail_manage 시스템을 사용하여 메일을 발송합니다.
|
||||
*/
|
||||
private function sendEmail($to_email, $to_name, $subject, $content)
|
||||
{
|
||||
// mail_manage의 라이브러리 포함
|
||||
if (file_exists(G5_ADMIN_PATH . '/mail_manage/mailer.lib.php')) {
|
||||
include_once(G5_ADMIN_PATH . '/mail_manage/mailer.lib.php');
|
||||
|
||||
// mailer() 함수 호출 (mail_manage의 함수명에 따라 수정 필요)
|
||||
// mailer($from_name, $from_email, $to_email, $subject, $content, 1);
|
||||
consultant_log("[Email Sent] To: {$to_email}, Subject: {$subject}");
|
||||
return true;
|
||||
}
|
||||
consultant_log("[Email Error] mail_manage/mailer.lib.php not found.", 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 💡 [연동] sms_admin 시스템을 사용하여 문자를 발송합니다.
|
||||
*/
|
||||
private function sendSms($to_phone, $content)
|
||||
{
|
||||
// sms_admin의 라이브러리 포함
|
||||
if (file_exists(G5_ADMIN_PATH . '/sms_admin/sms.lib.php')) {
|
||||
include_once(G5_ADMIN_PATH . '/sms_admin/sms.lib.php');
|
||||
|
||||
// sms_send() 함수 호출 (sms_admin의 함수명에 따라 수정 필요)
|
||||
// $result = sms_send($to_phone, $content);
|
||||
consultant_log("[SMS Sent] To: {$to_phone}, Content: {$content}");
|
||||
return true;
|
||||
}
|
||||
consultant_log("[SMS Error] sms_admin/sms.lib.php not found.", 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
if (!defined('_GNUBOARD_')) exit;
|
||||
|
||||
/**
|
||||
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
|
||||
*/
|
||||
class SchemaManager
|
||||
{
|
||||
private $sql_file_path;
|
||||
private $results;
|
||||
private $conn; // DB 연결 객체를 저장할 변수
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
* @param string $sql_file_path install.sql 파일의 절대 경로
|
||||
*/
|
||||
public function __construct($sql_file_path)
|
||||
{
|
||||
global $g5; // 그누보드 DB 연결 객체에 접근하기 위해 global 선언
|
||||
$this->conn = $g5['connect_db']; // DB 연결 객체를 저장
|
||||
|
||||
if (!file_exists($sql_file_path)) {
|
||||
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
|
||||
}
|
||||
$this->sql_file_path = $sql_file_path;
|
||||
$this->results = [
|
||||
'created' => [],
|
||||
'existing' => [],
|
||||
'updated' => [],
|
||||
'failed' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스키마 설치/업데이트를 실행합니다.
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$sql_statements = $this->parse_sql_file();
|
||||
|
||||
foreach ($sql_statements as $stmt) {
|
||||
// CREATE TABLE 문인지 확인
|
||||
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
|
||||
$schema = $this->parse_create_table_sql($stmt);
|
||||
if ($schema && !empty($schema['name'])) {
|
||||
$this->process_table_schema($stmt, $schema);
|
||||
}
|
||||
} else {
|
||||
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
|
||||
mysqli_query($this->conn, $stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 처리 결과를 반환합니다.
|
||||
* @return array
|
||||
*/
|
||||
public function get_results()
|
||||
{
|
||||
return $this->results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
|
||||
* @param string $create_sql 전체 CREATE TABLE 구문
|
||||
* @param array $schema 파싱된 스키마 정보
|
||||
*/
|
||||
private function process_table_schema($create_sql, $schema)
|
||||
{
|
||||
$table_name = $schema['name'];
|
||||
|
||||
if ($this->table_exists($table_name)) {
|
||||
// 테이블이 존재하면, 컬럼 비교 및 추가/수정
|
||||
$this->results['existing'][] = $table_name;
|
||||
$this->update_table_columns($table_name, $schema['columns']);
|
||||
} else {
|
||||
// 테이블이 존재하지 않으면, 새로 생성
|
||||
if (mysqli_query($this->conn, $create_sql)) {
|
||||
$this->results['created'][] = $table_name;
|
||||
} else {
|
||||
$this->results['failed'][] = $table_name;
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 컬럼 구조를 업데이트합니다.
|
||||
* @param string $table_name
|
||||
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
|
||||
*/
|
||||
private function update_table_columns($table_name, $target_columns)
|
||||
{
|
||||
$current_columns = $this->get_current_columns($table_name);
|
||||
$added_columns_in_table = [];
|
||||
|
||||
foreach ($target_columns as $col_name => $col_definition) {
|
||||
// 현재 테이블에 해당 컬럼이 없으면 추가
|
||||
if (!isset($current_columns[$col_name])) {
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
|
||||
if (mysqli_query($this->conn, $alter_sql)) {
|
||||
$added_columns_in_table[] = $col_name;
|
||||
} else {
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
} else {
|
||||
// 💡 [핵심 수정] 컬럼이 이미 존재하면, 코멘트 등을 업데이트하기 위해 MODIFY 실행
|
||||
$alter_sql = "ALTER TABLE `{$table_name}` MODIFY COLUMN `{$col_name}` {$col_definition}";
|
||||
if (!mysqli_query($this->conn, $alter_sql)) {
|
||||
// MODIFY 실패 시 에러 기록
|
||||
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
|
||||
$this->results['errors'][] = "<strong>{$table_name} 테이블의 '{$col_name}' 컬럼 수정 실패</strong>: " . mysqli_error($this->conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($added_columns_in_table)) {
|
||||
$this->results['updated'][$table_name] = $added_columns_in_table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 파일을 읽고 각 구문으로 분리합니다.
|
||||
* @return array
|
||||
*/
|
||||
private function parse_sql_file()
|
||||
{
|
||||
$sql = file_get_contents($this->sql_file_path);
|
||||
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
|
||||
$sql = preg_replace('/--.*/', '', $sql);
|
||||
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
|
||||
$sql = trim($sql);
|
||||
|
||||
// 세미콜론(;)을 기준으로 쿼리 분리
|
||||
return array_filter(array_map('trim', explode(';', $sql)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 존재 여부를 확인합니다.
|
||||
* @param string $table_name
|
||||
* @return bool
|
||||
*/
|
||||
private function table_exists($table_name)
|
||||
{
|
||||
$res = mysqli_query($this->conn, "SHOW TABLES LIKE '{$table_name}'");
|
||||
return mysqli_num_rows($res) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
|
||||
* @param string $table_name
|
||||
* @return array
|
||||
*/
|
||||
private function get_current_columns($table_name)
|
||||
{
|
||||
$res = mysqli_query($this->conn, "SHOW COLUMNS FROM `{$table_name}`");
|
||||
$columns = [];
|
||||
while ($row = mysqli_fetch_array($res)) {
|
||||
$columns[$row['Field']] = true;
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
|
||||
* @param string $query CREATE TABLE 구문
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_create_table_sql($query)
|
||||
{
|
||||
$table_name = '';
|
||||
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
|
||||
$table_name = $matches[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 괄호 안의 내용만 추출
|
||||
$start = strpos($query, '(');
|
||||
$end = strrpos($query, ')');
|
||||
if ($start === false || $end === false) {
|
||||
return ['name' => $table_name, 'columns' => []];
|
||||
}
|
||||
$content = substr($query, $start + 1, $end - $start - 1);
|
||||
|
||||
// 줄 단위로 분리
|
||||
$lines = explode("\n", $content);
|
||||
|
||||
$columns = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line, " \t\n\r\0\x0B,"); // 양쪽 공백과 마지막 쉼표 제거
|
||||
|
||||
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
|
||||
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
|
||||
$col_name = $match[1];
|
||||
$col_definition = $match[2];
|
||||
$columns[$col_name] = $col_definition;
|
||||
}
|
||||
}
|
||||
|
||||
return ['name' => $table_name, 'columns' => $columns];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 로그 조회 페이지
|
||||
*/
|
||||
$sub_menu = '850630';// 새로운 메뉴 코드
|
||||
include_once('./_common.php');
|
||||
|
||||
// 권한 확인
|
||||
auth_check_menu($auth, $sub_menu, 'r');
|
||||
|
||||
$g5['title'] = '시스템 로그 조회';
|
||||
|
||||
// 페이징 설정
|
||||
$page = (int)($_GET['page'] ?? 1);
|
||||
$page_rows = 20; // 페이지당 로그 수
|
||||
|
||||
$sql_common = " FROM consultant_log ";
|
||||
$sql_order = " ORDER BY log_time DESC ";
|
||||
|
||||
// 전체 로그 수
|
||||
$row = sql_fetch(" SELECT COUNT(*) AS cnt " . $sql_common);
|
||||
$total_count = $row['cnt'];
|
||||
$total_page = ceil($total_count / $page_rows);
|
||||
$from_record = ($page - 1) * $page_rows;
|
||||
|
||||
// 로그 조회
|
||||
$sql = " SELECT * " . $sql_common . $sql_order . " LIMIT {$from_record}, {$page_rows} ";
|
||||
$result = sql_query($sql);
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.log-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.log-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.log-table th, .log-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.log-table th {
|
||||
background-color: #fff;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.log-table td.log-message {
|
||||
word-break: break-all;
|
||||
}
|
||||
.log-level {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.log-level.info { background-color: #17a2b8; }
|
||||
.log-level.warning { background-color: #ffc107; color: #212529; }
|
||||
.log-level.error { background-color: #dc3545; }
|
||||
.log-table tr:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
.text-center { text-align: center !important; }
|
||||
</style>
|
||||
|
||||
<div class="log-container">
|
||||
<div class="local_ov01 local_ov">
|
||||
<span class="btn_ov01"><span class="ov_txt">전체 로그</span><span class="ov_num"> <?php echo number_format($total_count) ?>건</span></span>
|
||||
</div>
|
||||
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table class="log-table">
|
||||
<caption><?php echo $g5['title']; ?></caption>
|
||||
<colgroup>
|
||||
<col style="width: 180px;">
|
||||
<col style="width: 120px;">
|
||||
<col style="width: 100px;">
|
||||
<col>
|
||||
<col style="width: 150px;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">기록 시간</th>
|
||||
<th scope="col">관리자 ID</th>
|
||||
<th scope="col">로그 종류</th>
|
||||
<th scope="col">내용</th>
|
||||
<th scope="col">IP 주소</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($i = 0; $row = sql_fetch_array($result); $i++) {
|
||||
$log_level_class = strtolower($row['log_level']);
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center"><?php echo $row['log_time']; ?></td>
|
||||
<td class="text-center"><?php echo htmlspecialchars($row['mb_id'] ?: '비회원/시스템'); ?></td>
|
||||
<td class="text-center">
|
||||
<span class="log-level <?php echo $log_level_class; ?>">
|
||||
<?php echo ucfirst($log_level_class); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="log-message"><?php echo htmlspecialchars($row['log_message']); ?></td>
|
||||
<td class="text-center"><?php echo $row['ip_address']; ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
if ($i == 0) {
|
||||
echo '<tr><td colspan="5" class="empty_table">데이터가 없습니다.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['PHP_SELF'].'?'.$qstr.'&page='); ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-
|
||||
@@ -0,0 +1,500 @@
|
||||
<?php
|
||||
/**
|
||||
* 예약 관리
|
||||
*/
|
||||
$sub_menu = '850200';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '예약 관리';
|
||||
|
||||
// 필터 파라미터
|
||||
$status = $_GET['status'] ?? '';
|
||||
$date = $_GET['date'] ?? '';
|
||||
$search = $_GET['search'] ?? '';
|
||||
$page = (int)($_GET['page'] ?? 1);
|
||||
$per_page = 20;
|
||||
|
||||
// 상태 변경 처리
|
||||
if ($_POST['action'] == 'update_status') {
|
||||
$reservation_id = (int)$_POST['reservation_id'];
|
||||
$new_status = $_POST['new_status'];
|
||||
$memo = $_POST['memo'] ?? '';
|
||||
$send_sms = isset($_POST['send_sms']) && $_POST['send_sms'] == '1';
|
||||
$send_email = isset($_POST['send_email']) && $_POST['send_email'] == '1';
|
||||
|
||||
if ($reservation_id && $new_status) {
|
||||
// 기존 상태 조회
|
||||
$old_res = sql_fetch("SELECT * FROM consultant_reservations WHERE id = {$reservation_id}");
|
||||
|
||||
$sql = "UPDATE consultant_reservations
|
||||
SET status = '" . sql_real_escape_string($new_status) . "',
|
||||
admin_memo = '" . sql_real_escape_string($memo) . "',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$reservation_id}";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
// 💡 [추가] 알림 발송 로직
|
||||
if ($send_sms || $send_email) {
|
||||
// 템플릿 키 결정
|
||||
$template_key = '';
|
||||
if ($new_status == 'reserved') {
|
||||
$template_key = 'consultant_confirmed_customer';
|
||||
} elseif ($new_status == 'cancelled') {
|
||||
$template_key = 'consultant_cancelled_customer';
|
||||
}
|
||||
|
||||
if ($template_key) {
|
||||
// 알림 데이터 준비
|
||||
$noti_data = [
|
||||
'customer_name' => $old_res['customer_name'],
|
||||
'customer_phone' => $old_res['customer_phone'],
|
||||
'customer_email' => $old_res['customer_email'],
|
||||
'reservation_date' => $old_res['reservation_date'],
|
||||
'reservation_time' => substr($old_res['reservation_time'], 0, 5),
|
||||
'payment_amount' => number_format($old_res['payment_amount']),
|
||||
'cancel_reason' => $memo // 취소 사유로 메모 사용
|
||||
];
|
||||
|
||||
if ($send_sms) {
|
||||
consultant_send_notification('sms', $template_key, $noti_data);
|
||||
}
|
||||
if ($send_email) {
|
||||
consultant_send_notification('email', $template_key, $noti_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alert('상태가 변경되었습니다.', $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING']);
|
||||
} else {
|
||||
alert('상태 변경에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 조건 구성
|
||||
$where_conditions = ["is_deleted = 0"];
|
||||
|
||||
if ($status) {
|
||||
$where_conditions[] = "status = '" . sql_real_escape_string($status) . "'";
|
||||
}
|
||||
|
||||
if ($date) {
|
||||
$where_conditions[] = "reservation_date = '" . sql_real_escape_string($date) . "'";
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search_escaped = sql_real_escape_string($search);
|
||||
$where_conditions[] = "(customer_name LIKE '%{$search_escaped}%' OR customer_phone LIKE '%{$search_escaped}%')";
|
||||
}
|
||||
|
||||
$where_clause = implode(' AND ', $where_conditions);
|
||||
|
||||
// 전체 개수 조회
|
||||
$count_sql = "SELECT COUNT(*) as total FROM consultant_reservations WHERE {$where_clause}";
|
||||
$count_result = sql_fetch($count_sql);
|
||||
$total = $count_result['total'];
|
||||
|
||||
// 페이징 계산
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$total_pages = ceil($total / $per_page);
|
||||
|
||||
// 예약 목록 조회
|
||||
$sql = "SELECT * FROM consultant_reservations
|
||||
WHERE {$where_clause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT {$offset}, {$per_page}";
|
||||
|
||||
$reservations = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$reservations[] = $row;
|
||||
}
|
||||
|
||||
// 상태 라벨
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.reservations-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reservations-table {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-payment_pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-reserved {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-success { background: #28a745; color: white; }
|
||||
.btn-warning { background: #ffc107; color: #212529; }
|
||||
.btn-danger { background: #dc3545; color: white; }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
.btn-info { background: #17a2b8; color: white; } /* 견적작성 버튼용 */
|
||||
|
||||
.form-control {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination .current {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="reservations-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<!-- 필터 폼 -->
|
||||
<form method="get" class="filter-form">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">전체 상태</option>
|
||||
<?php foreach ($status_labels as $key => $label): ?>
|
||||
<option value="<?php echo $key; ?>" <?php echo $status == $key ? 'selected' : ''; ?>>
|
||||
<?php echo $label; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<input type="date" name="date" value="<?php echo $date; ?>" class="form-control" placeholder="예약일">
|
||||
|
||||
<input type="text" name="search" value="<?php echo htmlspecialchars($search); ?>"
|
||||
class="form-control" placeholder="고객명 또는 연락처 검색">
|
||||
|
||||
<button type="submit" class="btn btn-primary">검색</button>
|
||||
<a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="btn btn-secondary">초기화</a>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</form>
|
||||
|
||||
<!-- 예약 목록 -->
|
||||
<div class="reservations-table">
|
||||
<div class="table-header">
|
||||
<span>예약 목록 (총 <?php echo number_format($total); ?>건)</span>
|
||||
<span>페이지 <?php echo $page; ?> / <?php echo $total_pages; ?></span>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($reservations)): ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>예약번호</th>
|
||||
<th>고객정보</th>
|
||||
<th>예약일시</th>
|
||||
<th>상담유형</th>
|
||||
<th>상담비</th>
|
||||
<th>상태</th>
|
||||
<th>신청일</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($reservations as $reservation): ?>
|
||||
<tr>
|
||||
<td>#<?php echo $reservation['id']; ?></td>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($reservation['customer_name']); ?></strong><br>
|
||||
<small><?php echo htmlspecialchars($reservation['customer_phone']); ?></small><br>
|
||||
<small><?php echo htmlspecialchars($reservation['customer_email']); ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo $reservation['reservation_date']; ?><br>
|
||||
<small><?php echo substr($reservation['reservation_time'], 0, 5); ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$types = ['onsite' => '현장', 'online' => '온라인', 'phone' => '전화'];
|
||||
echo $types[$reservation['consultation_type']] ?? $reservation['consultation_type'];
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo number_format($reservation['payment_amount']); ?>원</td>
|
||||
<td>
|
||||
<span class="status-badge status-<?php echo $reservation['status']; ?>">
|
||||
<?php echo $status_labels[$reservation['status']] ?? $reservation['status']; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo date('m-d H:i', strtotime($reservation['created_at'])); ?></td>
|
||||
<td>
|
||||
<button onclick="openStatusModal(<?php echo $reservation['id']; ?>, '<?php echo $reservation['status']; ?>')" class="btn btn-warning btn-sm">상태변경</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="no-data">
|
||||
검색 조건에 맞는 예약이 없습니다.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 페이징 -->
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page - 1])); ?>">이전</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$start_page = max(1, $page - 5);
|
||||
$end_page = min($total_pages, $page + 5);
|
||||
|
||||
for ($i = $start_page; $i <= $end_page; $i++):
|
||||
?>
|
||||
<?php if ($i == $page): ?>
|
||||
<span class="current"><?php echo $i; ?></span>
|
||||
<?php else: ?>
|
||||
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $i])); ?>"><?php echo $i; ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page + 1])); ?>">다음</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 상태 변경 모달 -->
|
||||
<div id="statusModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h3>예약 상태 변경</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="update_status">
|
||||
<input type="hidden" name="reservation_id" id="modal_reservation_id">
|
||||
|
||||
<div style="margin: 15px 0;">
|
||||
<label for="modal_new_status">변경할 상태</label>
|
||||
<select name="new_status" id="modal_new_status" class="form-control" required>
|
||||
<?php foreach ($status_labels as $key => $label): ?>
|
||||
<option value="<?php echo $key; ?>"><?php echo $label; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin: 15px 0;">
|
||||
<label for="modal_memo">관리자 메모 (선택)</label>
|
||||
<textarea name="memo" rows="3" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [추가] 알림 발송 옵션 -->
|
||||
<div style="margin: 15px 0; background: #f8f9fa; padding: 10px; border-radius: 4px;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">알림 발송</div>
|
||||
<label style="margin-right: 15px;">
|
||||
<input type="checkbox" name="send_sms" value="1" checked> 문자(SMS) 발송
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="send_email" value="1" checked> 이메일 발송
|
||||
</label>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||
* 예약확정/취소 상태 변경 시에만 발송됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-primary">확인</button>
|
||||
<button type="button" onclick="closeModal()" class="btn btn-secondary">취소</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openStatusModal(reservationId, currentStatus) {
|
||||
document.getElementById('modal_reservation_id').value = reservationId;
|
||||
document.getElementById('modal_new_status').value = currentStatus;
|
||||
document.getElementById('statusModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('statusModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 모달 외부 클릭시 닫기
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('statusModal');
|
||||
if (event.target == modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 닫기 버튼
|
||||
document.querySelector('.close').onclick = function() {
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 리소스(상담사/공간) 관리
|
||||
*/
|
||||
$sub_menu = '850615';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
$g5['title'] = '리소스(상담사) 관리';
|
||||
|
||||
// --- 액션 처리 ---
|
||||
|
||||
// 그룹 추가/수정
|
||||
if ($_POST['action'] == 'save_group') {
|
||||
$group_name = trim($_POST['group_name']);
|
||||
$group_id = (int)$_POST['group_id'];
|
||||
|
||||
if ($group_name) {
|
||||
if ($group_id > 0) {
|
||||
$sql = "UPDATE consultant_groups SET name = '{$group_name}' WHERE id = {$group_id}";
|
||||
} else {
|
||||
$sql = "INSERT INTO consultant_groups (name, is_active) VALUES ('{$group_name}', 1)";
|
||||
}
|
||||
sql_query($sql);
|
||||
alert('저장되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// 그룹 삭제
|
||||
if ($_POST['action'] == 'delete_group') {
|
||||
$group_id = (int)$_POST['group_id'];
|
||||
// 리소스가 있는지 확인
|
||||
$cnt = sql_fetch("SELECT COUNT(*) as cnt FROM consultant_resources WHERE group_id = {$group_id}")['cnt'];
|
||||
if ($cnt > 0) {
|
||||
alert('해당 그룹에 속한 리소스가 있어 삭제할 수 없습니다.');
|
||||
} else {
|
||||
sql_query("DELETE FROM consultant_groups WHERE id = {$group_id}");
|
||||
alert('삭제되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// 리소스 추가/수정
|
||||
if ($_POST['action'] == 'save_resource') {
|
||||
$resource_id = (int)$_POST['resource_id'];
|
||||
$group_id = (int)$_POST['group_id'];
|
||||
$name = trim($_POST['resource_name']);
|
||||
$desc = trim($_POST['resource_desc']);
|
||||
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($name && $group_id) {
|
||||
if ($resource_id > 0) {
|
||||
$sql = "UPDATE consultant_resources
|
||||
SET group_id = {$group_id}, name = '{$name}', description = '{$desc}', is_active = {$is_active}
|
||||
WHERE id = {$resource_id}";
|
||||
} else {
|
||||
$sql = "INSERT INTO consultant_resources
|
||||
(group_id, name, description, is_active)
|
||||
VALUES ({$group_id}, '{$name}', '{$desc}', {$is_active})";
|
||||
}
|
||||
sql_query($sql);
|
||||
alert('저장되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// 리소스 삭제
|
||||
if ($_POST['action'] == 'delete_resource') {
|
||||
$resource_id = (int)$_POST['resource_id'];
|
||||
// 예약 내역 확인 (안전 삭제)
|
||||
$cnt = sql_fetch("SELECT COUNT(*) as cnt FROM consultant_reservations WHERE resource_id = {$resource_id}")['cnt'];
|
||||
if ($cnt > 0) {
|
||||
// 실제 삭제 대신 비활성화 처리 권장하지만, 여기서는 경고 후 삭제 방지
|
||||
alert('해당 리소스로 접수된 예약이 있어 삭제할 수 없습니다. 대신 사용 안 함으로 설정해주세요.');
|
||||
} else {
|
||||
sql_query("DELETE FROM consultant_resources WHERE id = {$resource_id}");
|
||||
alert('삭제되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 데이터 조회 ---
|
||||
$groups = [];
|
||||
$result = sql_query("SELECT * FROM consultant_groups ORDER BY id");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$groups[$row['id']] = $row;
|
||||
}
|
||||
|
||||
$resources = [];
|
||||
$result = sql_query("SELECT r.*, g.name as group_name
|
||||
FROM consultant_resources r
|
||||
LEFT JOIN consultant_groups g ON r.group_id = g.id
|
||||
ORDER BY r.group_id, r.id");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$resources[] = $row;
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.resource-container { max-width: 1200px; margin: 0 auto; padding: 20px; display: grid; grid-template-columns: 300px 1fr; gap: 20px; }
|
||||
.card { background: #fff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
|
||||
.card-header { padding: 15px; background: #f8f9fa; border-bottom: 1px solid #ddd; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-body { padding: 20px; }
|
||||
|
||||
.group-list { list-style: none; padding: 0; margin: 0; }
|
||||
.group-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
||||
.group-item:last-child { border-bottom: none; }
|
||||
.group-item:hover { background: #f1f3f5; }
|
||||
|
||||
.resource-table { width: 100%; border-collapse: collapse; }
|
||||
.resource-table th, .resource-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
.resource-table th { background: #f8f9fa; font-weight: bold; }
|
||||
|
||||
.btn-xs { padding: 2px 6px; font-size: 11px; border-radius: 3px; }
|
||||
.badge { padding: 3px 8px; border-radius: 10px; font-size: 11px; color: #fff; }
|
||||
.badge-success { background: #28a745; }
|
||||
.badge-secondary { background: #6c757d; }
|
||||
|
||||
.form-control { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.resource-container { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="resource-container">
|
||||
<!-- 좌측: 그룹 관리 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>그룹 관리</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="openGroupModal()">+ 추가</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<ul class="group-list">
|
||||
<?php if (empty($groups)): ?>
|
||||
<li class="group-item" style="justify-content:center; color:#999;">등록된 그룹이 없습니다.</li>
|
||||
<?php else: ?>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<li class="group-item">
|
||||
<span><?php echo htmlspecialchars($group['name']); ?></span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary btn-xs" onclick="openGroupModal(<?php echo $group['id']; ?>, '<?php echo htmlspecialchars($group['name']); ?>')">수정</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" onclick="deleteGroup(<?php echo $group['id']; ?>)">삭제</button>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 리소스 목록 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>리소스(상담사/공간) 목록</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="openResourceModal()">+ 리소스 추가</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<table class="resource-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>그룹</th>
|
||||
<th>이름</th>
|
||||
<th>설명</th>
|
||||
<th>상태</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($resources)): ?>
|
||||
<tr><td colspan="5" style="text-align:center; padding:30px; color:#999;">등록된 리소스가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($resources as $res): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($res['group_name']); ?></td>
|
||||
<td><strong><?php echo htmlspecialchars($res['name']); ?></strong></td>
|
||||
<td><?php echo htmlspecialchars($res['description']); ?></td>
|
||||
<td>
|
||||
<?php if ($res['is_active']): ?>
|
||||
<span class="badge badge-success">사용중</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">중지</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-secondary btn-xs"
|
||||
onclick="openResourceModal(<?php echo $res['id']; ?>, <?php echo $res['group_id']; ?>, '<?php echo htmlspecialchars($res['name']); ?>', '<?php echo htmlspecialchars($res['description']); ?>', <?php echo $res['is_active']; ?>)">
|
||||
수정
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" onclick="deleteResource(<?php echo $res['id']; ?>)">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 그룹 추가/수정 모달 -->
|
||||
<div id="groupModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="width:350px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="groupModalTitle">그룹 추가</h3>
|
||||
<span class="close" onclick="closeModal('groupModal')">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="save_group">
|
||||
<input type="hidden" name="group_id" id="modal_group_id" value="0">
|
||||
<div class="form-group">
|
||||
<label>그룹명 (예: 내과, 회의실)</label>
|
||||
<input type="text" name="group_name" id="modal_group_name" class="form-control" required>
|
||||
</div>
|
||||
<div style="text-align:right; margin-top:15px;">
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 리소스 추가/수정 모달 -->
|
||||
<div id="resourceModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="resourceModalTitle">리소스 추가</h3>
|
||||
<span class="close" onclick="closeModal('resourceModal')">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="save_resource">
|
||||
<input type="hidden" name="resource_id" id="modal_res_id" value="0">
|
||||
|
||||
<div class="form-group">
|
||||
<label>그룹 선택</label>
|
||||
<select name="group_id" id="modal_res_group" class="form-control" required>
|
||||
<option value="">선택하세요</option>
|
||||
<?php foreach ($groups as $group): ?>
|
||||
<option value="<?php echo $group['id']; ?>"><?php echo htmlspecialchars($group['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>이름 (예: 김의사, A회의실)</label>
|
||||
<input type="text" name="resource_name" id="modal_res_name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>설명 (선택)</label>
|
||||
<input type="text" name="resource_desc" id="modal_res_desc" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" name="is_active" id="modal_res_active" value="1" checked> 사용함</label>
|
||||
</div>
|
||||
|
||||
<div style="text-align:right; margin-top:15px;">
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 삭제 폼 (히든) -->
|
||||
<form id="deleteForm" method="post">
|
||||
<input type="hidden" name="action" id="deleteAction">
|
||||
<input type="hidden" name="group_id" id="deleteGroupId">
|
||||
<input type="hidden" name="resource_id" id="deleteResourceId">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function openGroupModal(id = 0, name = '') {
|
||||
document.getElementById('groupModalTitle').textContent = id ? '그룹 수정' : '그룹 추가';
|
||||
document.getElementById('modal_group_id').value = id;
|
||||
document.getElementById('modal_group_name').value = name;
|
||||
document.getElementById('groupModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function openResourceModal(id = 0, groupId = '', name = '', desc = '', active = 1) {
|
||||
document.getElementById('resourceModalTitle').textContent = id ? '리소스 수정' : '리소스 추가';
|
||||
document.getElementById('modal_res_id').value = id;
|
||||
document.getElementById('modal_res_group').value = groupId;
|
||||
document.getElementById('modal_res_name').value = name;
|
||||
document.getElementById('modal_res_desc').value = desc;
|
||||
document.getElementById('modal_res_active').checked = (active == 1);
|
||||
document.getElementById('resourceModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
}
|
||||
|
||||
function deleteGroup(id) {
|
||||
if(confirm('정말 삭제하시겠습니까?')) {
|
||||
document.getElementById('deleteAction').value = 'delete_group';
|
||||
document.getElementById('deleteGroupId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteResource(id) {
|
||||
if(confirm('정말 삭제하시겠습니까?')) {
|
||||
document.getElementById('deleteAction').value = 'delete_resource';
|
||||
document.getElementById('deleteResourceId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 닫기
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
$sub_menu = '850500'; // 💡 메뉴 코드 수정
|
||||
include_once('./_common.php');
|
||||
|
||||
$g5['title'] = '상담 예약 팝업 샘플';
|
||||
include_once(G5_ADMIN_PATH.'/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="local_desc01 local_desc">
|
||||
<p>
|
||||
이 페이지는 사이트의 어떤 페이지에서든 상담 예약 기능을 쉽게 추가하는 방법을 보여주는 예제입니다.<br>
|
||||
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
|
||||
</p>
|
||||
<pre><code><?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?></code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [시작] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
|
||||
<h3 style="margin-bottom:15px;">상담이 필요하신가요?</h3>
|
||||
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 상담을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
|
||||
|
||||
<!-- 1. 팝업을 여는 버튼들 -->
|
||||
<div class="consultant-buttons">
|
||||
<button type="button" class="reservation-btn-main" onclick="openReservationPopup()">
|
||||
<i class="fa fa-calendar"></i> 상담 예약 신청
|
||||
</button>
|
||||
<button type="button" class="reservation-check-btn" onclick="openReservationCheckPopup()">
|
||||
예약 확인/취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
|
||||
include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php');
|
||||
?>
|
||||
|
||||
<!-- 버튼 디자인을 위한 CSS (사이트의 공통 CSS 파일에 추가하는 것을 권장합니다) -->
|
||||
<style>
|
||||
.consultant-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.reservation-btn-main, .reservation-check-btn {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.reservation-btn-main {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
.reservation-btn-main:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
.reservation-btn-main i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.reservation-check-btn {
|
||||
background: #6c757d;
|
||||
}
|
||||
.reservation-check-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- 💡 [끝] 상담 예약 기능 추가 예제 -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH.'/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 일정 설정
|
||||
*/
|
||||
$sub_menu = '850300';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 일정 설정';
|
||||
|
||||
// 월별 스케줄 생성 처리
|
||||
if ($_POST['action'] == 'generate_monthly_schedule') {
|
||||
try {
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
$year = (int)($_POST['year'] ?? date('Y'));
|
||||
$month = (int)($_POST['month'] ?? date('n'));
|
||||
|
||||
// 충돌 검사
|
||||
$conflicts = $generator->checkScheduleConflicts($year, $month);
|
||||
if (!empty($conflicts)) {
|
||||
$conflictMsg = "다음 예약과 충돌이 발생합니다:\\n";
|
||||
foreach ($conflicts as $conflict) {
|
||||
$conflictMsg .= "- {$conflict['date']} {$conflict['time']} {$conflict['customer']} ({$conflict['phone']})\\n";
|
||||
}
|
||||
$conflictMsg .= "\\n계속 진행하시겠습니까?";
|
||||
|
||||
if (!isset($_POST['force_generate'])) {
|
||||
echo "<script>
|
||||
if (confirm('{$conflictMsg}')) {
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = '<input type=\"hidden\" name=\"action\" value=\"generate_monthly_schedule\">' +
|
||||
'<input type=\"hidden\" name=\"year\" value=\"{$year}\">' +
|
||||
'<input type=\"hidden\" name=\"month\" value=\"{$month}\">' +
|
||||
'<input type=\"hidden\" name=\"force_generate\" value=\"1\">';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 스케줄 생성
|
||||
if ($generator->generateMonth($year, $month)) {
|
||||
alert("{$year}년 {$month}월 스케줄이 생성되었습니다.", $_SERVER['PHP_SELF']);
|
||||
} else {
|
||||
alert("스케줄 생성에 실패했습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert("오류가 발생했습니다: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 폼 처리 (기존 코드 유지)
|
||||
if ($_POST['action'] == 'save_schedule') {
|
||||
// 기존 스케줄 삭제
|
||||
sql_query("DELETE FROM consultant_schedule WHERE day_of_week IS NOT NULL");
|
||||
|
||||
// 새 스케줄 저장
|
||||
for ($day = 1; $day <= 7; $day++) {
|
||||
$enabled = $_POST["day_{$day}_enabled"] ?? 0;
|
||||
$start_time = $_POST["day_{$day}_start"] ?? '09:00';
|
||||
$end_time = $_POST["day_{$day}_end"] ?? '18:00';
|
||||
|
||||
if ($enabled) {
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(day_of_week, start_time, end_time, time_slot, max_persons, is_available)
|
||||
VALUES ({$day}, '{$start_time}', '{$end_time}', 60, 2, 1)";
|
||||
sql_query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
alert('일정이 저장되었습니다.', $_SERVER['PHP_SELF']);
|
||||
}
|
||||
|
||||
// 현재 스케줄 조회
|
||||
$schedules = [];
|
||||
$sql = "SELECT * FROM consultant_schedule WHERE day_of_week IS NOT NULL ORDER BY day_of_week";
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$schedules[$row['day_of_week']] = $row;
|
||||
}
|
||||
|
||||
$days = ['', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.schedule-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.schedule-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #ddd;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 16px 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monthly-generator {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.generator-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #856404;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
.schedule-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.schedule-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
}
|
||||
.day-schedule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.day-schedule:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.day-name {
|
||||
width: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.day-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
.time-input {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 100px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="schedule-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>안내:</strong> 상담 가능한 요일과 시간을 설정하세요. 체크된 요일만 예약이 가능합니다.
|
||||
</div>
|
||||
|
||||
<form method="post" class="schedule-form">
|
||||
<input type="hidden" name="action" value="save_schedule">
|
||||
|
||||
<?php for ($day = 1; $day <= 7; $day++): ?>
|
||||
<?php
|
||||
$schedule = $schedules[$day] ?? null;
|
||||
$enabled = $schedule ? 1 : 0;
|
||||
$start_time = $schedule['start_time'] ?? '09:00';
|
||||
$end_time = $schedule['end_time'] ?? '18:00';
|
||||
?>
|
||||
|
||||
<div class="day-schedule">
|
||||
<div class="day-name"><?php echo $days[$day]; ?></div>
|
||||
<div class="day-controls">
|
||||
<label>
|
||||
<input type="checkbox" name="day_<?php echo $day; ?>_enabled" value="1"
|
||||
<?php echo $enabled ? 'checked' : ''; ?>>
|
||||
운영
|
||||
</label>
|
||||
|
||||
<label>
|
||||
시작:
|
||||
<input type="time" name="day_<?php echo $day; ?>_start"
|
||||
value="<?php echo $start_time; ?>" class="time-input">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
종료:
|
||||
<input type="time" name="day_<?php echo $day; ?>_end"
|
||||
value="<?php echo $end_time; ?>" class="time-input">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
/**
|
||||
* 스케줄 생성 및 빠른 관리 페이지
|
||||
*/
|
||||
|
||||
$sub_menu = '850300';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 권한 확인
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
// ScheduleGenerator 클래스 로드
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '스케줄 빠른 관리';
|
||||
|
||||
// AJAX 요청 처리
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
|
||||
if ($action) {
|
||||
header('Content-Type: application/json');
|
||||
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
|
||||
|
||||
// 월별 스케줄 데이터 조회 (달력용)
|
||||
if ($action == 'get_monthly_schedule') {
|
||||
$year = (int) ($_GET['year'] ?? 0);
|
||||
$month = (int) ($_GET['month'] ?? 0);
|
||||
|
||||
if ($year && $month) {
|
||||
$sql = "SELECT id, specific_date, start_time, is_available, temp_1, temp_2
|
||||
FROM consultant_schedule
|
||||
WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}
|
||||
ORDER BY specific_date, start_time";
|
||||
$result = sql_query($sql);
|
||||
$schedule_data = [];
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$schedule_data[] = $row;
|
||||
}
|
||||
$response = ['success' => true, 'data' => $schedule_data];
|
||||
} else {
|
||||
$response['message'] = '년도와 월 정보가 올바르지 않습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
// 스케줄 슬롯 상태 변경 (블락/해제)
|
||||
if ($action == 'toggle_slot_status') {
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if ($id) {
|
||||
$slot = sql_fetch("SELECT is_available, temp_1, temp_2 FROM consultant_schedule WHERE id = {$id}");
|
||||
if ($slot) {
|
||||
$is_lunch_override = $_POST['is_lunch_override'] ?? '0';
|
||||
$new_status = $slot['is_available'] ? 0 : 1; // 기본 토글 동작
|
||||
$new_temp1 = $slot['temp_1'];
|
||||
$new_temp2 = $slot['temp_2'];
|
||||
$log_msg_action = '';
|
||||
|
||||
// 💡 [로직 개선] 휴게시간을 상담시간으로 변경하는 경우
|
||||
if ($is_lunch_override === '1' && $slot['temp_1'] === 'lunch_time' && $new_status == 1) {
|
||||
$new_temp1 = 'manual_override'; // 휴게시간을 수동으로 변경했음을 명시
|
||||
$new_temp2 = '관리자 긴급 설정';
|
||||
$log_msg_action = "휴게시간을 상담 슬롯으로 변경";
|
||||
}
|
||||
// 💡 [로직 개선] 긴급 설정된 상담시간을 다시 휴게시간으로 되돌리는 경우
|
||||
else if ($slot['temp_1'] === 'manual_override' && $new_status == 0) {
|
||||
$new_temp1 = 'lunch_time';
|
||||
$new_temp2 = '점심시간';
|
||||
$log_msg_action = "긴급 슬롯을 다시 휴게시간으로 복원";
|
||||
}
|
||||
// 일반 슬롯을 블락/해제하는 경우
|
||||
else {
|
||||
$new_temp1 = $new_status ? 'auto_generated' : 'manual_block';
|
||||
$new_temp2 = $new_status ? '' : '관리자 설정';
|
||||
$log_msg_action = $new_status ? "슬롯 활성화" : "슬롯 비활성화";
|
||||
}
|
||||
|
||||
$sql = "UPDATE consultant_schedule
|
||||
SET is_available = '{$new_status}',
|
||||
temp_1 = '{$new_temp1}',
|
||||
temp_2 = '{$new_temp2}',
|
||||
updated_at = NOW()
|
||||
WHERE id = {$id}";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
consultant_log("스케줄 수동 변경 (ID:{$id}): {$log_msg_action} (관리자: " . ($member['mb_id'] ?? 'unknown') . ")");
|
||||
$response = ['success' => true, 'new_status' => $new_status];
|
||||
} else {
|
||||
$response['message'] = '데이터베이스 업데이트에 실패했습니다.';
|
||||
}
|
||||
} else {
|
||||
$response['message'] = '해당 스케줄을 찾을 수 없습니다.';
|
||||
}
|
||||
} else {
|
||||
$response['message'] = 'ID가 제공되지 않았습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 스케줄 생성
|
||||
if ($action == 'generate_schedule') {
|
||||
$year = (int) ($_POST['year'] ?? 0);
|
||||
$month = (int) ($_POST['month'] ?? 0);
|
||||
if ($year && $month) {
|
||||
try {
|
||||
$generator = new ScheduleGenerator();
|
||||
if ($generator->generateMonth($year, $month)) {
|
||||
consultant_log("스케줄 생성/재생성 완료: {$year}년 {$month}월");
|
||||
$response = ['success' => true, 'message' => "{$year}년 {$month}월 스케줄이 성공적으로 생성되었습니다."];
|
||||
} else {
|
||||
$response['message'] = "{$year}년 {$month}월 스케줄 생성에 실패했습니다.";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$response['message'] = '스케줄 생성 중 오류 발생: ' . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$response['message'] = '년도와 월 정보가 올바르지 않습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- 페이지 로드 시 실행 ---
|
||||
|
||||
// 월별 스케줄 상태 조회 함수
|
||||
function get_schedule_generation_status($year, $month) {
|
||||
$year = (int)$year;
|
||||
$month = (int)$month;
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total_slots,
|
||||
SUM(CASE WHEN temp_1 = 'auto_generated' AND is_available = 1 THEN 1 ELSE 0 END) as available_slots,
|
||||
SUM(CASE WHEN temp_1 = 'lunch_time' THEN 1 ELSE 0 END) as lunch_slots,
|
||||
SUM(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as holiday_slots,
|
||||
SUM(CASE WHEN is_available = 0 AND temp_1 NOT IN ('lunch_time', 'holiday') THEN 1 ELSE 0 END) as blocked_slots
|
||||
FROM consultant_schedule
|
||||
WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}";
|
||||
return sql_fetch($sql);
|
||||
}
|
||||
|
||||
// 다음 3개월 상태 조회
|
||||
$next_months = [];
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$target_date = mktime(0, 0, 0, date('n') + $i, 1, date('Y'));
|
||||
$year = date('Y', $target_date);
|
||||
$month = date('m', $target_date);
|
||||
|
||||
$next_months[] = [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'name' => date('Y년 n월', $target_date),
|
||||
'status' => get_schedule_generation_status($year, $month)
|
||||
];
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.schedule-container { max-width: 1000px; margin: 0 auto; padding: 20px; }
|
||||
.schedule-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
|
||||
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||
.status-card { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
|
||||
.status-card h3 { margin: 0 0 15px 0; color: #333; display: flex; align-items: center; gap: 8px; }
|
||||
.status-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
|
||||
.status-item:last-child { border-bottom: none; }
|
||||
.status-label { color: #666; }
|
||||
.status-value { font-weight: bold; color: #333; }
|
||||
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; text-decoration: none; display: inline-block; transition: all 0.2s; font-size: 14px; }
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-success { background: #28a745; color: white; }
|
||||
.btn-warning { background: #ffc107; color: #212529; }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
.btn-info { background: #17a2b8; color: white; }
|
||||
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
|
||||
.card-actions { display: flex; gap: 10px; margin-top: 20px; }
|
||||
.card-actions .btn { flex-grow: 1; }
|
||||
|
||||
/* Modal Styles */
|
||||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); }
|
||||
.modal-content { background-color: #fefefe; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 1200px; border-radius: 8px; }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #ddd; }
|
||||
.modal-header h2 { margin: 0; font-size: 20px; }
|
||||
.close-button { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
|
||||
.close-button:hover, .close-button:focus { color: black; }
|
||||
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-top: 20px; }
|
||||
.calendar-day { border: 1px solid #eee; min-height: 150px; }
|
||||
.calendar-day-header { background: #f9f9f9; padding: 5px; font-weight: bold; text-align: center; font-size: 14px; }
|
||||
.calendar-day-body { padding: 5px; max-height: 300px; overflow-y: auto; }
|
||||
.day-name-header { background: #f1f1f1; text-align: center; padding: 8px; font-weight: bold; }
|
||||
.time-slot { padding: 4px 6px; margin: 3px 0; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid transparent; }
|
||||
.time-slot.available { background-color: #e7f3ff; border-color: #b3d7ff; color: #004085; }
|
||||
.time-slot.available:hover { background-color: #cce5ff; }
|
||||
.time-slot.blocked { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; text-decoration: line-through; }
|
||||
.time-slot.blocked:hover { background-color: #f5c6cb; }
|
||||
.time-slot.lunch { background-color: #fff3cd; border-color: #ffeeba; color: #856404; cursor: pointer; }
|
||||
.time-slot.lunch:hover { background-color: #ffeeba; }
|
||||
.time-slot.holiday { background-color: #e2e3e5; color: #383d41; text-align: center; justify-content: center; cursor: not-allowed; }
|
||||
.other-month { background-color: #fafafa; }
|
||||
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 20px auto; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
|
||||
<div class="schedule-container">
|
||||
<div class="schedule-header">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
<div>
|
||||
<a href="settings.php" class="btn btn-secondary">⚙️ 설정 관리</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>스케줄 관리 안내:</strong> 월별 스케줄 생성 상태를 확인하고, '달력 보기'를 통해 각 시간 슬롯을 빠르게 예약 마감 처리할 수 있습니다.
|
||||
</div>
|
||||
|
||||
<!-- 월별 상태 표시 -->
|
||||
<div class="status-grid">
|
||||
<?php foreach ($next_months as $month_info):
|
||||
$is_generated = ($month_info['status']['total_slots'] ?? 0) > 0;
|
||||
?>
|
||||
<div class="status-card">
|
||||
<h3>
|
||||
📅 <?php echo $month_info['name']; ?>
|
||||
<?php if ($is_generated): ?>
|
||||
<span style="color: #28a745; font-weight: bold;">✓ 생성됨</span>
|
||||
<?php else: ?>
|
||||
<span style="color: #dc3545; font-weight: bold;">✗ 미생성</span>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
|
||||
<div class="status-item">
|
||||
<span class="status-label">상담 가능 슬롯</span>
|
||||
<span class="status-value"><?php echo number_format($month_info['status']['available_slots'] ?? 0); ?>개</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">예약 마감 슬롯</span>
|
||||
<span class="status-value"><?php echo number_format($month_info['status']['blocked_slots'] ?? 0); ?>개</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">전체 슬롯 (휴무/점심 제외)</span>
|
||||
<span class="status-value"><?php echo number_format(($month_info['status']['available_slots'] ?? 0) + ($month_info['status']['blocked_slots'] ?? 0)); ?>개</span>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button onclick="generateSchedule(<?php echo $month_info['year']; ?>, <?php echo $month_info['month']; ?>,this)" class="btn btn-success">
|
||||
<?php echo $is_generated ? '🔄 스케줄 재생성' : '✨ 스케줄 생성'; ?>
|
||||
</button>
|
||||
<?php if ($is_generated): ?>
|
||||
<button onclick="openCalendarModal(<?php echo $month_info['year']; ?>, <?php echo $month_info['month']; ?>)" class="btn btn-info">
|
||||
달력 보기
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal -->
|
||||
<div id="calendarModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle"></h2>
|
||||
<span class="close-button">×</span>
|
||||
</div>
|
||||
<div id="modalBody">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- Modal Control ---
|
||||
const modal = document.getElementById("calendarModal");
|
||||
const closeButton = document.querySelector(".close-button");
|
||||
if(closeButton) closeButton.onclick = () => modal.style.display = "none";
|
||||
window.onclick = (event) => {
|
||||
if (event.target == modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
async function openCalendarModal(year, month) {
|
||||
document.getElementById("modalTitle").innerText = `${year}년 ${month}월 스케줄`;
|
||||
const modalBody = document.getElementById("modalBody");
|
||||
modalBody.innerHTML = '<div class="spinner"></div>';
|
||||
modal.style.display = "block";
|
||||
|
||||
try {
|
||||
const response = await fetch(`?action=get_monthly_schedule&year=${year}&month=${month}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderCalendar(year, month, result.data);
|
||||
} else {
|
||||
modalBody.innerHTML = `<p style="color: red;">스케줄을 불러오는 데 실패했습니다: ${result.message}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
modalBody.innerHTML = `<p style="color: red;">오류가 발생했습니다: ${error}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCalendar(year, month, scheduleData) {
|
||||
const modalBody = document.getElementById("modalBody");
|
||||
const firstDay = new Date(year, month - 1, 1).getDay(); // 0=일, 1=월, ...
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
let html = '<div class="calendar-grid">';
|
||||
dayNames.forEach(name => html += `<div class="day-name-header">${name}</div>`);
|
||||
|
||||
// Group data by date
|
||||
const scheduleByDate = scheduleData.reduce((acc, slot) => {
|
||||
const day = new Date(slot.specific_date).getDate();
|
||||
if (!acc[day]) acc[day] = [];
|
||||
acc[day].push(slot);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Pad start of month
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
html += '<div class="calendar-day other-month"></div>';
|
||||
}
|
||||
|
||||
// Render days
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
html += `<div class="calendar-day">
|
||||
<div class="calendar-day-header">${day}</div>
|
||||
<div class="calendar-day-body">`;
|
||||
|
||||
if (scheduleByDate[day]) {
|
||||
scheduleByDate[day].forEach(slot => {
|
||||
let slotClass = '';
|
||||
let slotText = `<span>${slot.start_time.substring(0, 5)}</span>`;
|
||||
if (slot.temp_1 === 'holiday') {
|
||||
slotClass = 'holiday';
|
||||
slotText = `<strong>${slot.temp_2 || '휴무일'}</strong>`;
|
||||
} else if (slot.temp_1 === 'lunch_time' || slot.temp_1 === 'manual_override') {
|
||||
slotText = `<span>${slot.start_time.substring(0, 5)} (휴게)</span>`;
|
||||
slotClass = (slot.is_available == '1') ? 'available' : 'lunch';
|
||||
} else if (slot.is_available == '1') {
|
||||
slotClass = 'available';
|
||||
} else {
|
||||
slotClass = 'blocked';
|
||||
}
|
||||
html += `<div class="time-slot ${slotClass}" data-id="${slot.id}" onclick="toggleSlot(this, '${slot.temp_1}')">
|
||||
${slotText}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
modalBody.innerHTML = html;
|
||||
}
|
||||
|
||||
async function toggleSlot(element, type) {
|
||||
if (type === 'holiday') return;
|
||||
|
||||
let isLunchOverride = false;
|
||||
if (type === 'lunch_time' || type === 'manual_override') {
|
||||
const slotIsAvailable = element.classList.contains('available');
|
||||
if (!slotIsAvailable) {
|
||||
if (!confirm('휴게시간입니다. 정말 이 시간에 상담을 등록하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
isLunchOverride = true;
|
||||
}
|
||||
}
|
||||
|
||||
const id = element.dataset.id;
|
||||
const originalClasses = element.className;
|
||||
element.innerHTML = '<span>처리중...</span>';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'toggle_slot_status');
|
||||
formData.append('id', id);
|
||||
if (isLunchOverride) formData.append('is_lunch_override', '1');
|
||||
|
||||
const response = await fetch('', { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Re-render the slot based on the new state
|
||||
const timeText = element.innerText.split(' ')[0];
|
||||
if (type === 'lunch_time' || type === 'manual_override') {
|
||||
if (element.classList.contains('lunch')) {
|
||||
element.className = 'time-slot available';
|
||||
} else {
|
||||
element.className = 'time-slot lunch';
|
||||
}
|
||||
element.innerHTML = `<span>${timeText} (휴게)</span>`;
|
||||
} else {
|
||||
element.className = result.new_status == 1 ? 'time-slot available' : 'time-slot blocked';
|
||||
element.innerHTML = `<span>${timeText}</span>`;
|
||||
}
|
||||
} else {
|
||||
alert('상태 변경 실패: ' + result.message);
|
||||
element.className = originalClasses;
|
||||
element.innerHTML = `<span>${element.innerText.split(' ')[0]}</span>`;
|
||||
}
|
||||
} catch (error) {
|
||||
alert('오류 발생: ' + error);
|
||||
element.className = originalClasses;
|
||||
element.innerHTML = `<span>${element.innerText.split(' ')[0]}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 [추가] 스케줄 생성/재생성 함수
|
||||
async function generateSchedule(year, month,btn) {
|
||||
// const btn = document.querySelector('.status-card button[onclick="generateSchedule("+year+","+month")"]');
|
||||
const actionText = btn.textContent.trim().includes('재생성') ? '재생성' : '생성';
|
||||
|
||||
if (!confirm(`${year}년 ${month}월 스케줄을 ${actionText}하시겠습니까?\n\n기존에 수동으로 변경한 슬롯이 있다면 초기화될 수 있습니다.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 생성 중...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'generate_schedule');
|
||||
formData.append('year', year);
|
||||
formData.append('month', month);
|
||||
|
||||
// 현재 페이지 URL('')로 AJAX 요청을 보냅니다.
|
||||
const response = await fetch('', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
alert(result.message);
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('스케줄 생성 중 오류가 발생했습니다: ' + error);
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
/**
|
||||
* 월별 스케줄 자동 생성 엔진
|
||||
*/
|
||||
|
||||
if (!defined('_GNUBOARD_'))
|
||||
exit;
|
||||
|
||||
class ScheduleGenerator
|
||||
{
|
||||
|
||||
/**
|
||||
* 특정 월의 전체 스케줄 생성
|
||||
*/
|
||||
public function generateMonth($year, $month)
|
||||
{
|
||||
try {
|
||||
// 기본 설정 조회
|
||||
$basic_settings = $this->getBasicSettings();
|
||||
|
||||
// 요일별 설정 조회
|
||||
$weekly_settings = $this->getWeeklySettings();
|
||||
|
||||
// 해당 월의 모든 날짜 생성
|
||||
$dates = $this->getMonthDates($year, $month);
|
||||
|
||||
// 기존 스케줄 삭제 (자동 생성된 것만)
|
||||
$this->clearAutoGeneratedSchedule($year, $month);
|
||||
|
||||
$generated_count = 0;
|
||||
|
||||
foreach ($dates as $date) {
|
||||
$day_of_week = date('w', strtotime($date)); // 0=일요일, 1=월요일, ...
|
||||
$day_name = $this->getDayName($day_of_week);
|
||||
|
||||
// 해당 요일의 설정 확인
|
||||
if (isset($weekly_settings[$day_name]) && $weekly_settings[$day_name]['enabled'] == '1') {
|
||||
// 운영일인 경우 스케줄 생성
|
||||
$slots_created = $this->generateDay($date, $weekly_settings[$day_name], $basic_settings);
|
||||
$generated_count += $slots_created;
|
||||
} else {
|
||||
// 휴무일인 경우 휴무 표시
|
||||
$this->createHolidaySlot($date, '휴무일');
|
||||
}
|
||||
}
|
||||
|
||||
consultant_log("월별 스케줄 생성 완료: {$year}-{$month}, 생성된 슬롯: {$generated_count}개");
|
||||
return $generated_count;
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("월별 스케줄 생성 실패: " . $e->getMessage(), 'error');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 스케줄 생성
|
||||
*/
|
||||
public function generateDay($date, $day_settings, $basic_settings)
|
||||
{
|
||||
$slots_created = 0;
|
||||
|
||||
try {
|
||||
$start_time = $day_settings['start'];
|
||||
$end_time = $day_settings['end'];
|
||||
$lunch_start = $day_settings['lunch_start'];
|
||||
$lunch_end = $day_settings['lunch_end'];
|
||||
|
||||
$slot_duration = (int) $basic_settings['consultation_duration'];
|
||||
$max_persons = (int) $basic_settings['max_persons_per_slot'];
|
||||
|
||||
// 시간 슬롯 생성
|
||||
$current_time = strtotime($start_time);
|
||||
$end_timestamp = strtotime($end_time);
|
||||
|
||||
while ($current_time < $end_timestamp) {
|
||||
$slot_start = date('H:i', $current_time);
|
||||
$slot_end = date('H:i', $current_time + ($slot_duration * 60));
|
||||
|
||||
// 종료시간이 운영시간을 넘지 않도록 체크
|
||||
if (strtotime($slot_end) > $end_timestamp) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 점심시간 체크
|
||||
$is_lunch_time = $this->isLunchTime($slot_start, $slot_end, $lunch_start, $lunch_end);
|
||||
|
||||
if ($is_lunch_time) {
|
||||
// 점심시간 슬롯 생성
|
||||
$this->createTimeSlot($date, $slot_start, $slot_end, $slot_duration, 0, 0, 'lunch_time');
|
||||
} else {
|
||||
// 일반 상담 슬롯 생성
|
||||
$this->createTimeSlot($date, $slot_start, $slot_end, $slot_duration, $max_persons, 1, 'auto_generated');
|
||||
}
|
||||
|
||||
$slots_created++;
|
||||
$current_time += ($slot_duration * 60);
|
||||
}
|
||||
|
||||
return $slots_created;
|
||||
|
||||
} catch (Exception $e) {
|
||||
consultant_log("일별 스케줄 생성 실패 ({$date}): " . $e->getMessage(), 'error');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 슬롯 생성
|
||||
*/
|
||||
private function createTimeSlot($date, $start_time, $end_time, $duration, $max_persons, $is_available, $type)
|
||||
{
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, created_at)
|
||||
VALUES
|
||||
('{$date}', '{$start_time}', '{$end_time}', {$duration}, {$max_persons}, {$is_available}, '{$type}', NOW())";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴무일 슬롯 생성
|
||||
*/
|
||||
private function createHolidaySlot($date, $reason = '휴무일')
|
||||
{
|
||||
$sql = "INSERT INTO consultant_schedule
|
||||
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, temp_2, created_at)
|
||||
VALUES
|
||||
('{$date}', '00:00', '23:59', 0, 0, 0, 'holiday', '{$reason}', NOW())";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 점심시간 여부 확인
|
||||
*/
|
||||
private function isLunchTime($slot_start, $slot_end, $lunch_start, $lunch_end)
|
||||
{
|
||||
if (empty($lunch_start) || empty($lunch_end)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$slot_start_time = strtotime($slot_start);
|
||||
$slot_end_time = strtotime($slot_end);
|
||||
$lunch_start_time = strtotime($lunch_start);
|
||||
$lunch_end_time = strtotime($lunch_end);
|
||||
|
||||
// 슬롯이 점심시간과 겹치는지 확인
|
||||
return ($slot_start_time >= $lunch_start_time && $slot_start_time < $lunch_end_time) ||
|
||||
($slot_end_time > $lunch_start_time && $slot_end_time <= $lunch_end_time) ||
|
||||
($slot_start_time <= $lunch_start_time && $slot_end_time >= $lunch_end_time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 월의 모든 날짜 배열 생성
|
||||
*/
|
||||
private function getMonthDates($year, $month)
|
||||
{
|
||||
$dates = [];
|
||||
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
|
||||
|
||||
for ($day = 1; $day <= $days_in_month; $day++) {
|
||||
$dates[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일 숫자를 요일명으로 변환
|
||||
*/
|
||||
private function getDayName($day_of_week)
|
||||
{
|
||||
$day_names = [
|
||||
0 => 'sunday',
|
||||
1 => 'monday',
|
||||
2 => 'tuesday',
|
||||
3 => 'wednesday',
|
||||
4 => 'thursday',
|
||||
5 => 'friday',
|
||||
6 => 'saturday'
|
||||
];
|
||||
|
||||
return $day_names[$day_of_week] ?? 'sunday';
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 설정 조회
|
||||
*/
|
||||
private function getBasicSettings()
|
||||
{
|
||||
return [
|
||||
'consultation_duration' => consultant_get_config('consultation_duration', '60'),
|
||||
'max_persons_per_slot' => consultant_get_config('max_persons_per_slot', '2'),
|
||||
'consultation_fee' => consultant_get_config('consultation_fee', '50000')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일별 설정 조회
|
||||
*/
|
||||
private function getWeeklySettings()
|
||||
{
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$settings = [];
|
||||
|
||||
foreach ($days as $day) {
|
||||
$settings[$day] = [
|
||||
'enabled' => consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1'),
|
||||
'start' => consultant_get_config($day . '_start', '09:00'),
|
||||
'end' => consultant_get_config($day . '_end', '18:00'),
|
||||
'lunch_start' => consultant_get_config($day . '_lunch_start', '12:00'),
|
||||
'lunch_end' => consultant_get_config($day . '_lunch_end', '13:00')
|
||||
];
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 생성된 스케줄 삭제
|
||||
*/
|
||||
private function clearAutoGeneratedSchedule($year, $month)
|
||||
{
|
||||
$start_date = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
||||
|
||||
// 기존 예약이 없는 자동 생성 스케줄만 삭제
|
||||
$sql = "DELETE cs FROM consultant_schedule cs
|
||||
LEFT JOIN consultant_reservations cr ON (
|
||||
cs.specific_date = cr.reservation_date
|
||||
AND cs.start_time = cr.reservation_time
|
||||
AND cr.is_deleted = 0
|
||||
)
|
||||
WHERE cs.specific_date >= '{$start_date}'
|
||||
AND cs.specific_date <= '{$end_date}'
|
||||
AND cs.temp_1 IN ('auto_generated', 'lunch_time', 'holiday')
|
||||
AND cr.id IS NULL";
|
||||
|
||||
return sql_query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 예약 보호 - 예약이 있는 시간대 확인
|
||||
*/
|
||||
public function getExistingReservations($year, $month)
|
||||
{
|
||||
$start_date = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
||||
|
||||
$sql = "SELECT reservation_date, reservation_time, COUNT(*) as count
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date >= '{$start_date}'
|
||||
AND reservation_date <= '{$end_date}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_date, reservation_time";
|
||||
|
||||
$result = sql_query($sql);
|
||||
$reservations = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$key = $row['reservation_date'] . '_' . $row['reservation_time'];
|
||||
$reservations[$key] = $row['count'];
|
||||
}
|
||||
}
|
||||
|
||||
return $reservations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 달 스케줄 자동 생성 (크론잡용)
|
||||
*/
|
||||
public function generateNextMonth()
|
||||
{
|
||||
$next_month = date('Y-m', strtotime('+1 month'));
|
||||
list($year, $month) = explode('-', $next_month);
|
||||
|
||||
return $this->generateMonth((int) $year, (int) $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 생성 상태 확인
|
||||
*/
|
||||
public function checkScheduleStatus($year, $month)
|
||||
{
|
||||
$start_date = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
||||
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'auto_generated' THEN 1 END) as auto_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'lunch_time' THEN 1 END) as lunch_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'holiday' THEN 1 END) as holiday_slots,
|
||||
COUNT(CASE WHEN temp_1 = 'admin_blocked' THEN 1 END) as blocked_slots
|
||||
FROM consultant_schedule
|
||||
WHERE specific_date >= '{$start_date}'
|
||||
AND specific_date <= '{$end_date}'";
|
||||
|
||||
return sql_fetch($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 변경 시 영향받는 예약 확인
|
||||
*/
|
||||
public function checkSettingConflicts($year, $month)
|
||||
{
|
||||
$existing_reservations = $this->getExistingReservations($year, $month);
|
||||
$conflicts = [];
|
||||
|
||||
// 새로운 설정으로 생성될 스케줄과 기존 예약 비교
|
||||
$weekly_settings = $this->getWeeklySettings();
|
||||
$dates = $this->getMonthDates($year, $month);
|
||||
|
||||
foreach ($dates as $date) {
|
||||
$day_of_week = date('w', strtotime($date));
|
||||
$day_name = $this->getDayName($day_of_week);
|
||||
|
||||
// 휴무일로 변경되었는데 예약이 있는 경우
|
||||
if (!isset($weekly_settings[$day_name]) || $weekly_settings[$day_name]['enabled'] != '1') {
|
||||
foreach ($existing_reservations as $key => $count) {
|
||||
if (strpos($key, $date) === 0) {
|
||||
$conflicts[] = [
|
||||
'date' => $date,
|
||||
'type' => 'holiday_conflict',
|
||||
'message' => "{$date}는 휴무일로 설정되었지만 {$count}건의 예약이 있습니다."
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 생성 헬퍼 함수들
|
||||
*/
|
||||
|
||||
/**
|
||||
* 월별 스케줄 생성 실행
|
||||
*/
|
||||
function generate_monthly_schedule($year, $month)
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->generateMonth($year, $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 달 스케줄 자동 생성
|
||||
*/
|
||||
function auto_generate_next_month_schedule()
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->generateNextMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 생성 상태 확인
|
||||
*/
|
||||
function get_schedule_generation_status($year, $month)
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->checkScheduleStatus($year, $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 변경 영향 확인
|
||||
*/
|
||||
function check_schedule_setting_conflicts($year, $month)
|
||||
{
|
||||
$generator = new ScheduleGenerator();
|
||||
return $generator->checkSettingConflicts($year, $month);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 통합 설정 관리
|
||||
*/
|
||||
|
||||
$sub_menu = '850610'; // 메뉴 코드 (기본/운영 설정)
|
||||
include_once('./_common.php');
|
||||
|
||||
// 권한 확인
|
||||
auth_check_menu($auth, $sub_menu, 'w');
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '상담 예약 설정';
|
||||
|
||||
// 현재 탭 확인
|
||||
$current_tab = $_GET['tab'] ?? 'basic';
|
||||
|
||||
// 폼 처리
|
||||
if (isset($_POST['action']) && $_POST['action']) {
|
||||
try {
|
||||
// 기본 설정 저장
|
||||
if ($_POST['action'] == 'save_basic_settings') {
|
||||
$basic_settings = [
|
||||
'consultation_duration' => (int) ($_POST['consultation_duration'] ?? 60),
|
||||
'max_persons_per_slot' => (int) ($_POST['max_persons_per_slot'] ?? 2),
|
||||
'consultation_fee' => (int) ($_POST['consultation_fee'] ?? 50000),
|
||||
'account_info' => trim($_POST['account_info'] ?? ''),
|
||||
'max_advance_days' => (int) ($_POST['max_advance_days'] ?? 30),
|
||||
'min_advance_hours' => (int) ($_POST['min_advance_hours'] ?? 24),
|
||||
'cancel_deadline_hours' => (int) ($_POST['cancel_deadline_hours'] ?? 24)
|
||||
];
|
||||
|
||||
// 유효성 검증 (생략)
|
||||
|
||||
foreach ($basic_settings as $key => $value) {
|
||||
consultant_set_config($key, $value);
|
||||
}
|
||||
alert('기본 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=basic');
|
||||
}
|
||||
|
||||
// 요일별 설정 저장
|
||||
if ($_POST['action'] == 'save_weekly_settings') {
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
foreach ($days as $day) {
|
||||
consultant_set_config($day . '_enabled', $_POST[$day . '_enabled'] ?? '0');
|
||||
consultant_set_config($day . '_start', $_POST[$day . '_start'] ?? '09:00');
|
||||
consultant_set_config($day . '_end', $_POST[$day . '_end'] ?? '18:00');
|
||||
consultant_set_config($day . '_lunch_start', $_POST[$day . '_lunch_start'] ?? '12:00');
|
||||
consultant_set_config($day . '_lunch_end', $_POST[$day . '_lunch_end'] ?? '13:00');
|
||||
}
|
||||
alert('요일별 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=weekly');
|
||||
}
|
||||
|
||||
// 알림 설정 저장
|
||||
if ($_POST['action'] == 'save_notification_settings') {
|
||||
consultant_set_config('notification_enabled', $_POST['notification_enabled'] ?? '0');
|
||||
alert('알림 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=notification');
|
||||
}
|
||||
|
||||
// 💡 [추가] 고급 설정 저장 처리
|
||||
if ($_POST['action'] == 'save_advanced_settings') {
|
||||
$config_values = $_POST['config_value'] ?? [];
|
||||
$config_descs = $_POST['config_desc'] ?? [];
|
||||
|
||||
foreach ($config_values as $key => $value) {
|
||||
$sql = "UPDATE consultant_config
|
||||
SET config_value = '" . sql_real_escape_string($value) . "',
|
||||
config_desc = '" . sql_real_escape_string($config_descs[$key] ?? '') . "'
|
||||
WHERE config_key = '" . sql_real_escape_string($key) . "'";
|
||||
sql_query($sql);
|
||||
}
|
||||
alert('고급 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=advanced');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
alert('설정 저장 중 오류가 발생했습니다: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- 데이터 조회 ---
|
||||
// 기본 설정
|
||||
$consultation_duration = consultant_get_config('consultation_duration', '60');
|
||||
$max_persons_per_slot = consultant_get_config('max_persons_per_slot', '2');
|
||||
$consultation_fee = consultant_get_config('consultation_fee', '50000');
|
||||
$account_info = consultant_get_config('account_info', '국민은행 123-456-789 (주)상담센터');
|
||||
$notification_enabled = consultant_get_config('notification_enabled', '1');
|
||||
$max_advance_days = consultant_get_config('max_advance_days', '30');
|
||||
$min_advance_hours = consultant_get_config('min_advance_hours', '24');
|
||||
$cancel_deadline_hours = consultant_get_config('cancel_deadline_hours', '24');
|
||||
|
||||
// 요일별 설정
|
||||
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
$day_names = ['월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
|
||||
$weekly_settings = [];
|
||||
foreach ($days as $i => $day) {
|
||||
$weekly_settings[$day] = [
|
||||
'name' => $day_names[$i],
|
||||
'enabled' => consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1'),
|
||||
'start' => consultant_get_config($day . '_start', '09:00'),
|
||||
'end' => consultant_get_config($day . '_end', '18:00'),
|
||||
'lunch_start' => consultant_get_config($day . '_lunch_start', '12:00'),
|
||||
'lunch_end' => consultant_get_config($day . '_lunch_end', '13:00')
|
||||
];
|
||||
}
|
||||
|
||||
// 💡 [추가] 고급 설정 데이터 조회
|
||||
$advanced_configs = [];
|
||||
$result = sql_query("SELECT * FROM consultant_config ORDER BY id");
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$advanced_configs[] = $row;
|
||||
}
|
||||
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
<div>
|
||||
<a href="dashboard.php" class="header-btn">📊 대시보드</a>
|
||||
<a href="schedule_generate.php" class="header-btn primary">📅 빠른 스케줄 관리</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="settings-tabs">
|
||||
<a href="?tab=basic" class="tab-button <?php echo $current_tab == 'basic' ? 'active' : ''; ?>">⚙️ 기본 설정</a>
|
||||
<a href="?tab=weekly" class="tab-button <?php echo $current_tab == 'weekly' ? 'active' : ''; ?>">📅 요일별 운영시간</a>
|
||||
<a href="?tab=advanced" class="tab-button <?php echo $current_tab == 'advanced' ? 'active' : ''; ?>">🛠️ 고급 설정</a>
|
||||
<a href="?tab=notification" class="tab-button <?php echo $current_tab == 'notification' ? 'active' : ''; ?>">🔔 알림 설정</a>
|
||||
</div>
|
||||
|
||||
<!-- 기본 설정 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'basic' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>기본 설정:</strong> 1회 상담시간, 최대인원, 상담비 등 기본적인 상담 조건을 설정합니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_basic_settings">
|
||||
|
||||
<div class="section-title">⏰ 상담 기본 조건</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="consultation_duration">1회 상담 시간 (분)</label>
|
||||
<select id="consultation_duration" name="consultation_duration">
|
||||
<?php
|
||||
for ($i = 15; $i <= 480; $i += 15) {
|
||||
$selected = ($consultation_duration == $i) ? 'selected' : '';
|
||||
echo "<option value=\"{$i}\" {$selected}>{$i}분</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<small>15분~480분 사이로 설정 가능합니다. (15분 단위)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="max_persons_per_slot">1회 상담 최대 인원 (명)</label>
|
||||
<input type="number" id="max_persons_per_slot" name="max_persons_per_slot" value="<?php echo htmlspecialchars($max_persons_per_slot); ?>" min="1" max="50" placeholder="2">
|
||||
<small>1명~50명 사이로 설정 가능합니다.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">💰 결제 정보</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="consultation_fee">상담 비용 (원)</label>
|
||||
<input type="number" id="consultation_fee" name="consultation_fee" value="<?php echo htmlspecialchars($consultation_fee); ?>" min="0" step="1000" placeholder="50000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_info">입금 계좌 정보</label>
|
||||
<textarea id="account_info" name="account_info" placeholder="예: 국민은행 123-456-789 (주)상담센터"><?php echo htmlspecialchars($account_info); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="section-title">📅 예약 제한 설정</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="max_advance_days">최대 예약 가능 일수</label>
|
||||
<input type="number" id="max_advance_days" name="max_advance_days" value="<?php echo htmlspecialchars($max_advance_days); ?>" min="1" max="365">
|
||||
<small>오늘부터 몇 일 후까지 예약 가능한지 설정</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min_advance_hours">최소 예약 시간 (시간)</label>
|
||||
<input type="number" id="min_advance_hours" name="min_advance_hours" value="<?php echo htmlspecialchars($min_advance_hours); ?>" min="1" max="168">
|
||||
<small>최소 몇 시간 전에 예약해야 하는지 설정</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cancel_deadline_hours">예약 취소 마감 (시간)</label>
|
||||
<input type="number" id="cancel_deadline_hours" name="cancel_deadline_hours" value="<?php echo htmlspecialchars($cancel_deadline_hours); ?>" min="1" max="168">
|
||||
<small>상담 시작 몇 시간 전까지 고객이 직접 취소할 수 있는지 설정</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">기본 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 요일별 운영시간 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'weekly' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>요일별 운영시간:</strong> 각 요일의 상담 운영 여부와 시간을 설정합니다. '운영'을 선택해야 해당 요일의 스케줄이 생성됩니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_weekly_settings">
|
||||
|
||||
<?php foreach ($weekly_settings as $day => $setting): ?>
|
||||
<div class="day-setting">
|
||||
<div class="day-header">
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="hidden" name="<?php echo $day; ?>_enabled" value="0">
|
||||
<input type="checkbox" id="<?php echo $day; ?>_enabled" name="<?php echo $day; ?>_enabled" value="1" <?php echo $setting['enabled'] == '1' ? 'checked' : ''; ?> onchange="toggleDayTimes('<?php echo $day; ?>')">
|
||||
<label for="<?php echo $day; ?>_enabled" class="day-name"><?php echo $setting['name']; ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day-times" id="<?php echo $day; ?>_times">
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_start">업무 시작</label>
|
||||
<input type="time" id="<?php echo $day; ?>_start" name="<?php echo $day; ?>_start" value="<?php echo htmlspecialchars($setting['start']); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_end">업무 종료</label>
|
||||
<input type="time" id="<?php echo $day; ?>_end" name="<?php echo $day; ?>_end" value="<?php echo htmlspecialchars($setting['end']); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_lunch_start">점심 시작</label>
|
||||
<input type="time" id="<?php echo $day; ?>_lunch_start" name="<?php echo $day; ?>_lunch_start" value="<?php echo htmlspecialchars($setting['lunch_start']); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="<?php echo $day; ?>_lunch_end">점심 종료</label>
|
||||
<input type="time" id="<?php echo $day; ?>_lunch_end" name="<?php echo $day; ?>_lunch_end" value="<?php echo htmlspecialchars($setting['lunch_end']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">요일별 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [추가] 고급 설정 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'advanced' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>고급 설정:</strong> 시스템의 모든 설정값을 직접 관리합니다. <strong>'Key'는 시스템에서 사용하는 고유값이므로 변경할 수 없습니다.</strong><br>
|
||||
'Value'는 실제 적용되는 값이며, 'Description'은 관리자가 참고하기 위한 설명입니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_advanced_settings">
|
||||
<div class="tbl_head01 tbl_wrap">
|
||||
<table>
|
||||
<caption>고급 설정 목록</caption>
|
||||
<colgroup>
|
||||
<col style="width: 25%;">
|
||||
<col style="width: 40%;">
|
||||
<col style="width: 35%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Key (변경불가)</th>
|
||||
<th scope="col">Value (설정값)</th>
|
||||
<th scope="col">Description (설명)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($advanced_configs as $config_item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="config_value_<?php echo $config_item['config_key']; ?>">
|
||||
<code style="font-size: 13px; font-weight: bold;"><?php echo htmlspecialchars($config_item['config_key']); ?></code>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (strlen($config_item['config_value']) > 50 || strpos($config_item['config_value'], "\n") !== false): ?>
|
||||
<textarea class="form-control" id="config_value_<?php echo $config_item['config_key']; ?>" name="config_value[<?php echo $config_item['config_key']; ?>]" rows="2"><?php echo htmlspecialchars($config_item['config_value']); ?></textarea>
|
||||
<?php else: ?>
|
||||
<input type="text" class="form-control" id="config_value_<?php echo $config_item['config_key']; ?>" name="config_value[<?php echo $config_item['config_key']; ?>]" value="<?php echo htmlspecialchars($config_item['config_value']); ?>">
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" name="config_desc[<?php echo $config_item['config_key']; ?>]" value="<?php echo htmlspecialchars($config_item['config_desc']); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">고급 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 알림 설정 탭 -->
|
||||
<div class="tab-content <?php echo $current_tab == 'notification' ? 'active' : ''; ?>">
|
||||
<div class="alert alert-info">
|
||||
<strong>알림 설정:</strong> 예약 관련 알림 기능을 설정합니다.
|
||||
</div>
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save_notification_settings">
|
||||
<div class="section-title">🔔 알림 기능</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox" id="notification_enabled" name="notification_enabled" value="1" <?php echo $notification_enabled == '1' ? 'checked' : ''; ?>>
|
||||
<label for="notification_enabled">알림 기능 사용</label>
|
||||
</div>
|
||||
<small>예약 확정, 취소 등의 상황에서 고객에게 알림을 발송합니다.</small>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 40px;">
|
||||
<button type="submit" class="btn btn-primary">알림 설정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 💡 [복구] 깨진 화면을 복구하기 위해 CSS와 JS를 파일 내에 다시 포함합니다. -->
|
||||
<style>
|
||||
.settings-container { max-width: 1000px; margin: 0 auto; padding: 20px; }
|
||||
.settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.settings-header div { display: flex; gap: 8px; }
|
||||
.settings-tabs { display: flex; border-bottom: 2px solid #ddd; margin-bottom: 30px; }
|
||||
.tab-button { padding: 16px 28px; border: none; background: none; cursor: pointer; font-size: 16px; font-weight: 600; color: #666; text-decoration: none; border-bottom: 3px solid transparent; transition: all 0.3s; }
|
||||
.tab-button.active { color: #007bff; border-bottom-color: #007bff; }
|
||||
.tab-button:hover { color: #007bff; background: #fff; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.settings-form { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #333; }
|
||||
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 12px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; min-height: 44px; line-height: 1.4; }
|
||||
.form-group textarea { height: auto; resize: vertical; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.btn { padding: 16px 32px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 16px; text-decoration: none; display: inline-block; transition: all 0.2s; text-align: center; min-height: 50px; line-height: 1.4; box-sizing: border-box; vertical-align: middle; }
|
||||
.btn-primary { background: #007bff; color: white; }
|
||||
.btn-primary:hover { background: #0056b3; }
|
||||
.alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
|
||||
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; }
|
||||
.section-title { font-size: 18px; font-weight: bold; margin: 30px 0 15px 0; padding-bottom: 8px; border-bottom: 2px solid #007bff; color: #333; }
|
||||
.section-title:first-child { margin-top: 0; }
|
||||
.day-setting { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
||||
.day-header { display: flex; align-items: center; margin-bottom: 15px; gap: 15px; }
|
||||
.day-name { font-weight: bold; font-size: 16px; color: #333; min-width: 80px; }
|
||||
.day-times { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 15px; }
|
||||
.day-times.disabled { opacity: 0.5; pointer-events: none; }
|
||||
.checkbox-wrapper { display: flex; align-items: center; gap: 8px; }
|
||||
.checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; margin: 0; }
|
||||
.form-group small { display: block; margin-top: 5px; font-size: 12px; color: #666; line-height: 1.3; }
|
||||
.header-btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 14px; text-decoration: none; display: inline-block; transition: all 0.2s; text-align: center; min-height: auto; line-height: 1.2; box-sizing: border-box; background: #fff; color: #333; }
|
||||
.header-btn:hover { background: #fff; border-color: #adb5bd; color: #333; }
|
||||
.header-btn.primary { background: #e3f2fd; border-color: #90caf9; color: #1976d2; }
|
||||
.header-btn.primary:hover { background: #bbdefb; border-color: #64b5f6; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleDayTimes(day) {
|
||||
const checkbox = document.getElementById(day + '_enabled');
|
||||
const timesDiv = document.getElementById(day + '_times');
|
||||
if (checkbox?.checked) {
|
||||
timesDiv.classList.remove('disabled');
|
||||
} else {
|
||||
timesDiv?.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
days.forEach(function (day) {
|
||||
toggleDayTimes(day);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 통계 분석
|
||||
*/
|
||||
$sub_menu = '850400';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '통계 분석';
|
||||
|
||||
// 기간 설정
|
||||
$start_date = $_GET['start_date'] ?? date('Y-m-01'); // 이번 달 첫날
|
||||
$end_date = $_GET['end_date'] ?? date('Y-m-d'); // 오늘
|
||||
|
||||
// 전체 통계
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
|
||||
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
|
||||
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as total_revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0";
|
||||
$total_stats = sql_fetch($sql);
|
||||
|
||||
// 일별 통계
|
||||
$sql = "SELECT
|
||||
reservation_date,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
|
||||
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
|
||||
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
|
||||
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_date
|
||||
ORDER BY reservation_date DESC";
|
||||
$daily_stats = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$daily_stats[] = $row;
|
||||
}
|
||||
|
||||
// 시간대별 통계
|
||||
$sql = "SELECT
|
||||
reservation_time,
|
||||
COUNT(*) as count
|
||||
FROM consultant_reservations
|
||||
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
|
||||
AND is_deleted = 0
|
||||
GROUP BY reservation_time
|
||||
ORDER BY reservation_time";
|
||||
$time_stats = [];
|
||||
$result = sql_query($sql);
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$time_stats[] = $row;
|
||||
}
|
||||
|
||||
// 상태별 통계
|
||||
$status_labels = [
|
||||
'payment_pending' => '입금대기',
|
||||
'reserved' => '예약확정',
|
||||
'completed' => '상담완료',
|
||||
'cancelled' => '예약취소'
|
||||
];
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.statistics-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-card.total .stat-number {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.stat-card.pending .stat-number {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.stat-card.confirmed .stat-number {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.stat-card.completed .stat-number {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.stat-card.cancelled .stat-number {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.stat-card.revenue .stat-number {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.table-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
height: 200px;
|
||||
gap: 10px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.bar {
|
||||
background: #007bff;
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-width: 40px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
transform: rotate(-45deg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="statistics-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<!-- 기간 필터 -->
|
||||
<form method="get" class="filter-form">
|
||||
<label>
|
||||
시작일:
|
||||
<input type="date" name="start_date" value="<?php echo $start_date; ?>" class="form-control">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
종료일:
|
||||
<input type="date" name="end_date" value="<?php echo $end_date; ?>" class="form-control">
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary">조회</button>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</form>
|
||||
|
||||
<!-- 전체 통계 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card total">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['total']); ?></div>
|
||||
<div class="stat-label">전체 예약</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['pending']); ?></div>
|
||||
<div class="stat-label">입금대기</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card confirmed">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['confirmed']); ?></div>
|
||||
<div class="stat-label">예약확정</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card completed">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['completed']); ?></div>
|
||||
<div class="stat-label">상담완료</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card cancelled">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['cancelled']); ?></div>
|
||||
<div class="stat-label">예약취소</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card revenue">
|
||||
<div class="stat-number"><?php echo number_format($total_stats['total_revenue']); ?>원</div>
|
||||
<div class="stat-label">총 매출</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시간대별 예약 현황 -->
|
||||
<?php if (!empty($time_stats)): ?>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">시간대별 예약 현황</div>
|
||||
<div class="bar-chart">
|
||||
<?php
|
||||
$max_count = max(array_column($time_stats, 'count'));
|
||||
foreach ($time_stats as $stat):
|
||||
$height = $max_count > 0 ? ($stat['count'] / $max_count) * 150 : 0;
|
||||
?>
|
||||
<div class="bar" style="height: <?php echo $height; ?>px;">
|
||||
<div class="bar-value"><?php echo $stat['count']; ?></div>
|
||||
<div class="bar-label"><?php echo substr($stat['reservation_time'], 0, 5); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 일별 상세 통계 -->
|
||||
<div class="table-container">
|
||||
<div class="table-header">일별 예약 현황</div>
|
||||
<div class="table-content">
|
||||
<?php if (!empty($daily_stats)): ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<th>전체</th>
|
||||
<th>입금대기</th>
|
||||
<th>예약확정</th>
|
||||
<th>상담완료</th>
|
||||
<th>예약취소</th>
|
||||
<th>매출</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($daily_stats as $stat): ?>
|
||||
<tr>
|
||||
<td><?php echo $stat['reservation_date']; ?></td>
|
||||
<td><?php echo number_format($stat['total']); ?></td>
|
||||
<td><?php echo number_format($stat['pending']); ?></td>
|
||||
<td><?php echo number_format($stat['confirmed']); ?></td>
|
||||
<td><?php echo number_format($stat['completed']); ?></td>
|
||||
<td><?php echo number_format($stat['cancelled']); ?></td>
|
||||
<td><?php echo number_format($stat['revenue']); ?>원</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="no-data">
|
||||
선택한 기간에 예약 데이터가 없습니다.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 차트 애니메이션
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const bars = document.querySelectorAll('.bar');
|
||||
bars.forEach((bar, index) => {
|
||||
setTimeout(() => {
|
||||
bar.style.opacity = '0';
|
||||
bar.style.transform = 'scaleY(0)';
|
||||
bar.style.transformOrigin = 'bottom';
|
||||
|
||||
setTimeout(() => {
|
||||
bar.style.transition = 'all 0.5s ease';
|
||||
bar.style.opacity = '1';
|
||||
bar.style.transform = 'scaleY(1)';
|
||||
}, 100);
|
||||
}, index * 100);
|
||||
});
|
||||
});
|
||||
|
||||
// 기간 설정 단축키
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const startDateInput = document.querySelector('input[name="start_date"]');
|
||||
const endDateInput = document.querySelector('input[name="end_date"]');
|
||||
|
||||
// 오늘 날짜
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
|
||||
// 이번 주 시작일 (월요일)
|
||||
const thisWeekStart = new Date(today);
|
||||
thisWeekStart.setDate(today.getDate() - today.getDay() + 1);
|
||||
const thisWeekStartStr = thisWeekStart.toISOString().split('T')[0];
|
||||
|
||||
// 이번 달 시작일
|
||||
const thisMonthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const thisMonthStartStr = thisMonthStart.toISOString().split('T')[0];
|
||||
|
||||
// 단축키 버튼들 추가
|
||||
const filterForm = document.querySelector('.filter-form');
|
||||
const shortcutButtons = document.createElement('div');
|
||||
shortcutButtons.innerHTML = `
|
||||
<button type="button" onclick="setDateRange('${todayStr}', '${todayStr}')" class="btn btn-secondary">오늘</button>
|
||||
<button type="button" onclick="setDateRange('${thisWeekStartStr}', '${todayStr}')" class="btn btn-secondary">이번주</button>
|
||||
<button type="button" onclick="setDateRange('${thisMonthStartStr}', '${todayStr}')" class="btn btn-secondary">이번달</button>
|
||||
`;
|
||||
filterForm.appendChild(shortcutButtons);
|
||||
|
||||
window.setDateRange = function (start, end) {
|
||||
startDateInput.value = start;
|
||||
endDateInput.value = end;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
/**
|
||||
* 알림 템플릿 관리
|
||||
*/
|
||||
$sub_menu = '850620';
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '알림 템플릿 관리';
|
||||
|
||||
// 템플릿 저장 처리
|
||||
if ($_POST['action'] == 'save_template') {
|
||||
$template_key = $_POST['template_key'];
|
||||
$template_subject = $_POST['template_subject'];
|
||||
$template_type = $_POST['template_type'] ?? 'email'; // 💡 [추가] 템플릿 타입
|
||||
$template_content = $_POST['template_content'];
|
||||
$template_name = $_POST['template_name']; // 💡 [추가] 템플릿 이름을 폼에서 받아옵니다.
|
||||
|
||||
if ($template_key && $template_subject && $template_content) {
|
||||
// 템플릿 저장/업데이트
|
||||
// 💡 [수정] 타입에 따라 테이블 분기
|
||||
$table_name = ($template_type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
|
||||
|
||||
$sql = "INSERT INTO {$table_name}
|
||||
(template_key, template_type, template_name, template_subject, template_content, updated_at)
|
||||
VALUES (
|
||||
'" . sql_real_escape_string($template_key) . "',
|
||||
'" . sql_real_escape_string($template_type) . "',
|
||||
'" . sql_real_escape_string($template_name) . "',
|
||||
'" . sql_real_escape_string($template_subject) . "',
|
||||
'" . sql_real_escape_string($template_content) . "',
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
template_subject = '" . sql_real_escape_string($template_subject) . "',
|
||||
template_type = '" . sql_real_escape_string($template_type) . "',
|
||||
template_content = '" . sql_real_escape_string($template_content) . "',
|
||||
updated_at = NOW()";
|
||||
|
||||
if (sql_query($sql)) {
|
||||
alert('템플릿이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?type=' . $template_type . '&template=' . $template_key);
|
||||
} else {
|
||||
alert('템플릿 저장에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 [추가] 현재 탭 확인
|
||||
$current_type = $_GET['type'] ?? 'email';
|
||||
|
||||
// 기본 템플릿 정의
|
||||
$default_templates = [
|
||||
'consultant_reservation_customer' => [
|
||||
'name' => '고객 예약 신청 확인',
|
||||
'subject' => '[상담예약] 예약 신청이 접수되었습니다',
|
||||
'content' => "안녕하세요 {customer_name}님,\n\n상담 예약 신청이 정상적으로 접수되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n- 상담비: {payment_amount}원\n\n입금 계좌: {account_info}\n\n입금 확인 후 예약이 확정됩니다.\n\n감사합니다."
|
||||
],
|
||||
'consultant_confirmed_customer' => [
|
||||
'name' => '고객 예약 확정 알림',
|
||||
'subject' => '[상담예약] 예약이 확정되었습니다',
|
||||
'content' => "안녕하세요 {customer_name}님,\n\n입금이 확인되어 예약이 확정되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n상담 당일 시간에 맞춰 방문해주시기 바랍니다.\n\n감사합니다."
|
||||
],
|
||||
'consultant_cancelled_customer' => [
|
||||
'name' => '고객 예약 취소 알림',
|
||||
'subject' => '[상담예약] 예약이 취소되었습니다',
|
||||
'content' => "안녕하세요 {customer_name}님,\n\n예약이 취소되었습니다.\n\n취소된 예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n취소 사유: {cancel_reason}\n\n문의사항이 있으시면 연락주시기 바랍니다.\n\n감사합니다."
|
||||
]
|
||||
];
|
||||
|
||||
// 현재 템플릿 조회
|
||||
$templates = [];
|
||||
// 💡 [수정] 현재 탭의 타입에 맞는 템플릿만 조회합니다.
|
||||
if (is_consultant_installed()) {
|
||||
// 💡 [수정] 타입에 따라 테이블 분기
|
||||
$table_name = ($current_type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
|
||||
|
||||
// 테이블 존재 여부 확인 (설치 초기 단계 고려)
|
||||
$table_check = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
|
||||
if (sql_num_rows($table_check) > 0) {
|
||||
$sql = "SELECT * FROM {$table_name} WHERE template_type = '".sql_real_escape_string($current_type)."' ORDER BY template_key";
|
||||
$result = sql_query($sql, false);
|
||||
if ($result) {
|
||||
while ($row = sql_fetch_array($result)) {
|
||||
$templates[$row['template_key']] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 템플릿과 병합
|
||||
foreach ($default_templates as $key => $default) {
|
||||
if (!isset($templates[$key])) {
|
||||
$templates[$key] = [
|
||||
'template_key' => $key,
|
||||
'template_name' => $default['name'],
|
||||
'template_subject' => $default['subject'],
|
||||
'template_content' => $default['content']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$current_template_key = $_GET['template'] ?? array_key_first($templates);
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.templates-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.template-nav {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.template-tabs {
|
||||
display: flex; border-bottom: 2px solid #ddd; margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.template-tab {
|
||||
padding: 16px 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-tab.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.template-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.variables-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.variables-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.variables-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.variable-item {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.template-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.variables-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="templates-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>안내:</strong> 예약 관련 자동 발송 <?php echo ($current_type == 'sms') ? '문자' : '이메일'; ?>의 템플릿을 관리합니다.
|
||||
중괄호 {} 안의 변수들은 실제 데이터로 자동 치환됩니다.
|
||||
</div>
|
||||
|
||||
<!-- 템플릿 탭 -->
|
||||
<div class="template-nav">
|
||||
<div class="template-tabs">
|
||||
<a href="?type=email" class="template-tab <?php echo $current_type == 'email' ? 'active' : ''; ?>">📧 메일 템플릿</a>
|
||||
<a href="?type=sms" class="template-tab <?php echo $current_type == 'sms' ? 'active' : ''; ?>">📱 문자 템플릿</a>
|
||||
</div>
|
||||
|
||||
<?php if(!empty($templates)): ?>
|
||||
<div class="template-tabs" style="border-bottom:none; margin-bottom: 10px;">
|
||||
<?php foreach ($templates as $key => $template): ?>
|
||||
<a href="?type=<?php echo $current_type; ?>&template=<?php echo $key; ?>"
|
||||
class="template-tab <?php echo $current_template_key == $key ? 'active' : ''; ?>" style="font-size:14px; padding: 8px 16px;">
|
||||
<?php echo htmlspecialchars($template['template_name']); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 사용 가능한 변수 안내 -->
|
||||
<div class="variables-info">
|
||||
<div class="variables-title">📝 사용 가능한 변수</div>
|
||||
<div class="variables-list">
|
||||
<div class="variable-item">{customer_name} - 고객명</div>
|
||||
<div class="variable-item">{customer_phone} - 고객 연락처</div>
|
||||
<div class="variable-item">{customer_email} - 고객 이메일</div>
|
||||
<div class="variable-item">{reservation_date} - 예약 날짜</div>
|
||||
<div class="variable-item">{reservation_time} - 예약 시간</div>
|
||||
<div class="variable-item">{payment_amount} - 상담 비용</div>
|
||||
<div class="variable-item">{account_info} - 입금 계좌</div>
|
||||
<div class="variable-item">{cancel_reason} - 취소 사유</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 템플릿 편집 폼 -->
|
||||
<?php if (isset($templates[$current_template_key])): ?>
|
||||
<?php $template = $templates[$current_template_key]; ?>
|
||||
<form method="post" class="template-form">
|
||||
<input type="hidden" name="action" value="save_template">
|
||||
<input type="hidden" name="template_key" value="<?php echo $current_template_key; ?>">
|
||||
<input type="hidden" name="template_name" value="<?php echo htmlspecialchars($template['template_name']); ?>">
|
||||
<input type="hidden" name="template_type" value="<?php echo $current_type; ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_subject"><?php echo ($current_type == 'sms') ? '문자 제목 (LMS용)' : '이메일 제목'; ?></label>
|
||||
<input type="text" id="template_subject" name="template_subject"
|
||||
value="<?php echo htmlspecialchars($template['template_subject']); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template_content"><?php echo ($current_type == 'sms') ? '문자 내용' : '이메일 내용'; ?></label>
|
||||
<textarea id="template_content" name="template_content" required><?php echo htmlspecialchars($template['template_content']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<!-- 💡 [개선] 미리보기 버튼을 JS로 생성하는 대신 HTML에 직접 추가하여 안정성을 높이고, 저장 버튼과 나란히 배치합니다. -->
|
||||
<button type="button" onclick="previewTemplate()" class="btn btn-secondary" style="margin-right: 10px;">미리보기</button>
|
||||
<button type="submit" class="btn btn-primary">템플릿 저장</button>
|
||||
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 변수 삽입 도우미
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const textarea = document.getElementById('template_content');
|
||||
const variableItems = document.querySelectorAll('.variable-item');
|
||||
|
||||
variableItems.forEach(item => {
|
||||
item.style.cursor = 'pointer';
|
||||
item.title = '클릭하여 템플릿에 삽입';
|
||||
|
||||
item.addEventListener('click', function() {
|
||||
const variable = this.textContent.split(' - ')[0];
|
||||
const cursorPos = textarea.selectionStart;
|
||||
const textBefore = textarea.value.substring(0, cursorPos);
|
||||
const textAfter = textarea.value.substring(cursorPos);
|
||||
|
||||
textarea.value = textBefore + variable + textAfter;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(cursorPos + variable.length, cursorPos + variable.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 템플릿 미리보기
|
||||
function previewTemplate() {
|
||||
const subject = document.getElementById('template_subject').value;
|
||||
const content = document.getElementById('template_content').value;
|
||||
|
||||
// 샘플 데이터로 치환
|
||||
const sampleData = {
|
||||
'{customer_name}': '홍길동',
|
||||
'{customer_phone}': '010-1234-5678',
|
||||
'{customer_email}': 'hong@example.com',
|
||||
'{reservation_date}': '2024-12-15',
|
||||
'{reservation_time}': '14:00',
|
||||
'{payment_amount}': '50,000',
|
||||
'{account_info}': '국민은행 123-456-789 (주)상담센터',
|
||||
'{cancel_reason}': '개인 사정'
|
||||
};
|
||||
|
||||
let previewSubject = subject;
|
||||
let previewContent = content;
|
||||
|
||||
for (const [variable, value] of Object.entries(sampleData)) {
|
||||
previewSubject = previewSubject.replace(new RegExp(variable.replace(/[{}]/g, '\\\\$&'), 'g'), value);
|
||||
previewContent = previewContent.replace(new RegExp(variable.replace(/[{}]/g, '\\\\$&'), 'g'), value);
|
||||
}
|
||||
|
||||
// 💡 [개선] 실제 이메일처럼 보이도록 nl2br 처리 및 UI 개선
|
||||
const previewHtmlContent = previewContent.replace(/\n/g, '<br>');
|
||||
|
||||
// 💡 [개선] document.write() 대신 DOM 조작을 사용하여 안정성을 높입니다.
|
||||
const previewWindow = window.open('', '_blank', 'width=800,height=600');
|
||||
const previewDoc = previewWindow.document;
|
||||
|
||||
previewDoc.open();
|
||||
previewDoc.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>미리보기</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f6f8fa; }
|
||||
.preview-container { max-width: 800px; margin: 20px auto; background-color: #fff; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); }
|
||||
.preview-header { padding: 20px; border-bottom: 1px solid #eee; }
|
||||
.preview-header h2 { margin: 0; font-size: 20px; color: #333; }
|
||||
.preview-meta { padding: 15px 20px; background-color: #fdfdfd; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||
.meta-item { display: flex; margin-bottom: 8px; }
|
||||
.meta-label { font-weight: bold; color: #555; width: 80px; }
|
||||
.meta-value { color: #333; }
|
||||
.preview-body { padding: 30px 20px; line-height: 1.7; color: #333; font-size: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<bo`+`dy>
|
||||
<div class='preview-container'>
|
||||
<div class="preview-header"><h2>미리보기</h2></div>
|
||||
<div class="preview-meta">
|
||||
<div class="meta-item"><span class="meta-label">보내는사람:</span><span class="meta-value">관리자 <admin@example.com></span></div>
|
||||
<div class="meta-item"><span class="meta-label">받는사람:</span><span class="meta-value">홍길동 <hong@example.com></span></div>
|
||||
<div class="meta-item"><span class="meta-label">제 목:</span><span class="meta-value">${previewSubject}</span></div>
|
||||
</div>
|
||||
<div class="preview-body">${previewHtmlContent}</div>
|
||||
</div>
|
||||
</bo`+`dy>
|
||||
</html>
|
||||
`);
|
||||
previewWindow.document.close();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 테스트 페이지
|
||||
*/
|
||||
|
||||
// 오류 표시 활성화
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
echo "<h1>상담 예약 시스템 테스트</h1>";
|
||||
|
||||
// 1. 그누보드 기본 파일 로드 테스트
|
||||
echo "<h3>1. 그누보드 기본 파일 로드 테스트</h3>";
|
||||
try {
|
||||
include_once('./_common_con.php');
|
||||
echo "✅ common.php 로드 성공<br>";
|
||||
echo "✅ 데이터베이스 연결: " . (isset($connect_db) ? "성공" : "실패") . "<br>";
|
||||
echo "✅ 관리자 권한: " . ($is_admin ? "있음" : "없음") . "<br>";
|
||||
echo "✅ G5_PATH: " . (defined('G5_PATH') ? G5_PATH : "정의되지 않음") . "<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ common.php 로드 실패: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 2. 함수 존재 확인
|
||||
echo "<h3>2. 필수 함수 존재 확인</h3>";
|
||||
$functions = ['sql_query', 'sql_fetch', 'sql_real_escape_string', 'alert'];
|
||||
foreach ($functions as $func) {
|
||||
echo (function_exists($func) ? "✅" : "❌") . " {$func}<br>";
|
||||
}
|
||||
|
||||
// 3. 상수 확인
|
||||
echo "<h3>3. 필수 상수 확인</h3>";
|
||||
$constants = ['G5_PATH', 'G5_ADMIN_PATH', 'G5_DATA_PATH'];
|
||||
foreach ($constants as $const) {
|
||||
echo (defined($const) ? "✅" : "❌") . " {$const}: " . (defined($const) ? constant($const) : "정의되지 않음") . "<br>";
|
||||
}
|
||||
|
||||
// 4. 데이터베이스 연결 테스트
|
||||
echo "<h3>4. 데이터베이스 연결 테스트</h3>";
|
||||
try {
|
||||
$sql = "SELECT 1 as test";
|
||||
$result = sql_query($sql);
|
||||
if ($result) {
|
||||
echo "✅ 데이터베이스 쿼리 성공<br>";
|
||||
} else {
|
||||
echo "❌ 데이터베이스 쿼리 실패<br>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ 데이터베이스 오류: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 5. 테이블 존재 확인
|
||||
echo "<h3>5. 상담 시스템 테이블 확인</h3>";
|
||||
$tables = ['consultant_config', 'consultant_schedule', 'consultant_reservations'];
|
||||
foreach ($tables as $table) {
|
||||
$sql = "SHOW TABLES LIKE '{$table}'";
|
||||
$result = sql_query($sql, false);
|
||||
$exists = $result && sql_num_rows($result) > 0;
|
||||
echo ($exists ? "✅" : "❌") . " {$table}<br>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<p><a href='install_simple.php'>간단 설치 페이지로 이동</a></p>";
|
||||
echo "<p><a href='../install.php'>원본 설치 페이지로 이동</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* 상담 예약 시스템 설치 테스트
|
||||
*/
|
||||
|
||||
// 오류 표시 활성화
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
echo "<h1>상담 예약 시스템 설치 테스트</h1>";
|
||||
|
||||
// 1. 그누보드 기본 파일 로드 테스트
|
||||
echo "<h3>1. 그누보드 기본 파일 로드 테스트</h3>";
|
||||
try {
|
||||
include_once('../../common.php');
|
||||
echo "✅ common.php 로드 성공<br>";
|
||||
echo "✅ 데이터베이스 연결: " . (isset($connect_db) ? "성공" : "실패") . "<br>";
|
||||
echo "✅ 관리자 권한: " . ($is_admin ? "있음" : "없음") . "<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ common.php 로드 실패: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 2. _common.php 로드 테스트
|
||||
echo "<h3>2. _common.php 로드 테스트</h3>";
|
||||
try {
|
||||
include_once('./_common.php');
|
||||
echo "✅ _common.php 로드 성공<br>";
|
||||
echo "✅ 상담 시스템 버전: " . (defined('G5_CONSULTANT_VERSION') ? G5_CONSULTANT_VERSION : "정의되지 않음") . "<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ _common.php 로드 실패: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
// 3. 함수 존재 확인
|
||||
echo "<h3>3. 필수 함수 존재 확인</h3>";
|
||||
$functions = ['sql_query', 'sql_fetch', 'sql_real_escape_string', 'alert'];
|
||||
foreach ($functions as $func) {
|
||||
echo (function_exists($func) ? "✅" : "❌") . " {$func}<br>";
|
||||
}
|
||||
|
||||
// 4. 상수 확인
|
||||
echo "<h3>4. 필수 상수 확인</h3>";
|
||||
$constants = ['G5_PATH', 'G5_ADMIN_PATH', 'G5_DATA_PATH'];
|
||||
foreach ($constants as $const) {
|
||||
echo (defined($const) ? "✅" : "❌") . " {$const}: " . (defined($const) ? constant($const) : "정의되지 않음") . "<br>";
|
||||
}
|
||||
|
||||
// 5. 데이터베이스 연결 테스트
|
||||
echo "<h3>5. 데이터베이스 연결 테스트</h3>";
|
||||
try {
|
||||
$sql = "SELECT 1 as test";
|
||||
$result = sql_query($sql);
|
||||
if ($result) {
|
||||
echo "✅ 데이터베이스 쿼리 성공<br>";
|
||||
} else {
|
||||
echo "❌ 데이터베이스 쿼리 실패<br>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "❌ 데이터베이스 오류: " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
echo "<p><a href='../install.php'>설치 페이지로 이동</a></p>";
|
||||
?>
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* 스케줄 생성 테스트 페이지
|
||||
*/
|
||||
|
||||
include_once('./_common.php');
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$is_admin) {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
}
|
||||
|
||||
// 설치 확인
|
||||
if (!is_consultant_installed()) {
|
||||
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
||||
}
|
||||
|
||||
$g5['title'] = '스케줄 생성 테스트';
|
||||
|
||||
// 테스트 실행
|
||||
if ($_POST['action'] == 'test_generation') {
|
||||
$year = (int) ($_POST['year'] ?? date('Y'));
|
||||
$month = (int) ($_POST['month'] ?? date('n'));
|
||||
|
||||
try {
|
||||
require_once('classes/ScheduleGenerator.class.php');
|
||||
$generator = new ScheduleGenerator();
|
||||
|
||||
echo "<h3>테스트 결과</h3>";
|
||||
echo "<p><strong>대상:</strong> {$year}년 {$month}월</p>";
|
||||
|
||||
// 기존 스케줄 확인
|
||||
$existing = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
|
||||
echo "<p><strong>기존 스케줄:</strong> {$existing['count']}개</p>";
|
||||
|
||||
// 충돌 검사
|
||||
// 💡 [수정] 최신 충돌 검사 로직으로 변경
|
||||
$conflicts = $generator->findConflictsWithNewSettings($year, $month);
|
||||
echo "<p><strong>충돌 검사:</strong> " . count($conflicts) . "건</p>";
|
||||
|
||||
if (!empty($conflicts)) {
|
||||
echo "<ul>";
|
||||
foreach ($conflicts as $conflict) {
|
||||
echo "<li>{$conflict['date']} {$conflict['time']} - {$conflict['customer']} ({$conflict['reason']})</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
|
||||
// 스케줄 생성
|
||||
$result = $generator->generateMonth($year, $month);
|
||||
|
||||
if ($result) {
|
||||
$new_count = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
|
||||
echo "<p style='color: green;'><strong>✅ 성공:</strong> {$new_count['count']}개 스케줄 생성 완료</p>";
|
||||
|
||||
// 생성된 스케줄 샘플 표시
|
||||
$samples = sql_query("SELECT * FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} ORDER BY specific_date, start_time LIMIT 10");
|
||||
echo "<h4>생성된 스케줄 샘플 (최대 10개)</h4>";
|
||||
echo "<table border='1' style='border-collapse: collapse; width: 100%;'>";
|
||||
echo "<tr><th>날짜</th><th>시작시간</th><th>종료시간</th><th>최대인원</th><th>사용가능</th><th>타입</th></tr>";
|
||||
while ($row = sql_fetch_array($samples)) {
|
||||
$available = $row['is_available'] ? '가능' : '불가능';
|
||||
$type = $row['temp_1'] ?? '일반';
|
||||
echo "<tr>";
|
||||
echo "<td>{$row['specific_date']}</td>";
|
||||
echo "<td>{$row['start_time']}</td>";
|
||||
echo "<td>{$row['end_time']}</td>";
|
||||
echo "<td>{$row['max_persons']}</td>";
|
||||
echo "<td>{$available}</td>";
|
||||
echo "<td>{$type}</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
echo "</table>";
|
||||
} else {
|
||||
echo "<p style='color: red;'><strong>❌ 실패:</strong> 스케줄 생성에 실패했습니다.</p>";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "<p style='color: red;'><strong>오류:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
|
||||
}
|
||||
|
||||
echo "<hr>";
|
||||
}
|
||||
|
||||
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.test-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="test-container">
|
||||
<h2><?php echo $g5['title']; ?></h2>
|
||||
|
||||
<div class="test-form">
|
||||
<h3>스케줄 생성 테스트</h3>
|
||||
<p>선택한 년월의 스케줄을 생성하고 결과를 확인합니다.</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="test_generation">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="year">년도</label>
|
||||
<input type="number" id="year" name="year" value="<?php echo date('Y'); ?>" min="2024" max="2030">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="month">월</label>
|
||||
<select id="month" name="month">
|
||||
<?php for ($i = 1; $i <= 12; $i++): ?>
|
||||
<option value="<?php echo $i; ?>" <?php echo $i == date('n') ? 'selected' : ''; ?>>
|
||||
<?php echo $i; ?>월
|
||||
</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-primary">스케줄 생성 테스트</button>
|
||||
<a href="../settings.php" class="btn btn-secondary">설정으로 돌아가기</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
||||
?>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user