diff --git a/.env.sample b/.env.sample
new file mode 100644
index 0000000..b914814
--- /dev/null
+++ b/.env.sample
@@ -0,0 +1,16 @@
+# DB 접속 정보
+DB_HOST=db
+DB_PORT=5432
+DB_NAME=kngil
+DB_USER=postgres
+DB_PASS=postgres
+
+# 참고: kngil_DB 덤프의 소유자가 postgres이므로 DB_USER 변경 시 초기 복원 실패 가능
+
+# OIDC 설정
+OIDC_ISSUER=
+OIDC_CLIENT_ID=
+OIDC_CLIENT_SECRET=
+OIDC_REDIRECT_URL=
+OIDC_SCOPES=openid profile email
+IDP_SERVICE_URL=
diff --git a/.gitea/workflows/deploy-registry.yml b/.gitea/workflows/deploy-registry.yml
new file mode 100644
index 0000000..459fac5
--- /dev/null
+++ b/.gitea/workflows/deploy-registry.yml
@@ -0,0 +1,54 @@
+name: Deploy (registry)
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ vars.HARBOR_ENDPOINT }}
+ username: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
+ password: ${{ secrets.HARBOR_ROBOT_KEY }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: |
+ ${{ vars.HARBOR_ENDPOINT }}/${{ vars.IMAGE_NAME }}:latest
+ ${{ vars.HARBOR_ENDPOINT }}/${{ vars.IMAGE_NAME }}:${{ github.sha }}
+
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ vars.SSH_HOST }}
+ username: ${{ vars.SSH_USER }}
+ key: ${{ secrets.SSH_KEY }}
+ port: ${{ vars.SSH_PORT }}
+ script: |
+ cd ${{ secrets.DEPLOY_PATH }}
+ cat << 'ENVEOF' > .env
+ ${{ secrets.DEPLOY_ENV_FILE }}
+ ENVEOF
+
+ # Export variables from .env file
+ set -a
+ source .env
+ set +a
+
+ docker compose --env-file .env pull
+ docker compose --env-file .env up -d
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..5b7c6b7
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,54 @@
+name: Deploy (build on server)
+
+on:
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ vars.SSH_HOST }}
+ username: ${{ vars.SSH_USER }}
+ key: ${{ secrets.SSH_KEY }}
+ port: ${{ vars.SSH_PORT }}
+ script: |
+ set -e
+ cd ${{ vars.DEPLOY_PATH }}
+ # if [ ! -d .git ]; then
+ # git init
+ # git remote add origin ssh://git@127.0.0.1:222/b24014/kngil_home.git
+ # else
+ # git remote set-url origin ssh://git@127.0.0.1:222/b24014/kngil_home.git
+ # fi
+ cat << 'ENVEOF' > .env
+ WEB_HOST_PORT=${{ vars.WEB_HOST_PORT }}
+ DB_HOST=${{ vars.DB_HOST }}
+ DB_PORT=${{ vars.DB_PORT }}
+ DB_HOST_PORT=${{ vars.DB_HOST_PORT }}
+ DB_NAME=${{ vars.DB_NAME }}
+ DB_USER=${{ vars.DB_USER }}
+ DB_PASS=${{ secrets.DB_PASS }}
+ OIDC_ISSUER=${{ vars.OIDC_ISSUER }}
+ OIDC_CLIENT_ID=${{ vars.OIDC_CLIENT_ID }}
+ OIDC_CLIENT_SECRET=${{ secrets.OIDC_CLIENT_SECRET }}
+ OIDC_REDIRECT_URL=${{ vars.OIDC_REDIRECT_URL }}
+ OIDC_SCOPES=${{ vars.OIDC_SCOPES }}
+ IDP_SERVICE_URL=${{ vars.IDP_SERVICE_URL }}
+ ENVEOF
+
+ # Export variables from .env file
+ set -a
+ source .env
+ set +a
+
+ git fetch origin main
+ git checkout -B main origin/main
+ git pull --ff-only
+
+ docker compose --env-file .env up -d --build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b5fa9f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.env
+/kngil/vendor/
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..17c9d95
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,23 @@
+RewriteEngine On
+
+# Skip existing files and directories.
+RewriteCond %{REQUEST_FILENAME} -f [OR]
+RewriteCond %{REQUEST_FILENAME} -d
+RewriteRule ^ - [L]
+
+# Admin UI
+RewriteRule ^admin/?$ /kngil/skin/adm.php [L]
+RewriteRule ^admin/company/?$ /kngil/skin/adm_comp.php [L]
+
+# Admin APIs
+RewriteRule ^admin/api/super/?$ /kngil/bbs/adm.php [QSA,L]
+RewriteRule ^admin/api/company/?$ /kngil/bbs/adm_comp.php [QSA,L]
+RewriteRule ^admin/api/service/?$ /kngil/bbs/adm_service.php [QSA,L]
+RewriteRule ^admin/api/purchase-history/?$ /kngil/bbs/adm_purch_popup.php [QSA,L]
+RewriteRule ^admin/api/use-history/?$ /kngil/bbs/adm_use_history.php [QSA,L]
+RewriteRule ^admin/api/product/?$ /kngil/bbs/adm_product_popup.php [QSA,L]
+RewriteRule ^admin/api/product/save/?$ /kngil/bbs/adm_product_popup_save.php [QSA,L]
+RewriteRule ^admin/api/product/delete/?$ /kngil/bbs/adm_product_popup_delete.php [QSA,L]
+RewriteRule ^admin/api/faq/?$ /kngil/bbs/adm_faq_popup.php [QSA,L]
+RewriteRule ^admin/api/faq/save/?$ /kngil/bbs/adm_faq_popup_save.php [QSA,L]
+RewriteRule ^admin/api/faq/delete/?$ /kngil/bbs/adm_faq_popup_delete.php [QSA,L]
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0d57789
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+FROM php:8.2-apache
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends git unzip libpq-dev \
+ && docker-php-ext-install pdo_pgsql \
+ && a2enmod rewrite \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY docker/apache.conf /etc/apache2/conf-available/kngil.conf
+RUN a2enconf kngil
+
+COPY docker/entrypoint.sh /usr/local/bin/kngil-entrypoint.sh
+RUN chmod +x /usr/local/bin/kngil-entrypoint.sh
+ENTRYPOINT ["/usr/local/bin/kngil-entrypoint.sh"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..23b4325
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+# KNGIL 로컬 실행 (Docker Compose)
+
+## 빠른 시작
+```bash
+docker compose up -d --build
+```
+- 접속: `http://localhost:8080`
+```bash
+docker compose down
+```
+
+## 환경변수
+`docker-compose.yml`에서 기본값을 사용하며, 필요 시 `.env`로 덮어쓸 수 있습니다.
+
+- `DB_HOST` (기본값: `db`)
+- `DB_PORT` (기본값: `5432`) - 웹 컨테이너가 DB에 접속할 때 사용하는 포트
+- `DB_HOST_PORT` (기본값: `5432`) - 외부에서 포트포워딩으로 접속할 때 사용하는 호스트 포트
+- `DB_NAME` (기본값: `kngil`)
+- `DB_USER` (기본값: `postgres`)
+- `DB_PASS` (기본값: `postgres`)
+
+> 참고: `kngil_DB` 덤프가 `postgres` 소유자를 사용하므로, `DB_USER`를 변경하면 초기 복원에 실패할 수 있습니다.
+
+### OIDC (선택)
+로그인에서 OIDC를 사용하려면 아래 환경변수를 설정하세요.
+
+- `OIDC_ISSUER`
+- `OIDC_CLIENT_ID`
+- `OIDC_CLIENT_SECRET`
+- `OIDC_REDIRECT_URL`
+- `OIDC_SCOPES` (예: `openid profile email`)
+- `IDP_SERVICE_URL` (예: `https://idp.example.com`)
+
+## DB 초기화
+- `kngil_DB` 덤프는 **처음 실행 시** 자동으로 로드됩니다.
+- 이미 생성된 볼륨이 있으면 재적용되지 않습니다. 다시 초기화하려면 아래를 실행하세요.
+
+```bash
+docker compose down -v
+```
+
+## 덤프 호환성
+- `docker/initdb/01_kngil_DB.sql`는 PostgreSQL 16에서도 동작하도록 `transaction_timeout` 설정을 제거한 버전입니다.
+- 원본 덤프는 `kngil_DB`에 그대로 보관됩니다.
+
+## 기능 비활성화
+- MySQL 의존 기능은 현재 제외했습니다.
+- `kngil/bbs/sales_results.php`는 410 응답으로 비활성 처리되어 있습니다.
+
+## PostgreSQL 이미지 버전
+- 기본값은 `postgres:16`입니다.
+- 이미지 풀 실패 시 `docker-compose.yml`의 태그를 사용 가능한 버전으로 변경하세요.
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..7e91d19
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,45 @@
+services:
+ web:
+ build: .
+ ports:
+ - "${WEB_HOST_PORT:-8080}:80"
+ volumes:
+ - ./:/var/www/html
+ environment:
+ DB_HOST: ${DB_HOST:-db}
+ DB_PORT: ${DB_PORT:-5432}
+ DB_NAME: ${DB_NAME:-kngil}
+ DB_USER: ${DB_USER:-postgres}
+ DB_PASS: ${DB_PASS:-postgres}
+ OIDC_ISSUER: ${OIDC_ISSUER:-}
+ OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
+ OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
+ OIDC_REDIRECT_URL: ${OIDC_REDIRECT_URL:-}
+ OIDC_SCOPES: ${OIDC_SCOPES:-}
+ IDP_SERVICE_URL: ${IDP_SERVICE_URL:-}
+ restart: always
+ depends_on:
+ - db
+
+ db:
+ image: postgres:16
+ ports:
+ - "0.0.0.0:${DB_HOST_PORT:-5432}:5432"
+ command:
+ - "postgres"
+ - "-c"
+ - "listen_addresses=*"
+ - "-c"
+ - "hba_file=/etc/postgresql/pg_hba.conf"
+ environment:
+ POSTGRES_DB: ${DB_NAME:-kngil}
+ POSTGRES_USER: ${DB_USER:-postgres}
+ POSTGRES_PASSWORD: ${DB_PASS:-postgres}
+ volumes:
+ - db_data:/var/lib/postgresql/data
+ - ./docker/initdb/01_kngil_DB.sql:/docker-entrypoint-initdb.d/01_kngil_DB.sql:ro
+ - ./docker/postgres/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
+ restart: always
+
+volumes:
+ db_data:
diff --git a/docker/apache.conf b/docker/apache.conf
new file mode 100644
index 0000000..ede4140
--- /dev/null
+++ b/docker/apache.conf
@@ -0,0 +1,6 @@
+ServerName localhost
+
+
답글 0 Y m24031 2026-01-29 14:12:20.625501+09 m24031 2026-01-29 14:26:04.837518+09
+14 ㅇㅇㅇㅇ343 ㅇㅁㅇㄹㅇㅇㄹㅇㄹㅇㄹ 0 Y m24031 2026-02-02 11:22:02.315133+09 m24031 2026-02-02 11:22:07.740956+09
+\.
+
+
+--
+-- TOC entry 5022 (class 0 OID 16427)
+-- Dependencies: 221
+-- Data for Name: item; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.item (itm_cd, itm_nm, area, itm_amt, use_yn, rmks, cid, cdt, mid, mdt) FROM stdin;
+CGD01 골드 20000 10000000.00 Y test m24031 2026-01-14 17:55:27.062562 m24031 2026-01-14 17:55:27.062562
+ZET01 서비스 1 0.00 Y test m24031 2026-01-14 17:56:47.427489 m24031 2026-01-14 17:56:47.427489
+BSV01 실버 10000 5000000.00 Y test m24031 2026-01-14 17:54:53.937187 m24031 2026-01-14 17:58:17.831158
+BBBB TEST23 4 2.00 Y m24031 2026-01-23 14:28:12.793976 m24031 2026-01-23 15:23:19.231161
+DDA01 다이아 30000 30000000.00 Y testㅇㅇㅇ m24031 2026-01-14 17:56:22.996253 m24031 2026-01-29 13:50:16.978103
+A0000 회원가입 제공 3000 0.00 Y m24031 2026-01-30 10:01:26.15998 m24031 2026-01-30 10:01:26.15998
+\.
+
+
+--
+-- TOC entry 5026 (class 0 OID 16484)
+-- Dependencies: 225
+-- Data for Name: login_history; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.login_history (user_id, sq_no, public_ip, local_ip, login_tm) FROM stdin;
+\.
+
+
+--
+-- TOC entry 5021 (class 0 OID 16414)
+-- Dependencies: 220
+-- Data for Name: members; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.members (member_id, member_nm, co_bc, bs_no, co_nm, co_tel, tel_no, email, join_dt, end_dt, buy_area, use_area, stat_bc, memo, cid, cdt, mid, mdt) FROM stdin;
+b25027 김수현 CB100100 223-33-44445 한맥기술 \N 010-5645-5153 b25027@hanmaceng.co.kr 2026-01-29 \N \N \N SA100100 \N b25027 2026-01-29 11:25:14.184682 b24014 2026-01-29 16:21:12.842875
+B260001 바론 관리자 SA150100 222222222 바론 컨설턴트 \N 010-1111-1111 sdi@sdi.com 2026-01-01 \N 140000 1200 SA100100 \N \N \N SYSTEM 2026-01-21 15:45:40.131991
+tester001 테스터10 SA150100 \N 기업1 \N 010-1111-1111 111@gmail.com 2026-01-20 \N 3000 \N SA100100 \N m24031 2026-01-20 19:50:23.029203 \N \N
+ctest004 c테스터33 SA150200 \N 기업3 \N 010-3333-3333 333@gmail.com 2026-01-20 \N 1000 \N SA100100 \N m24031 2026-01-20 19:57:42.806965 \N \N
+sdi9429 송대일 CB100100 \N 바론 \N 010-8627-0921 sdi9429@naver.com 2026-01-22 \N 1000 \N SA100100 \N sdi9429 2026-01-22 12:56:43.651431 \N \N
+tester003 테스터33 SA150200 222222222 기업3 \N 010-3333-3333 333@gmail.com 2026-01-20 \N 2000 \N SA100100 \N m24031 2026-01-20 19:52:19.322838 m24031 2026-01-23 16:50:12.264163
+tester002 테스터10 SA150100 222222 기업1 \N 010-1111-1111 111@gmail.com 2026-01-20 \N 2000 \N SA100100 \N m24031 2026-01-20 19:51:18.809202 m24031 2026-01-27 10:44:31.258757
+sdisdi 송대일 CB100100 \N 바론11 \N 010-8627-0923 sdi9429@naver.com 2026-01-22 \N 0 \N SA100100 \N sdisdi 2026-01-22 13:12:29.376769 m24031 2026-01-27 10:44:47.776389
+Km24031 권오재 CB100100 \N 한맥기술 \N 010-9114-3944 koj111@naver.com 2026-01-30 \N \N \N SA100100 \N Km24031 2026-01-30 09:02:15.195496 \N \N
+am24031 권오재A CB100100 \N 한맥 \N 010-2222-2222 ddd@gmail.com 2026-01-30 \N 3000 \N SA100100 \N am24031 2026-01-30 13:25:57.005379 \N \N
+\.
+
+
+--
+-- TOC entry 5029 (class 0 OID 16594)
+-- Dependencies: 228
+-- Data for Name: qa_attachments; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.qa_attachments (post_id, ori_name, save_path, file_size, uploaded_at, id) FROM stdin;
+7 traffic_image.jpg /kngil/uploads/qa/1769509499_417330b6f49e.jpg 123753 2026-01-27 19:24:59.134487+09 1
+8 traffic_image.jpg /kngil/uploads/qa/1769513369_959a050cb972.jpg 123753 2026-01-27 20:29:29.973998+09 2
+\.
+
+
+--
+-- TOC entry 5035 (class 0 OID 16627)
+-- Dependencies: 234
+-- Data for Name: qa_comment_images; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.qa_comment_images (id, comment_id, file_name, file_path, thumb_path, file_size, uploaded_at) FROM stdin;
+1 2 og-main-thumb.JPG /kngil/uploads/comment/1769511961_dda23bd3.jpg /kngil/uploads/comment/1769511961_dda23bd3.jpg 112930 2026-01-27 20:06:01.050825+09
+\.
+
+
+--
+-- TOC entry 5033 (class 0 OID 16618)
+-- Dependencies: 232
+-- Data for Name: qa_comments; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.qa_comments (comment_id, post_id, commenter, content, cdt_dt, user_nm, mdt_dt) FROM stdin;
+2 7 b24014 test 2026-01-27 20:06:01.050825+09 송대일1 \N
+\.
+
+
+--
+-- TOC entry 5031 (class 0 OID 16603)
+-- Dependencies: 230
+-- Data for Name: qa_posts; Type: TABLE DATA; Schema: kngil; Owner: postgres
+--
+
+COPY kngil.qa_posts (post_id, tel_no, user_id, user_nm, category, co_nm, dept_nm, title, content, attachment, stat_bc, complete_form, cdt_dt, mid_dt, is_secret, is_read_admin) FROM stdin;
+7 \N b24014 송대일1 오류문의 \N \N 테스트
1234
\N review 0 2026-01-27 19:24:59.131617+09 2026-01-27 20:14:21.993651+09 0 Y +8 \N b24014 송대일1 오류문의 \N \N 첨부파일 업로드123
\N deep 0 2026-01-27 20:29:29.970856+09 2026-01-27 20:32:21.107049+09 0 Y +10 010-8627-0921 b24014 송대일1 오류문의 바론 컨설턴트 총괄기획실 test1qasdweqwe
\N review 0 2026-01-28 10:43:24.655249+09 2026-01-28 11:00:56.361487+09 Y Y +1 오류문의 11113333
\N wait 0 2026-01-27 14:09:56.639903+09 \N Y 0 +2 \N b24014 송대일1 오류문의 \N \N 12343333
\N wait 0 2026-01-27 15:56:13.097696+09 \N 0 N +5 \N b24014 송대일1 오류문의 \N \N 123456
\N wait 0 2026-01-27 16:40:55.238147+09 \N 0 N +9 \N b24014 송대일1 오류문의 \N \N 최종테스트11111333
\N wait 0 2026-01-28 10:09:07.943555+09 \N 0 Y +6 \N b24014 송대일1 오류문의 \N \N 첨부파일 업로드123
\N wait 0 2026-01-27 19:17:08.118218+09 \N 0 Y +4 \N b24014 송대일1 개선문의 \N \N teststests
\N wait 0 2026-01-27 16:00:43.415892+09 \N 0 Y +12 010-8627-0921 b24014 송대일1 일반문의 바론 컨설턴트 총괄기획실 테스트 작성글ㅅㅅㅅㅅㅅㅅㅅ
\N wait 0 2026-01-30 16:10:49.845849+09 2026-02-02 09:32:40.063076+09 N Y +11 010-8627-0921 b24014 송대일1 오류문의 바론 컨설턴트 총괄기획실 11113123123
\N wait 0 2026-01-30 16:10:30.821517+09 2026-02-02 09:32:54.632593+09 N Y +13 010-8627-0921 b24014 송대일1 오류문의 바론 컨설턴트 총괄기획실 테스트 중입니다3333444
\N wait 0 2026-02-02 09:33:22.636799+09 \N N Y +14 010-8627-0921 b24014 송대일1 공지사항 바론 컨설턴트 총괄기획실 공지 테스트1234
\N wait 0 2026-02-02 09:40:50.464071+09 \N N Y +\. + + +-- +-- TOC entry 5025 (class 0 OID 16471) +-- Dependencies: 224 +-- Data for Name: use_history; Type: TABLE DATA; Schema: kngil; Owner: postgres +-- + +COPY kngil.use_history (user_id, sq_no, use_dt, use_area, ser_bc, cid, cdt, mid, mdt) FROM stdin; +m24031 1 2026-01-18 100 SA200100 \N \N \N \N +test001 1 2026-01-18 200 SA200100 \N \N \N \N +test001 2 2026-01-19 200 SA200100 \N \N \N \N +m24031 2 2026-01-19 200 SA200300 \N \N \N \N +m24031 3 2026-01-19 300 SA200200 \N \N \N \N +m24031 4 2026-01-19 100 SA200300 \N \N \N \N +m24031 5 2026-01-19 20000 SA200300 \N \N \N \N +m24031 6 2026-01-19 1000 SA200200 \N \N \N \N +m24031 7 2025-01-19 20000 SA200100 \N \N \N \N +\. + + +-- +-- TOC entry 5023 (class 0 OID 16433) +-- Dependencies: 222 +-- Data for Name: users; Type: TABLE DATA; Schema: kngil; Owner: postgres +-- + +COPY kngil.users (member_id, user_id, user_pw, user_nm, dept_nm, posit_nm, tel_no, email, auth_bc, use_yn, rmks, cid, cdt, mid, mdt, oidc_sub) FROM stdin; +B260001 test003 test003 테스터3 기술개발센터 수석연구원 010-1111-3333 test@test.com BS100400 N test 입니다. m24031 2026-01-14 18:39:43.030024 m24031 2026-01-14 18:42:22.488198 \N +sdisdi song12124 test1234!!@ 테스터4 erp 1012346770 test@test.co.kr BS100500 Y song12124 2026-01-30 11:12:14.172743 song12124 2026-01-30 11:12:14.172743 \N +tester002 tester002 tester002 류호성 전산실 \N 010-3371-5649 111@gmail.com BS100100 Y \N m24031 2026-01-20 19:51:18.809202 m24031 2026-01-27 10:44:31.258757 \N +sdisdi song12125 test1234!!@ 테스터5 erp 1012346771 test@test.co.kr BS100500 Y song12125 2026-01-30 11:12:14.184522 song12125 2026-01-30 11:12:14.184522 \N +ctest004 ctest004 ctest004 권오재2 전산실 \N 010-9114-3943 333@gmail.com BS100300 N \N m24031 2026-01-20 19:57:42.806965 \N \N \N +B260001 sdi1108 sdi1108 어드민 ERP팀ㅁ 010-1111-1112 test@test1.com BS100400 Y \N 2026-01-15 20:08:48.731648 m24031 2026-01-29 13:29:52.388359 \N +sdisdi song12126 test1234!!@ 테스터6 erp 1012346772 test@test.co.kr BS100500 Y song12126 2026-01-30 11:18:04.916446 song12126 2026-01-30 11:18:04.916446 \N +sdisdi song12127 test1234!!@ 테스터7 erp 1012346773 test@test.co.kr BS100500 Y song12127 2026-01-30 11:18:04.921195 song12127 2026-01-30 11:18:04.921195 \N +sdisdi song12128 test1234!!@ 테스터8 erp 1012346774 test@test.co.kr BS100500 Y song12128 2026-01-30 12:53:06.276828 song12128 2026-01-30 12:53:06.276828 \N +sdisdi song12129 test1234!!@ 테스터9 erp 1012346775 test@test.co.kr BS100500 Y song12129 2026-01-30 12:53:06.284774 song12129 2026-01-30 12:53:06.284774 \N +sdisdi song12130 test1234!!@ 테스터10 erp 1012346776 test@test.co.kr BS100500 Y song12130 2026-01-30 13:01:11.566978 song12130 2026-01-30 13:01:11.566978 \N +sdisdi song12131 test1234!!@ 테스터11 erp 1012346777 test@test.co.kr BS100500 Y song12131 2026-01-30 13:01:11.57227 song12131 2026-01-30 13:01:11.57227 \N +am24031 am24031 !rnjsdhwo729 권오재A ERP 기획 \N 010-2222-2222 ddd@gmail.com BS100300 Y \N am24031 2026-01-30 13:25:57.005379 \N \N \N +b25027 b25027 a1357125!@23 김수현 디자인기획팀 \N 010-5645-5153 b25027@hanmaceng.co.kr BS100300 Y \N b25027 2026-01-29 11:25:14.184682 b24014 2026-01-29 16:21:12.842875 \N +B260001 test005 b23008 염승호 총괄기획실 수석 연구원 010-8835-0501 df BS100100 Y m24031 2026-01-29 13:30:30.456963 m24031 2026-01-29 13:30:30.456963 \N +B260001 test002 test002 테스터2 기술개발센터 010-1111-2222 test@test.com BS100400 Y m24031 2026-01-14 18:39:03.432801 m24031 2026-01-29 20:31:27.681975 \N +tester003 btest001 btest001 테스터33 도로부 BS100500 Y test m24031 2026-01-20 19:56:32.467743 m24031 2026-01-20 19:56:32.467743 \N +tester003 btest002 btest002 b테스터22 도로부 BS100500 Y test m24031 2026-01-20 19:56:48.750462 m24031 2026-01-20 19:56:48.750462 \N +B260001 m24031 m24031 권오재 총괄기획실 선임 연구원 010-9114-3943 m24031@hanmaceng.co.kr BS100100 Y \N \N \N \N \N \N +Km24031 Km24031 !rnjsdhwo729 권오재 ERP기획팀 \N 010-9114-3944 koj111@naver.com BS100300 Y \N Km24031 2026-01-30 09:02:15.195496 \N \N \N +sdi9429 sdi9429 test1111 송대일 총괄기획실 \N 010-8627-0922 sdi9429@naver.com BS100300 Y \N sdi9429 2026-01-22 12:56:43.651431 \N \N \N +B260001 b24014 test001 송대일1 총괄기획실 010-8627-0921 b24014@hanmaceng.co.kr\n BS100100 Y 123 \N \N SYSTEM 2026-01-19 16:44:25.814475 \N +tester003 tester003 test001 테스터33 전산실 \N 010-3333-3333 333@gmail.com BS100300 Y \N m24031 2026-01-20 19:52:19.322838 m24031 2026-01-23 16:50:12.264163 \N +B260001 test001 test001 회사관리자 총괄기획실1 010-3189-1514 sdi1111@sdi.com BS100300 Y 권한위임[sdi1108] \N \N m24031 2026-01-22 17:11:58.275998 \N +B260001 test0005 test0005 테스트55 총괄기획실1 010-4158-5840 b24000@hanmaceng.co.kr BS100400 Y 비고 입력 테스트 SYSTEM 2026-01-19 19:39:10.476205 SYSTEM 2026-01-19 19:39:10.476205 \N +tester001 tester001 test001 테스터10 전산실 \N 010-9523-0055 111@gmail.com BS100300 Y \N m24031 2026-01-20 19:50:23.029203 \N \N \N +sdisdi sdisdi song1108! 송대일 총괄기획실 \N 010-8627-0923 sdi9429@naver.com BS100300 Y \N sdisdi 2026-01-22 13:12:29.376769 m24031 2026-01-27 10:44:47.776389 \N +sdisdi song1212 test1234!!@ 테스터 erp 01012345689 test@test.co.kr BS100500 N song1212 2026-01-30 09:58:45.274688 sdisdi 2026-01-30 10:02:41.963234 \N +sdisdi song12121 test1234!!@ 테스터1 erp 1012346767 test@test.co.kr BS100400 N song12121 2026-01-30 10:05:59.726345 song12121 2026-01-30 10:05:59.726345 \N +sdisdi song12122 test1234!!@ 테스터2 erp 1012346768 test@test.co.kr BS100500 Y song12122 2026-01-30 10:46:04.719717 song12122 2026-01-30 10:46:04.719717 \N +sdisdi song12123 test1234!!@ 테스터3 erp 1012346769 test@test.co.kr BS100500 Y song12123 2026-01-30 10:46:04.729306 song12123 2026-01-30 10:46:04.729306 \N +B260001 sdi111 000000 test123 ert33 010-8623-6564 test@test.com BS100500 Y b24014 2026-01-30 13:02:45.327993 b24014 2026-01-30 14:08:03.710466 \N +B260001 sdisssss 1234asd!! test1232 erpp 010-8686-2323 test@test.com BS100500 Y b24014 2026-01-30 13:40:50.09753 b24014 2026-01-30 14:08:14.578146 \N +B260001 test1010 test1010 test12 erp 010-8888-2222 test@test1.co.kr BS100500 Y b24014 2026-01-30 13:56:43.807999 b24014 2026-01-30 14:18:12.484107 \N +am24031 kwon001 kwon001 권1 dd 010-1111-2111 kwon001 BS100500 Y kwon001 2026-01-30 16:05:35.819863 kwon001 2026-01-30 16:05:35.819863 \N +am24031 kwon002 kwon002 권2 aa 010-1111-2112 kwon002 BS100500 Y kwon002 2026-01-30 16:05:35.846448 kwon002 2026-01-30 16:05:35.846448 \N +\. + + +-- +-- TOC entry 5058 (class 0 OID 0) +-- Dependencies: 236 +-- Name: fa_comments_fa_id_seq; Type: SEQUENCE SET; Schema: kngil; Owner: postgres +-- + +SELECT pg_catalog.setval('kngil.fa_comments_fa_id_seq', 14, true); + + +-- +-- TOC entry 5059 (class 0 OID 0) +-- Dependencies: 237 +-- Name: qa_attachments_id_seq; Type: SEQUENCE SET; Schema: kngil; Owner: postgres +-- + +SELECT pg_catalog.setval('kngil.qa_attachments_id_seq', 2, true); + + +-- +-- TOC entry 5060 (class 0 OID 0) +-- Dependencies: 233 +-- Name: qa_comment_images_id_seq; Type: SEQUENCE SET; Schema: kngil; Owner: postgres +-- + +SELECT pg_catalog.setval('kngil.qa_comment_images_id_seq', 1, true); + + +-- +-- TOC entry 5061 (class 0 OID 0) +-- Dependencies: 231 +-- Name: qa_comments_comment_id_seq; Type: SEQUENCE SET; Schema: kngil; Owner: postgres +-- + +SELECT pg_catalog.setval('kngil.qa_comments_comment_id_seq', 2, true); + + +-- +-- TOC entry 5062 (class 0 OID 0) +-- Dependencies: 229 +-- Name: qa_posts_post_id_seq; Type: SEQUENCE SET; Schema: kngil; Owner: postgres +-- + +SELECT pg_catalog.setval('kngil.qa_posts_post_id_seq', 14, true); + + +-- +-- TOC entry 4867 (class 2606 OID 16678) +-- Name: fa_comments fa_comments_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.fa_comments + ADD CONSTRAINT fa_comments_pkey PRIMARY KEY (fa_id); + + +-- +-- TOC entry 4855 (class 2606 OID 16643) +-- Name: login_history login_history_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.login_history + ADD CONSTRAINT login_history_pkey PRIMARY KEY (user_id, sq_no); + + +-- +-- TOC entry 4851 (class 2606 OID 16460) +-- Name: buy_item pk_buy_item; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.buy_item + ADD CONSTRAINT pk_buy_item PRIMARY KEY (member_id, sq_no); + + +-- +-- TOC entry 4859 (class 2606 OID 16532) +-- Name: code_detail pk_code_detail; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.code_detail + ADD CONSTRAINT pk_code_detail PRIMARY KEY (main_cd, sub_cd); + + +-- +-- TOC entry 4857 (class 2606 OID 16504) +-- Name: code_master pk_code_master; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.code_master + ADD CONSTRAINT pk_code_master PRIMARY KEY (main_cd); + + +-- +-- TOC entry 4845 (class 2606 OID 16555) +-- Name: item pk_item; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.item + ADD CONSTRAINT pk_item PRIMARY KEY (itm_cd); + + +-- +-- TOC entry 4843 (class 2606 OID 16426) +-- Name: members pk_members; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.members + ADD CONSTRAINT pk_members PRIMARY KEY (member_id); + + +-- +-- TOC entry 4861 (class 2606 OID 16690) +-- Name: qa_attachments qa_attachments_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.qa_attachments + ADD CONSTRAINT qa_attachments_pkey PRIMARY KEY (id); + + +-- +-- TOC entry 4865 (class 2606 OID 16625) +-- Name: qa_comments qa_comments_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.qa_comments + ADD CONSTRAINT qa_comments_pkey PRIMARY KEY (comment_id); + + +-- +-- TOC entry 4863 (class 2606 OID 16616) +-- Name: qa_posts qa_posts_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.qa_posts + ADD CONSTRAINT qa_posts_pkey PRIMARY KEY (post_id); + + +-- +-- TOC entry 4853 (class 2606 OID 16641) +-- Name: use_history use_history_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.use_history + ADD CONSTRAINT use_history_pkey PRIMARY KEY (user_id, sq_no); + + +-- +-- TOC entry 4847 (class 2606 OID 16721) +-- Name: users users_oidc_sub_key; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.users + ADD CONSTRAINT users_oidc_sub_key UNIQUE (oidc_sub); + + +-- +-- TOC entry 4849 (class 2606 OID 16645) +-- Name: users users_pkey; Type: CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.users + ADD CONSTRAINT users_pkey PRIMARY KEY (user_id); + + +-- +-- TOC entry 4873 (class 2620 OID 16705) +-- Name: buy_item trg_buy_item_changed; Type: TRIGGER; Schema: kngil; Owner: postgres +-- + +CREATE TRIGGER trg_buy_item_changed AFTER INSERT OR DELETE OR UPDATE ON kngil.buy_item FOR EACH ROW EXECUTE FUNCTION kngil.fn_update_buy_area(); + + +-- +-- TOC entry 4872 (class 2606 OID 16533) +-- Name: code_detail fk_code_master_to_code_detail; Type: FK CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.code_detail + ADD CONSTRAINT fk_code_master_to_code_detail FOREIGN KEY (main_cd) REFERENCES kngil.code_master(main_cd) ON DELETE CASCADE; + + +-- +-- TOC entry 4868 (class 2606 OID 16656) +-- Name: users fk_member_to_user; Type: FK CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.users + ADD CONSTRAINT fk_member_to_user FOREIGN KEY (member_id) REFERENCES kngil.members(member_id) NOT VALID; + + +-- +-- TOC entry 4869 (class 2606 OID 16461) +-- Name: buy_item fk_members_to_buy_item; Type: FK CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.buy_item + ADD CONSTRAINT fk_members_to_buy_item FOREIGN KEY (member_id) REFERENCES kngil.members(member_id); + + +-- +-- TOC entry 4871 (class 2606 OID 16651) +-- Name: login_history fk_user_to_login_history; Type: FK CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.login_history + ADD CONSTRAINT fk_user_to_login_history FOREIGN KEY (user_id) REFERENCES kngil.users(user_id) NOT VALID; + + +-- +-- TOC entry 4870 (class 2606 OID 16646) +-- Name: use_history fk_user_to_use_history; Type: FK CONSTRAINT; Schema: kngil; Owner: postgres +-- + +ALTER TABLE ONLY kngil.use_history + ADD CONSTRAINT fk_user_to_use_history FOREIGN KEY (user_id) REFERENCES kngil.users(user_id) NOT VALID; + + +-- Completed on 2026-02-02 14:06:04 + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict osPaC8Gqjay0KBMwX4hwgDvmjwF5rTGmBMzQBdxAne3SBCLMuCNQu2Xg15dPVeb diff --git a/docker/postgres/pg_hba.conf b/docker/postgres/pg_hba.conf new file mode 100644 index 0000000..2ccacfb --- /dev/null +++ b/docker/postgres/pg_hba.conf @@ -0,0 +1,11 @@ +# +# 로컬 소켓 접속 허용(초기화/관리용). 운영에서는 정책에 맞게 조정하세요. +# +local all all trust +host all all 127.0.0.1/32 scram-sha-256 +host all all ::1/128 scram-sha-256 +# +# 외부 TCP 접속 허용. 운영에서는 허용 대역을 제한하세요. +# +host all all 0.0.0.0/0 scram-sha-256 +host all all ::/0 scram-sha-256 diff --git a/index.php b/index.php index 3fe7bac..4df456f 100644 --- a/index.php +++ b/index.php @@ -7,7 +7,7 @@ declare(strict_types=1); // 1. 기본 상수 // --------------------------------- define('ROOT', __DIR__); -define('SKIN_PATH', ROOT.'/skin'); +define('SKIN_PATH', ROOT.'/kngil/skin'); // --------------------------------- // 2. 페이지 결정 diff --git a/kngil/auth/oidc-callback.php b/kngil/auth/oidc-callback.php index 60934e6..eacdee0 100644 --- a/kngil/auth/oidc-callback.php +++ b/kngil/auth/oidc-callback.php @@ -1,12 +1,17 @@ setRedirectURL($config['redirect_url']); try { + $stmt = $pdo->query("SELECT to_regclass('kngil.users') AS reg"); + $reg = $stmt ? $stmt->fetchColumn() : null; + if (!$reg) { + $stmt = $pdo->query("SELECT to_regclass('public.users') AS reg"); + $reg = $stmt ? $stmt->fetchColumn() : null; + if ($reg) { + $usersTable = 'public.users'; + $membersTable = 'public.members'; + } else { + throw new Exception( + "사용자 테이블을 찾을 수 없습니다. DB 초기화가 필요합니다. " + . "docker compose down -v 후 다시 실행하거나, " + . "DB_NAME/DB_USER/DB_PASS 설정을 확인하세요." + ); + } + } + + $memberReg = $pdo->query("SELECT to_regclass('{$membersTable}') AS reg"); + $memberReg = $memberReg ? $memberReg->fetchColumn() : null; + if (!$memberReg) { + $altMembersTable = $membersTable === 'kngil.members' ? 'public.members' : 'kngil.members'; + $memberReg = $pdo->query("SELECT to_regclass('{$altMembersTable}') AS reg"); + $memberReg = $memberReg ? $memberReg->fetchColumn() : null; + if ($memberReg) { + $membersTable = $altMembersTable; + } else { + throw new Exception("회원 테이블을 찾을 수 없습니다. DB 초기화가 필요합니다."); + } + } + + $pdo->exec("ALTER TABLE {$usersTable} ADD COLUMN IF NOT EXISTS oidc_sub VARCHAR(255) UNIQUE"); + if (!$oidc->authenticate()) { throw new Exception("Authentication failed"); } $userInfo = $oidc->requestUserInfo(); + $idToken = $oidc->getIdToken(); + $accessToken = $oidc->getAccessToken(); + $jwtClaims = []; + if (!empty($idToken)) { + $parts = explode('.', $idToken); + if (count($parts) >= 2) { + $payload = strtr($parts[1], '-_', '+/'); + $padding = 4 - (strlen($payload) % 4); + if ($padding < 4) { + $payload .= str_repeat('=', $padding); + } + $decoded = json_decode(base64_decode($payload), true); + if (is_array($decoded)) { + $jwtClaims = $decoded; + } + } + } + + // 디버그용: ID 토큰 확보 여부 로그 출력 (파일) + $logDir = dirname(__DIR__) . '/log'; + if (!is_dir($logDir)) { + @mkdir($logDir, 0775, true); + } + $logPath = $logDir . '/oidc_debug.log'; + if (!is_writable($logDir)) { + $logPath = '/tmp/oidc_debug.log'; + error_log('[OIDC_DEBUG] log_dir_not_writable, fallback=/tmp/oidc_debug.log'); + } + $tokenInfo = empty($idToken) ? 'MISSING' : ('PRESENT len=' . strlen($idToken)); + $claimKeys = empty($jwtClaims) ? 'none' : implode(',', array_keys($jwtClaims)); + $logLine = sprintf( + "[%s] host=%s uri=%s sid=%s id_token=%s claims=%s\n", + date('c'), + $_SERVER['HTTP_HOST'] ?? '-', + $_SERVER['REQUEST_URI'] ?? '-', + session_id(), + $tokenInfo, + $claimKeys + ); + $writeOk = @file_put_contents($logPath, $logLine, FILE_APPEND); + if ($writeOk === false) { + error_log('[OIDC_DEBUG] log_write_failed path=' . $logPath); + } + // 디버그용: userInfo/claims 전체 덤프 (토큰 제외) + $dump = [ + 'userInfo' => $userInfo, + 'jwtClaims' => $jwtClaims + ]; + $dumpLine = sprintf( + "[%s] oidc_dump=%s\n", + date('c'), + json_encode($dump, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + $dumpOk = @file_put_contents($logPath, $dumpLine, FILE_APPEND); + if ($dumpOk === false) { + error_log('[OIDC_DEBUG] dump_write_failed path=' . $logPath); + } + // 도커 로그로도 출력 + error_log('[OIDC_DEBUG] ' . $dumpLine); // $userInfo 에 포함된 데이터 예시: sub, email, name, preferred_username 등 $email = $userInfo->email ?? null; $sub = $userInfo->sub ?? null; // IDP 고유 식별자 - $name = $userInfo->name ?? ($userInfo->preferred_username ?? 'Unknown'); + $preferred = $userInfo->preferred_username ?? null; + $name = $userInfo->name ?? null; + if (!$email && isset($jwtClaims['email'])) { + $email = $jwtClaims['email']; + } + if (!$name && isset($jwtClaims['name'])) { + $name = $jwtClaims['name']; + } + if (!$name && $preferred) { + $name = $preferred; + } + if (!$name && $email) { + $name = $email; + } + if (!$name && $sub) { + $seed = strtolower(preg_replace('/[^a-z0-9]/', '', (string)$sub)); + $name = 'oidc_' . substr($seed, 0, 10); + } if (!$email && !$sub) { throw new Exception("IDP provided insufficient user information."); @@ -33,7 +146,7 @@ try { // 1. 사용자 매핑 (sub 또는 email 기준) $stmt = $pdo->prepare(" - SELECT * FROM kngil.users + SELECT * FROM {$usersTable} WHERE (oidc_sub = :sub OR LOWER(email) = LOWER(:email)) AND use_yn = 'Y' LIMIT 1 @@ -42,14 +155,99 @@ try { $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { - // [정책 선택] 새 사용자 자동 생성 또는 로그인 거부 - // 여기서는 예시로 로그인 거부 처리 - throw new Exception("등록되지 않은 사용자입니다. 관리자에게 문의하세요. (IDP: $email)"); + $defaultMemberId = getenv('OIDC_DEFAULT_MEMBER_ID') ?: ''; + if ($defaultMemberId !== '') { + $checkMember = $pdo->prepare("SELECT 1 FROM {$membersTable} WHERE member_id = :member_id LIMIT 1"); + $checkMember->execute([':member_id' => $defaultMemberId]); + if (!$checkMember->fetchColumn()) { + throw new Exception("OIDC_DEFAULT_MEMBER_ID가 members에 존재하지 않습니다: {$defaultMemberId}"); + } + } else { + $memberStmt = $pdo->query("SELECT member_id FROM {$membersTable} ORDER BY member_id ASC LIMIT 1"); + $defaultMemberId = $memberStmt ? $memberStmt->fetchColumn() : ''; + if (!$defaultMemberId) { + throw new Exception("기본 member_id를 찾을 수 없습니다. OIDC_DEFAULT_MEMBER_ID를 설정하세요."); + } + } + + $defaultAuth = getenv('OIDC_DEFAULT_AUTH_BC') ?: 'BS100500'; + + $baseId = $userInfo->preferred_username ?? ($email ? explode('@', $email)[0] : ''); + $baseId = strtolower(preg_replace('/[^a-z0-9]/', '', $baseId)); + if ($baseId === '') { + $seed = strtolower(preg_replace('/[^a-z0-9]/', '', (string)($sub ?? 'oidc'))); + $baseId = 'oidc' . substr($seed, 0, 10); + } + $baseId = substr($baseId, 0, 16); + $userId = $baseId; + + $existsStmt = $pdo->prepare("SELECT 1 FROM {$usersTable} WHERE LOWER(user_id) = LOWER(:user_id) LIMIT 1"); + $suffix = 1; + while (true) { + $existsStmt->execute([':user_id' => $userId]); + if (!$existsStmt->fetchColumn()) { + break; + } + $tail = sprintf('%02d', $suffix); + $userId = substr($baseId, 0, 20 - strlen($tail)) . $tail; + $suffix++; + if ($suffix > 99) { + $userId = 'oidc' . bin2hex(random_bytes(4)); + $userId = substr($userId, 0, 20); + } + } + + $userNm = $name ?: ($email ?: $userId); + $rawPhone = $userInfo->phone_number ?? ''; + $digits = preg_replace('/\D/', '', $rawPhone); + if (strlen($digits) === 11) { + $telNo = substr($digits, 0, 3) . '-' . substr($digits, 3, 4) . '-' . substr($digits, 7, 4); + } elseif (strlen($digits) === 10) { + $telNo = substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4); + } else { + $telNo = '000-0000-0000'; + } + + $insert = $pdo->prepare(" + INSERT INTO {$usersTable} ( + member_id, user_id, user_pw, user_nm, + dept_nm, posit_nm, tel_no, email, + auth_bc, use_yn, rmks, + cid, cdt, mid, mdt, oidc_sub + ) VALUES ( + :member_id, :user_id, NULL, :user_nm, + :dept_nm, :posit_nm, :tel_no, :email, + :auth_bc, 'Y', :rmks, + :cid, CURRENT_TIMESTAMP, :mid, CURRENT_TIMESTAMP, :oidc_sub + ) + "); + $insert->execute([ + ':member_id' => $defaultMemberId, + ':user_id' => $userId, + ':user_nm' => $userNm, + ':dept_nm' => $userInfo->department ?? null, + ':posit_nm' => $userInfo->title ?? null, + ':tel_no' => $telNo, + ':email' => $email, + ':auth_bc' => $defaultAuth, + ':rmks' => 'OIDC auto-registered', + ':cid' => $userId, + ':mid' => $userId, + ':oidc_sub' => $sub + ]); + + $stmt = $pdo->prepare(" + SELECT * FROM {$usersTable} + WHERE LOWER(user_id) = LOWER(:user_id) + LIMIT 1 + "); + $stmt->execute([':user_id' => $userId]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); } // 2. oidc_sub 업데이트 (최초 연동 시) if (empty($user['oidc_sub']) && $sub) { - $upd = $pdo->prepare("UPDATE kngil.users SET oidc_sub = :sub WHERE user_id = :id"); + $upd = $pdo->prepare("UPDATE {$usersTable} SET oidc_sub = :sub WHERE user_id = :id"); $upd->execute([':sub' => $sub, ':id' => $user['user_id']]); } @@ -63,20 +261,42 @@ try { 'dept_nm' => $user['dept_nm'] ?? null, 'tel_no' => $user['tel_no'] ?? null, 'email' => $user['email'] ?? null, + 'idp_name' => $name ?: null, + 'idp_email' => $email ?? null, + 'idp_id_token' => $idToken ?? null, + 'idp_access_token' => $accessToken ?? null, + 'idp_claims' => $jwtClaims ?? null, 'oidc_mode' => true // OIDC 로그인을 나타내는 플래그 ]; - // 로그인 완료 후 부모 창에 알리고 종료 + session_write_close(); + + // 로그인 완료 후 부모 창에 알리고 종료 (팝업이 아닐 경우 메인으로 이동) ?> + addScope($config['scopes']); // 필요한 경우 PKCE 활성화 // $oidc->setCodeChallengeMethod('S256'); -$oidc->authenticate(); +try { + $oidc->authenticate(); +} catch (Throwable $e) { + error_log($e->getMessage()); + http_response_code(500); + header('Content-Type: text/plain; charset=utf-8'); + echo 'OIDC 인증 중 오류가 발생했습니다.'; + exit; +} diff --git a/kngil/bbs/adm_guard.php b/kngil/bbs/adm_guard.php index 0bdfe26..50b3b35 100644 --- a/kngil/bbs/adm_guard.php +++ b/kngil/bbs/adm_guard.php @@ -7,7 +7,7 @@ if (session_status() === PHP_SESSION_NONE) { 1. 로그인 체크 ========================= */ if (empty($_SESSION['login'])) { - header('Location: /kngil/skin/index.php'); + header('Location: /'); exit; } @@ -33,4 +33,4 @@ if (!in_array($auth_bc, $ALLOW_AUTH, true)) { 3. 권한 플래그 (중요) ========================= */ define('IS_SUPER_ADMIN', in_array($auth_bc, ['BS100100','BS100200'], true)); -define('IS_COMPANY_ADMIN', in_array($auth_bc, ['BS100300','BS100400'], true)); \ No newline at end of file +define('IS_COMPANY_ADMIN', in_array($auth_bc, ['BS100300','BS100400'], true)); diff --git a/kngil/bbs/db_conn.php b/kngil/bbs/db_conn.php index 571dcf8..74b6021 100644 --- a/kngil/bbs/db_conn.php +++ b/kngil/bbs/db_conn.php @@ -1,10 +1,17 @@ PDO::ERRMODE_EXCEPTION ]); // echo "PostgreSQL 연결 성공 🎉"; diff --git a/kngil/bbs/env.php b/kngil/bbs/env.php new file mode 100644 index 0000000..2df1ac5 --- /dev/null +++ b/kngil/bbs/env.php @@ -0,0 +1,59 @@ + 'https://sss.hmac.kr/oidc', // 예: https://idp.example.com/auth/realms/master - 'client_id' => 'cc6f2f41-2705-4c7d-bffc-24c1d6b484f1', - 'client_secret' => 'ebjOE4I-gLnANV5KY623hClSAy', + 'issuer' => 'https://api.descope.com/v1/apps/P2x26KgEwOu0xIwgNZutJjIZc1zz', // 예: https://idp.example.com/auth/realms/master + 'client_id' => 'UDJ4MjZLZ0V3T3UweEl3Z05adXRKaklaYzF6ejpUUEEzOTVtSmx5MXhiczFwZWxrUHdDVFlvU2hiYXc=', + 'client_secret' => 'uTjiKweHYUINalroA1LVu9OacbEEMPtPbfFITfHu3r5', 'redirect_url' => "https://kngil.hmac.kr/kngil/auth/oidc-callback.php", - 'scopes' => ['openid', 'profile', 'email'], + 'scopes' => ['openid'], ]; diff --git a/kngil/bbs/sales_results.php b/kngil/bbs/sales_results.php index 701ac04..bdffcdd 100644 --- a/kngil/bbs/sales_results.php +++ b/kngil/bbs/sales_results.php @@ -3,6 +3,14 @@ ini_set('display_errors', 1); error_reporting(E_ALL); header("Content-Type: application/json; charset=utf-8"); +// 기능 비활성화 (PostgreSQL만 사용) +http_response_code(410); +echo json_encode([ + "status" => "disabled", + "message" => "해당 기능은 현재 비활성화되어 있습니다." +]); +exit; + /* ----------------------------------------------------- 🔵 DB 연결 ----------------------------------------------------- */ diff --git a/kngil/css/common.css b/kngil/css/common.css index 7552fce..2516ff9 100644 --- a/kngil/css/common.css +++ b/kngil/css/common.css @@ -1,5 +1,9 @@ @charset "UTF-8"; @import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap"); +:root { + --color-primary: #f95523; + --color-primary-border: #ca3f14; +} html { font-size: 10px; } @@ -3725,4 +3729,4 @@ i { html { font-size: 8px; } -} \ No newline at end of file +} diff --git a/kngil/js/adm.js b/kngil/js/adm.js index e340c80..7480246 100644 --- a/kngil/js/adm.js +++ b/kngil/js/adm.js @@ -102,7 +102,7 @@ userGrid document.getElementById('detailCard').style.display = 'block' // 하단 사용자 로드 - // fetch(`/kngil/bbs/adm.php?action=user_list&member_id=${record.member_id}`) + // fetch(`/admin/api/super?action=user_list&member_id=${record.member_id}`) // .then(res => res.json()) // .then(d => { // if (d.status !== 'success') { @@ -243,7 +243,7 @@ function formatBizNo(value) { 상단 회사 목록 로드 ---------------------------------------- */ function loadCompanies() { - fetch('/kngil/bbs/adm.php') + fetch('/admin/api/super') .then(res => res.json()) .then(json => { if (!json.records) return @@ -340,7 +340,7 @@ export function bindSaveButton() { return } - fetch('/kngil/bbs/adm.php?action=save', { + fetch('/admin/api/super?action=save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/kngil/js/adm_comp copy.js b/kngil/js/adm_comp copy.js index b4e9589..6b224a7 100644 --- a/kngil/js/adm_comp copy.js +++ b/kngil/js/adm_comp copy.js @@ -9,7 +9,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -144,7 +144,7 @@ export async function createUserGrid(boxId, options = {}) { } function loadUsers() { - fetch('/kngil/bbs/adm_comp.php') + fetch('/admin/api/company') .then(res => res.text()) // 🔥 먼저 text로 확인 .then(text => { try { @@ -168,7 +168,7 @@ export function loadUsersByMember(member_id) { return } - fetch('/kngil/bbs/adm_comp.php') + fetch('/admin/api/company') .then(res => res.json()) .then(json => { g.clear() @@ -198,7 +198,7 @@ export function setUserGridMode(mode = 'view') { export function loadData({ loadSummary = true } = {}) { - fetch('/kngil/bbs/adm_comp.php') + fetch('/admin/api/company') .then(res => res.json()) .then(async d => { @@ -323,7 +323,7 @@ document.getElementById('btnSave_comp')?.addEventListener('click', () => { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -408,7 +408,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`) .yes(() => { - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -436,7 +436,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { }) function loadTotalArea(memberId) { - return fetch(`/kngil/bbs/adm_comp.php?action=total_area&member_id=${memberId}`) + return fetch(`/admin/api/company?action=total_area&member_id=${memberId}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -467,7 +467,7 @@ function doSearch() { } // ⚠️ type === 'id' 는 DB로 안 보냄 - fetch(`/kngil/bbs/adm_comp.php?action=list` + fetch(`/admin/api/company?action=list` + `&user_nm=${encodeURIComponent(p_user_nm)}` + `&dept_nm=${encodeURIComponent(p_dept_nm)}` + `&use_yn=${useYn}` diff --git a/kngil/js/adm_comp.js b/kngil/js/adm_comp.js index 1460eb7..09fd94d 100644 --- a/kngil/js/adm_comp.js +++ b/kngil/js/adm_comp.js @@ -22,7 +22,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -252,7 +252,7 @@ export function loadUsersByMember(memberId) { return } - fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${memberId}`) + fetch(`/admin/api/company?action=list&member_id=${memberId}`) .then(res => res.json()) .then(d => { @@ -286,7 +286,7 @@ export function setUserGridMode(mode = 'view') { } export function loadData({ loadSummary = true } = {}) { - fetch('/kngil/bbs/adm_comp.php?action=list') + fetch('/admin/api/company?action=list') .then(res => res.json()) .then(async d => { @@ -405,7 +405,7 @@ document.getElementById('btnSave_comp')?.addEventListener('click', () => { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -490,7 +490,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`) .yes(() => { - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -518,7 +518,7 @@ document.getElementById('btnDelete')?.addEventListener('click', () => { }) function loadTotalArea(memberId) { - return fetch(`/kngil/bbs/adm_comp.php?action=total_area`) + return fetch(`/admin/api/company?action=total_area`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -549,7 +549,7 @@ function doSearch() { } // ⚠️ type === 'id' 는 DB로 안 보냄 - fetch(`/kngil/bbs/adm_comp.php?action=list` + fetch(`/admin/api/company?action=list` + `&user_nm=${encodeURIComponent(p_user_nm)}` + `&dept_nm=${encodeURIComponent(p_dept_nm)}` + `&use_yn=${useYn}` @@ -671,7 +671,7 @@ function openBulkCreatePopup(memberId) { function runBulkCreate(memberId, csvUrl) { - fetch('/kngil/bbs/adm_comp.php', { + fetch('/admin/api/company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -772,7 +772,7 @@ function loadDataByMemberId(memberId) { return; } - fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${encodeURIComponent(memberId)}`) + fetch(`/admin/api/company?action=list&member_id=${encodeURIComponent(memberId)}`) .then(res => res.json()) .then(async d => { diff --git a/kngil/js/adm_faq_popup.js b/kngil/js/adm_faq_popup.js index ac5e63f..38452d2 100644 --- a/kngil/js/adm_faq_popup.js +++ b/kngil/js/adm_faq_popup.js @@ -11,7 +11,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -101,7 +101,7 @@ export function openfaqPopup() { // 3. 브라우저 기본 확인창 사용 (가장 확실함) if (confirm(`선택한 ${ids.length}개의 상품을 삭제하시겠습니까?`)) { - fetch('/kngil/bbs/adm_faq_popup_delete.php', { + fetch('/admin/api/faq/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', ids: ids }) @@ -170,7 +170,7 @@ export function openfaqPopup() { console.log('INSERTS', inserts) console.log('UPDATES', updates) - fetch('/kngil/bbs/adm_faq_popup_save.php', { + fetch('/admin/api/faq/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -271,7 +271,7 @@ async function loadfaqData() { try { w2ui.faqGrid.lock('조회 중...', true); - const response = await fetch('/kngil/bbs/adm_faq_popup.php'); // PHP 파일 호출 + const response = await fetch('/admin/api/faq'); // PHP 파일 호출 const data = await response.json(); w2ui.faqGrid.clear(); diff --git a/kngil/js/adm_product_popup.js b/kngil/js/adm_product_popup.js index 006881f..309e38f 100644 --- a/kngil/js/adm_product_popup.js +++ b/kngil/js/adm_product_popup.js @@ -1,8 +1,8 @@ import { w2grid, w2ui, w2popup, w2alert } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js' const USE_YN_ITEMS = [ - { id: 'Y', text: '사용' }, - { id: 'N', text: '미사용' } + { id: 'Y', text: '사용' }, + { id: 'N', text: '미사용' } ] /* ------------------------------------------------- 공통 유틸 @@ -14,7 +14,7 @@ function destroyGrid(name) { } function loadBaseCode(mainCd) { - return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) + return fetch(`/admin/api/company?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { @@ -57,12 +57,12 @@ export function openProductPopup() { onOpen(event) { event.onComplete = () => { // 1. 그리드를 생성합니다. - createProductGrid('#productGrid'); + createProductGrid('#productGrid'); - - // 1. 추가 버튼 + + // 1. 추가 버튼 document.getElementById('prodAdd').onclick = () => { - const g = w2ui.productGrid + const g = w2ui.productGrid if (!g) return // 신규 row용 recid (음수로 충돌 방지) const newRecid = -Date.now() @@ -79,7 +79,7 @@ export function openProductPopup() { }; - + // 삭제 버튼 이벤트 연결 const removeBtn = document.getElementById('prodRemove'); if (removeBtn) { @@ -106,43 +106,43 @@ export function openProductPopup() { // 3. 브라우저 기본 확인창 사용 (가장 확실함) if (confirm(`선택한 ${ids.length}개의 상품을 삭제하시겠습니까?`)) { - fetch('/kngil/bbs/adm_product_popup_delete.php', { + fetch('/admin/api/product/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', ids: ids }) }) - .then(res => { - // 서버 응답이 오면 일단 텍스트로 받아서 확인 - return res.text(); - }) - .then(text => { - try { - const json = JSON.parse(text); - if (json.status === 'success') { - alert('삭제 완료'); - loadProductData(); // 목록 새로고침 - } else { - alert(json.message || '삭제 실패'); + .then(res => { + // 서버 응답이 오면 일단 텍스트로 받아서 확인 + return res.text(); + }) + .then(text => { + try { + const json = JSON.parse(text); + if (json.status === 'success') { + alert('삭제 완료'); + loadProductData(); // 목록 새로고침 + } else { + alert(json.message || '삭제 실패'); + } + } catch (e) { + console.error('응답 파싱 에러:', text); + alert('서버 응답 처리 중 오류가 발생했습니다.'); } - } catch (e) { - console.error('응답 파싱 에러:', text); - alert('서버 응답 처리 중 오류가 발생했습니다.'); - } - }) - .catch(err => { - console.error('통신 에러:', err); - alert('서버와 통신할 수 없습니다.'); - }); + }) + .catch(err => { + console.error('통신 에러:', err); + alert('서버와 통신할 수 없습니다.'); + }); } }; } - + // 3. 저장 버튼 document.getElementById('prodSave').onclick = () => { // 현재 사용 중인 grid - const g = w2ui.productGrid + const g = w2ui.productGrid if (!g) return const changes = g.getChanges() if (!changes.length) { @@ -163,20 +163,20 @@ export function openProductPopup() { } else { // UPDATE → PK + 변경 컬럼 updates.push({ - itm_cd : merged.itm_cd, - itm_nm : merged.itm_nm, - area : merged.area, + itm_cd: merged.itm_cd, + itm_nm: merged.itm_nm, + area: merged.area, itm_amt: merged.itm_amt, - use_yn : merged.use_yn?.id || merged.use_yn, - rmks : merged.rmks + use_yn: merged.use_yn?.id || merged.use_yn, + rmks: merged.rmks }) } }) console.log('INSERTS', inserts) console.log('UPDATES', updates) - - fetch('/kngil/bbs/adm_product_popup_save.php', { + + fetch('/admin/api/product/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -186,26 +186,26 @@ export function openProductPopup() { }) }) - .then(res => { - // 서버가 준 응답이 괜찮은지 확인 - if (!res.ok) throw new Error('서버 응답 오류'); - return res.json(); // 처음부터 깔끔하게 데이터로 읽기 - }) + .then(res => { + // 서버가 준 응답이 괜찮은지 확인 + if (!res.ok) throw new Error('서버 응답 오류'); + return res.json(); // 처음부터 깔끔하게 데이터로 읽기 + }) - .then(json => { - // 이제 json.status를 바로 쓸 수 있습니다. - if (json.status === 'success') { - w2alert('저장 완료'); - w2ui.productGrid.save(); // 빨간 삼각형 없애기 - loadProductData(); // 목록 새로고침 - } else { - w2alert(json.message || '저장 실패'); - } - }) - .catch(err => { - console.error('에러 발생:', err); - w2alert('처리 중 오류가 발생했습니다.'); - }); + .then(json => { + // 이제 json.status를 바로 쓸 수 있습니다. + if (json.status === 'success') { + w2alert('저장 완료'); + w2ui.productGrid.save(); // 빨간 삼각형 없애기 + loadProductData(); // 목록 새로고침 + } else { + w2alert(json.message || '저장 실패'); + } + }) + .catch(err => { + console.error('에러 발생:', err); + w2alert('처리 중 오류가 발생했습니다.'); + }); }; } @@ -220,7 +220,7 @@ export async function createProductGrid(boxId) { const authItems = await loadBaseCode('BS210') const grid = new w2grid({ - + name: 'productGrid', box: boxId, show: { @@ -230,27 +230,27 @@ export async function createProductGrid(boxId) { }, columns: [ - { field: 'itm_cd', text: '상품코드', size: '80px',style: 'text-align: center', attr: 'align=center', editable: { type: 'text' } ,sortable: true}, // name 아님! - { field: 'itm_nm', text: '상품명', size: '120px', style: 'text-align: center', editable: { type: 'text' } ,sortable: true}, // name 아님! - { field: 'area', text: '제공량', size: '100px', attr: 'align=center',render: 'number:0' , editable: { type: 'float' } ,sortable: true}, // volume 아님! - { field: 'itm_amt', text: '단가', size: '120px', attr: 'align=center',render: 'number:0', editable: { type: 'int' } ,sortable: true}, // + { field: 'itm_cd', text: '상품코드', size: '80px', style: 'text-align: center', attr: 'align=center', editable: { type: 'text' }, sortable: true }, // name 아님! + { field: 'itm_nm', text: '상품명', size: '120px', style: 'text-align: center', editable: { type: 'text' }, sortable: true }, // name 아님! + { field: 'area', text: '제공량', size: '100px', attr: 'align=center', render: 'number:0', editable: { type: 'float' }, sortable: true }, // volume 아님! + { field: 'itm_amt', text: '단가', size: '120px', attr: 'align=center', render: 'number:0', editable: { type: 'int' }, sortable: true }, // { - field: 'use_yn', - text: '사용여부', - size: '80px', - attr: 'align=center', - editable: { - type: 'list', - items: USE_YN_ITEMS, - showAll: true, - openOnFocus: true + field: 'use_yn', + text: '사용여부', + size: '80px', + attr: 'align=center', + editable: { + type: 'list', + items: USE_YN_ITEMS, + showAll: true, + openOnFocus: true + }, + render(record, extra) { + return extra?.value?.text || '' + }, + sortable: true }, - render(record, extra) { - return extra?.value?.text || '' - }, - sortable: true - }, - { field: 'rmks', text: '비고', size: '200px', editable: { type: 'text' } ,sortable: true} // memo 아님! + { field: 'rmks', text: '비고', size: '200px', editable: { type: 'text' }, sortable: true } // memo 아님! ], records: [] // 처음엔 비워둠 }); @@ -266,12 +266,10 @@ export async function createProductGrid(boxId) { async function loadProductData() { try { w2ui.productGrid.lock('조회 중...', true); - - const response = await fetch('/kngil/bbs/adm_product_popup.php'); // PHP 파일 호출 - let data = await response.json(); - data = normalizeProductUseYn(data) - + const response = await fetch('/kngil/bbs/adm_product_popup.php'); // PHP 파일 호출 + const data = await response.json(); + w2ui.productGrid.clear(); w2ui.productGrid.add(data); w2ui.productGrid.unlock(); @@ -284,11 +282,11 @@ async function loadProductData() { } function normalizeProductUseYn(records) { - return records.map(r => { - const item = USE_YN_ITEMS.find(u => u.id === r.use_yn) - return { - ...r, - use_yn: item || null - } - }) + return records.map(r => { + const item = USE_YN_ITEMS.find(u => u.id === r.use_yn) + return { + ...r, + use_yn: item || null + } + }) } \ No newline at end of file diff --git a/kngil/js/adm_purch_popup.js b/kngil/js/adm_purch_popup.js index 32f757f..9e762ac 100644 --- a/kngil/js/adm_purch_popup.js +++ b/kngil/js/adm_purch_popup.js @@ -133,7 +133,7 @@ async function loadPurchaseHistoryData() { searchParams.append('fbuy_dt', sfdt); searchParams.append('tbuy_dt', stdt); - const response = await fetch('/kngil/bbs/adm_purch_popup.php', { + const response = await fetch('/admin/api/purchase-history', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: searchParams diff --git a/kngil/js/adm_service copy.js b/kngil/js/adm_service copy.js index 5b778a6..b9d10f0 100644 --- a/kngil/js/adm_service copy.js +++ b/kngil/js/adm_service copy.js @@ -173,7 +173,7 @@ function addServiceFromProduct(p) { ------------------------------------------------- */ function loadExistingPurchase(memberId, buyDate) { - fetch(`/kngil/bbs/adm_service.php?member_id=${memberId}&buy_date=${buyDate}`) + fetch(`/admin/api/service?member_id=${memberId}&buy_date=${buyDate}`) .then(res => res.json()) .then(json => { diff --git a/kngil/js/adm_service.js b/kngil/js/adm_service.js index 2259762..24bb76f 100644 --- a/kngil/js/adm_service.js +++ b/kngil/js/adm_service.js @@ -451,7 +451,7 @@ function deleteServiceImmediately(row) { sq_no: row.sq_no }) - fetch('/kngil/bbs/adm_service.php', { + fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -484,7 +484,7 @@ function isServiceItem(r) { ------------------------------------------------- */ function loadExistingPurchase(memberId, buyDate) { - fetch('/kngil/bbs/adm_service.php', { + fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -647,7 +647,7 @@ function saveService(ctx) { _deleted: !!r._deleted })) - fetch('/kngil/bbs/adm_service.php', { + fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/kngil/js/adm_use_history.js b/kngil/js/adm_use_history.js index 93fcc50..8c9734e 100644 --- a/kngil/js/adm_use_history.js +++ b/kngil/js/adm_use_history.js @@ -156,7 +156,7 @@ async function loadUseHistoryData(memberId = ''){ searchParams.append('fuse_dt', sfdt); searchParams.append('tuse_dt', stdt); - const response = await fetch('/kngil/bbs/adm_use_history.php', { + const response = await fetch('/admin/api/use-history', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: searchParams diff --git a/kngil/js/common.js b/kngil/js/common.js index 1aeb687..6a192aa 100644 --- a/kngil/js/common.js +++ b/kngil/js/common.js @@ -4899,4 +4899,44 @@ return lottie; */ var Swiper=function(){"use strict";function e(e){return null!==e&&"object"==typeof e&&"constructor"in e&&e.constructor===Object}function t(s,a){void 0===s&&(s={}),void 0===a&&(a={});const i=["__proto__","constructor","prototype"];Object.keys(a).filter((e=>i.indexOf(e)<0)).forEach((i=>{void 0===s[i]?s[i]=a[i]:e(a[i])&&e(s[i])&&Object.keys(a[i]).length>0&&t(s[i],a[i])}))}const s={body:{},addEventListener(){},removeEventListener(){},activeElement:{blur(){},nodeName:""},querySelector:()=>null,querySelectorAll:()=>[],getElementById:()=>null,createEvent:()=>({initEvent(){}}),createElement:()=>({children:[],childNodes:[],style:{},setAttribute(){},getElementsByTagName:()=>[]}),createElementNS:()=>({}),importNode:()=>null,location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""}};function a(){const e="undefined"!=typeof document?document:{};return t(e,s),e}const i={document:s,navigator:{userAgent:""},location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""},history:{replaceState(){},pushState(){},go(){},back(){}},CustomEvent:function(){return this},addEventListener(){},removeEventListener(){},getComputedStyle:()=>({getPropertyValue:()=>""}),Image(){},Date(){},screen:{},setTimeout(){},clearTimeout(){},matchMedia:()=>({}),requestAnimationFrame:e=>"undefined"==typeof setTimeout?(e(),null):setTimeout(e,0),cancelAnimationFrame(e){"undefined"!=typeof setTimeout&&clearTimeout(e)}};function r(){const e="undefined"!=typeof window?window:{};return t(e,i),e}function n(e){return void 0===e&&(e=""),e.trim().split(" ").filter((e=>!!e.trim()))}function l(e,t){return void 0===t&&(t=0),setTimeout(e,t)}function o(){return Date.now()}function d(e,t){void 0===t&&(t="x");const s=r();let a,i,n;const l=function(e){const t=r();let s;return t.getComputedStyle&&(s=t.getComputedStyle(e,null)),!s&&e.currentStyle&&(s=e.currentStyle),s||(s=e.style),s}(e);return s.WebKitCSSMatrix?(i=l.transform||l.webkitTransform,i.split(",").length>6&&(i=i.split(", ").map((e=>e.replace(",","."))).join(", ")),n=new s.WebKitCSSMatrix("none"===i?"":i)):(n=l.MozTransform||l.OTransform||l.MsTransform||l.msTransform||l.transform||l.getPropertyValue("transform").replace("translate(","matrix(1, 0, 0, 1,"),a=n.toString().split(",")),"x"===t&&(i=s.WebKitCSSMatrix?n.m41:16===a.length?parseFloat(a[12]):parseFloat(a[4])),"y"===t&&(i=s.WebKitCSSMatrix?n.m42:16===a.length?parseFloat(a[13]):parseFloat(a[5])),i||0}function c(e){return"object"==typeof e&&null!==e&&e.constructor&&"Object"===Object.prototype.toString.call(e).slice(8,-1)}function p(){const e=Object(arguments.length<=0?void 0:arguments[0]),t=["__proto__","constructor","prototype"];for(let a=1;a1&&m.push(e.virtualSize-r)}if(o&&s.loop){const t=g[0]+x;if(s.slidesPerGroup>1){const a=Math.ceil((e.virtual.slidesBefore+e.virtual.slidesAfter)/s.slidesPerGroup),i=t*s.slidesPerGroup;for(let e=0;e!(s.cssMode&&!s.loop)||t!==c.length-1)).forEach((e=>{e.style[t]=`${x}px`}))}if(s.centeredSlides&&s.centeredSlidesBounds){let e=0;g.forEach((t=>{e+=t+(x||0)})),e-=x;const t=e>r?e-r:0;m=m.map((e=>e<=0?-v:e>t?t+w:e))}if(s.centerInsufficientSlides){let e=0;g.forEach((t=>{e+=t+(x||0)})),e-=x;const t=(s.slidesOffsetBefore||0)+(s.slidesOffsetAfter||0);if(e+t 0?(r._cssModeVirtualInitialSet=!0,requestAnimationFrame((()=>{h[e?"scrollLeft":"scrollTop"]=s}))):h[e?"scrollLeft":"scrollTop"]=s,y&&requestAnimationFrame((()=>{r.wrapperEl.style.scrollSnapType="",r._immediateVirtual=!1}));else{if(!r.support.smoothScroll)return m({swiper:r,targetPosition:s,side:e?"left":"top"}),!0;h.scrollTo({[e?"left":"top"]:s,behavior:"smooth"})}return!0}const E=A().isSafari;return y&&!i&&E&&r.isElement&&r.virtual.update(!1,!1,n),r.setTransition(t),r.setTranslate(w),r.updateActiveIndex(n),r.updateSlidesClasses(),r.emit("beforeTransitionStart",t,a),r.transitionStart(s,b),0===t?r.transitionEnd(s,b):r.animating||(r.animating=!0,r.onSlideToWrapperTransitionEnd||(r.onSlideToWrapperTransitionEnd=function(e){r&&!r.destroyed&&e.target===this&&(r.wrapperEl.removeEventListener("transitionend",r.onSlideToWrapperTransitionEnd),r.onSlideToWrapperTransitionEnd=null,delete r.onSlideToWrapperTransitionEnd,r.transitionEnd(s,b))}),r.wrapperEl.addEventListener("transitionend",r.onSlideToWrapperTransitionEnd)),!0},slideToLoop:function(e,t,s,a){if(void 0===e&&(e=0),void 0===s&&(s=!0),"string"==typeof e){e=parseInt(e,10)}const i=this;if(i.destroyed)return;void 0===t&&(t=i.params.speed);const r=i.grid&&i.params.grid&&i.params.grid.rows>1;let n=e;if(i.params.loop)if(i.virtual&&i.params.virtual.enabled)n+=i.virtual.slidesBefore;else{let e;if(r){const t=n*i.params.grid.rows;e=i.slides.find((e=>1*e.getAttribute("data-swiper-slide-index")===t)).column}else e=i.getSlideIndexByData(n);const t=r?Math.ceil(i.slides.length/i.params.grid.rows):i.slides.length,{centeredSlides:s}=i.params;let l=i.params.slidesPerView;"auto"===l?l=i.slidesPerViewDynamic():(l=Math.ceil(parseFloat(i.params.slidesPerView,10)),s&&l%2==0&&(l+=1));let o=t-e1){const e=[];return n.querySelectorAll(t.el).forEach((s=>{const a=p({},t,{el:s});e.push(new ie(a))})),e}const l=this;l.__swiper__=!0,l.support=I(),l.device=z({userAgent:t.userAgent}),l.browser=A(),l.eventsListeners={},l.eventsAnyListeners=[],l.modules=[...l.__modules__],t.modules&&Array.isArray(t.modules)&&l.modules.push(...t.modules);const o={};l.modules.forEach((e=>{e({params:t,swiper:l,extendParams:te(t,o),on:l.on.bind(l),once:l.once.bind(l),off:l.off.bind(l),emit:l.emit.bind(l)})}));const d=p({},ee,o);return l.params=p({},d,ae,t),l.originalParams=p({},l.params),l.passedParams=p({},t),l.params&&l.params.on&&Object.keys(l.params.on).forEach((e=>{l.on(e,l.params.on[e])})),l.params&&l.params.onAny&&l.onAny(l.params.onAny),Object.assign(l,{enabled:l.params.enabled,el:e,classNames:[],slides:[],slidesGrid:[],snapGrid:[],slidesSizesGrid:[],isHorizontal:()=>"horizontal"===l.params.direction,isVertical:()=>"vertical"===l.params.direction,activeIndex:0,realIndex:0,isBeginning:!0,isEnd:!1,translate:0,previousTranslate:0,progress:0,velocity:0,animating:!1,cssOverflowAdjustment(){return Math.trunc(this.translate/2**23)*2**23},allowSlideNext:l.params.allowSlideNext,allowSlidePrev:l.params.allowSlidePrev,touchEventsData:{isTouched:void 0,isMoved:void 0,allowTouchCallbacks:void 0,touchStartTime:void 0,isScrolling:void 0,currentTranslate:void 0,startTranslate:void 0,allowThresholdMove:void 0,focusableElements:l.params.focusableElements,lastClickTime:0,clickTimeout:void 0,velocities:[],allowMomentumBounce:void 0,startMoving:void 0,pointerId:null,touchId:null},allowClick:!0,allowTouchMove:l.params.allowTouchMove,touches:{startX:0,startY:0,currentX:0,currentY:0,diff:0},imagesToLoad:[],imagesLoaded:0}),l.emit("_swiper"),l.params.init&&l.init(),l}getDirectionLabel(e){return this.isHorizontal()?e:{width:"height","margin-top":"margin-left","margin-bottom ":"margin-right","margin-left":"margin-top","margin-right":"margin-bottom","padding-left":"padding-top","padding-right":"padding-bottom",marginRight:"marginBottom"}[e]}getSlideIndex(e){const{slidesEl:t,params:s}=this,a=y(f(t,`.${s.slideClass}, swiper-slide`)[0]);return y(e)-a}getSlideIndexByData(e){return this.getSlideIndex(this.slides.find((t=>1*t.getAttribute("data-swiper-slide-index")===e)))}recalcSlides(){const{slidesEl:e,params:t}=this;this.slides=f(e,`.${t.slideClass}, swiper-slide`)}enable(){const e=this;e.enabled||(e.enabled=!0,e.params.grabCursor&&e.setGrabCursor(),e.emit("enable"))}disable(){const e=this;e.enabled&&(e.enabled=!1,e.params.grabCursor&&e.unsetGrabCursor(),e.emit("disable"))}setProgress(e,t){const s=this;e=Math.min(Math.max(e,0),1);const a=s.minTranslate(),i=(s.maxTranslate()-a)*e+a;s.translateTo(i,void 0===t?0:t),s.updateActiveIndex(),s.updateSlidesClasses()}emitContainerClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=e.el.className.split(" ").filter((t=>0===t.indexOf("swiper")||0===t.indexOf(e.params.containerModifierClass)));e.emit("_containerClasses",t.join(" "))}getSlideClasses(e){const t=this;return t.destroyed?"":e.className.split(" ").filter((e=>0===e.indexOf("swiper-slide")||0===e.indexOf(t.params.slideClass))).join(" ")}emitSlidesClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=[];e.slides.forEach((s=>{const a=e.getSlideClasses(s);t.push({slideEl:s,classNames:a}),e.emit("_slideClass",s,a)})),e.emit("_slideClasses",t)}slidesPerViewDynamic(e,t){void 0===e&&(e="current"),void 0===t&&(t=!1);const{params:s,slides:a,slidesGrid:i,slidesSizesGrid:r,size:n,activeIndex:l}=this;let o=1;if("number"==typeof s.slidesPerView)return s.slidesPerView;if(s.centeredSlides){let e,t=a[l]?Math.ceil(a[l].swiperSlideSize):0;for(let s=l+1;sT){const t=I(e);s.slides.filter((e=>e.matches(`.${s.params.slideClass}[data-swiper-slide-index="${t}"], swiper-slide[data-swiper-slide-index="${t}"]`))).forEach((e=>{e.remove()}))}const z=o?-g.length:0,A=o?2*g.length:g.length;for(let t=z;t=S&&t<=T){const s=I(t);void 0===h||e?L.push(s):(t>h&&L.push(s),tC&&(u=C),m
- KNGIL
+ KNGIL
통합 회원관리
@@ -103,7 +184,7 @@ $isCompanyAdmin = in_array($auth, ['BS100100', 'BS100200', 'BS100300', 'BS100400
-
회사 관리자
@@ -112,30 +193,23 @@ $isCompanyAdmin = in_array($auth, ['BS100100', 'BS100200', 'BS100300', 'BS100400
-
-
+