summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-08-14 13:15:21 +0000
committerjoonhoekim <26rote@gmail.com>2025-08-14 13:15:21 +0000
commit49d236df3bd2bd976ebc424644f34f5affa1074f (patch)
tree7b0f60c399e724847894061fae74876aa1bf5c7e
parent969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 (diff)
(김준회) 결재 테스트 모듈 수정, 환경병수 eVCP 운영 대응, SGIPS JWT TOKEN 수정, SHI-API 기반 유저 관리 추가, 유저목록 테이블 변경
-rw-r--r--.env.development61
-rw-r--r--.env.production69
-rw-r--r--app/[lng]/evcp/(evcp)/system/page.tsx3
-rw-r--r--components/data-table/data-table.tsx6
-rw-r--r--components/knox/approval/ApprovalManager.tsx11
-rw-r--r--components/knox/approval/ApprovalSubmit.tsx51
-rw-r--r--config/euserColumnsConfig.ts54
-rw-r--r--db/db.ts4
-rw-r--r--db/schema/NONSAP/nonsap-user.ts101
-rw-r--r--db/schema/index.ts1
-rw-r--r--db/schema/users.ts23
-rw-r--r--drizzle.config.ts4
-rw-r--r--instrumentation.ts11
-rw-r--r--lib/knox-sync/master-sync-service.ts7
-rw-r--r--lib/sedp/sedp-token.ts4
-rw-r--r--lib/shi-api/shi-api-utils.ts125
-rw-r--r--lib/shi-api/users-sync-scheduler.ts42
-rw-r--r--lib/users/auth/verifyCredentails.ts2
-rw-r--r--lib/users/table/users-table-columns.tsx25
-rw-r--r--public/wsdl/IF_ECC_EVCP_PO_INFORMATION.wsdl21
20 files changed, 499 insertions, 126 deletions
diff --git a/.env.development b/.env.development
index 3bc2267f..b4ba1d87 100644
--- a/.env.development
+++ b/.env.development
@@ -23,7 +23,6 @@ Email_From_Address=dujin.kim@dtsolution.co.kr
NEXT_PUBLIC_MUI_KEY=da30586e1f20b93856a9783012fc9258Tz04ODI0MyxFPTE3NDQ0NTM2NzgwMDAsUz1wcmVtaXVtLExNPXN1YnNjcmlwdGlvbixLVj0y
# PDFTRON KEYS
NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1712033365211:7f00e5e80300000000ef1ecbafeaacf47ddd5a5ba2430f4c4fa58b2d09
-
NEXT_PUBLIC_PDFTRON_SERVER_KEY=demo:1740034881027:6175a0fc0300000000f155d153480e5ba091f17922a109cbd7cf6e40b3
# 메시어스 SPREAD JS
NEXT_PUBLIC_SPREAD_LICENSE="43.203.251.114|60.101.108.100|evcp.dtsolution.io,261619561743613#B1ZaK2ycWtEd7Z4S0FENYlXOQhWRsd7M92GewBlQGV5Qu3WcZdESRJmZup4RwljYzoEettkToRFeZJ5LnBlRhdWSDZHbtdVTQBnZttiWHhWTntScoV6LtF6YrknUa9mVyV6RkljTWtCZ5ETZr24bLpnaXd4cUlXOuhTQvMWV8MWU524K5sWRiZnVzUjTPpESrMzaxJUdMlFVntiVLtkd4hVVax6K8sEMQBFdFRUekB7QwU7LsFWQC3Ed7gEWpd7bRtSOy26cJ56LE96T5REbqJ7bl36dEZXewcUR9wWR8lWZax4RSdUSL5mZ9cmWxFWTlRlTGhjUypGZvI4UplEMJdGSy9UVj54dJREWpl4QvR6bzdFN7sCcMBlZxgTTWt4cJpURyRkI0IyUiwiIBZTMxE4QzQjI0ICSiwyNyATMwgzN9ITM0IicfJye#4Xfd5nIIlkSCJiOiMkIsICOx8idgMlSgQWYlJHcTJiOi8kI1tlOiQmcQJCLiATN8IjMwACNyYDM5IDMyIiOiQncDJCLiQjM7ATNyAjMiojIwhXRiwiIvlmLu3Wa4VHbvNHdk9CcjZXZsADMx8COwEjLxATMuAjNsQTMx8SM5IjLzAjMuMDNiojIz5GRiwiIYWI1oO00UaI1wuY1US90iojIh94QiwiIzEjNzQzNxYTN9EjNxYjMiojIklkIs4XXiQXZlh6U4J7bwVmUiwiI4JXYoNUY4FGRiwiIlxmYhRFdvZXaQJCLiQXZlh6U4RnbhdkIbpjInxmZiwSZzxWYmpjIyNHZisnOiwmbBJye0ICRiwiI34TUYlDTrEGTjlnQtR4L52yK4UjbZNzcDlzYsFWdw96VEhTdVx4RrlGat3SRnRXcjpTNfh"
@@ -32,12 +31,16 @@ NEXT_PUBLIC_DESIGNER_LICENSE="43.203.251.114|60.101.108.100|evcp.dtsolution.io,2
# NEXT_PUBLIC_SPREAD_LICENSE="60.101.108.100,674672615555322#B1dbvNkSiJXZDRFRYJVQHFWa6Y6KTVGV5cVWSRVWVlHejFlcvFWUFdGVzVVZVtEcsNjNvo5aHhjcSNVd6kzNvQUT9tCSxEXU6RzRrh5SxsUYqZjertEU7RWQu3yaDNXT5JmRIh7R6YnSGZlMDhkRqB7MIlTYvUWQFFzYulTTm3ENINEV7FWZMl4Q5cXSy96KthkVC3USvYXa8FnbtJWZFdlVSFmYwsEMKRkQxp6TRdGMLdVOTR7TMJEWiRGa6JncDRlWShTN9glc8FmQkBzdvMkUthHUoJGbOJGatVmUxtkRTVmeUlVWxJDN7kXQ6oHUwhEciZXNNJVOPBzc83UaTNmZVZ6aIxUcQdmcOJiOiMlIsICNzgTN5YTQyIiOigkIsYDM6gDN6YjM0IicfJye35XX3JCSJpkQiojIDJCLigTMuYHITpEIkFWZyB7UiojIOJyebpjIkJHUiwiIyQzMxkDMgQjM7ATNyAjMiojI4J7QiwiIwATMugDMx8SMwEjLwYjI0IyctRkIsIShXyetzqekkyesEyOvCyuI0ISYONkIsIiMyMTN5UTNxYjM7YDN7YjI0ICZJJCL35lI4VWZoNFdy3GclJlIsICdyFGaDFGdhRkIsISZsJWYUR7b6lGUiwiI4VWZoNFd49WYHJyW0IyZsZmIsU6csFmZ0IiczRmI1pjIs9WQisnOiQkIsISP3cXVw2meRZ4Yys4YB3UeaJkck9GWjhHUMVlU4gUcndlS63EWCB7YZh7bHBlVwBHe5kVcvEzc5N5aBZUZlJ6SpZHTHRFVjd5dxs6Yuh"
# NEXT_PUBLIC_DESIGNER_LICENSE="Designer-514482759413237#B1IdxRUVvQnMkFkYVBzLjRzZohUVWZnSiJWUO9WS4pnMLp5KJZ7dX3CelFlW53STTlkdLlzdYBFV6lzTLRGUKVWOU3UbR3GUXFWZxJ5K8lzTnpVcEBHT5p4Yqt6RvEXaTtWMrRmUWpGW5x6dZlzVM5GRjZXMNVGdKxUZptGVUlUWiRnZ7cnTndkWsRGZllTcDpXeVpWRIV5M9BDVkBFNElWUCd5ZzcUWLNjYPNXOl9ESVJTQ756MFlFWzcmcGFDcXt6dDdnV4YmejJHSnNUc6t4MxcXNzQkU9kFSBRGa73WNEtyR6MkZzsEbvRVVHdHWYVlMr2UTGFmZI3mWIdUTihWb43WY78Eaz3SV9d6UzU4R7V5YjJiOiMlIsISMFBjQ8kDRiojIIJCLxITMzUjN5cTN0IicfJye35XX3JCSJpkQiojIDJCLigTMuYHITpEIkFWZyB7UiojIOJyebpjIkJHUiwiI9QTOwMDMgEjM7ATNyAjMiojI4J7QiwiIw8CMuAjLw2icl96ZpNXZkJiOiMXbEJCLikCjGyOoEyOshyOngyOsxqOKFeJ15Or0RSK1xSI12KI1iojIh94QiwiI7MjMzEDN9UzNygDN4ETNiojIklkIs4XXiQXZlh6U4J7bwVmUiwiI4JXYoNUY4FGRiwiIlxmYhRFdvZXaQJCLiQXZlh6U4RnbhdkIbpjInxmZiwSZ5JHd0IiczRmI1pjIs9WQisnOiQkIsISP3E4N82mcKVkdBZ7butkQQNXcMJWNnVVMxE6aUZ4QXBldnZWcrAXM9lmS9FDbp9ERUV7Q9IndiNHd0plb7pmd5debKh"
+# SPREAD JS 내부망 eVCP 운영 (개발 배포시 이 키로 대체)
+# NEXT_PUBLIC_SPREAD_LICENSE=""
+# NEXT_PUBLIC_DESIGNER_LICENSE=""
+
# === 기간계 시스템 연동 설정 ===
ERP_API_URL=https://erp.example.com/api/vendors
ERP_API_KEY=your-erp-api-key
ERP_HEALTH_CHECK_URL=https://erp.example.com/api/health
-# S-EDP (설계정보)
-SEDP_API_BASE_URL=http://sedpwebapi.ship.samsung.co.kr/dev/api
+# S-EDP (설계정보) (품질 및 개발은 포트가 다른 것으로 변경됨. 전부 운영 연결)
+SEDP_API_BASE_URL=http://sedpwebapi.ship.samsung.co.kr/api
SEDP_API_USER_ID=EVCPUSER
SEDP_API_PASSWORD=evcpusr@2025
@@ -46,17 +49,16 @@ SEDP_API_PASSWORD=evcpusr@2025
# ORACLE_USER=system
# ORACLE_PASSWORD=oracle
# ORACLE_CONNECTION_STRING=localhost:1521/XEPDB1
-# Oracle DB 연결 설정 (SHI 품질)
+# Oracle DB 연결 설정
ORACLE_USER=shievcp
ORACLE_PASSWORD=evp_2025
-ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ
+# ORACLE_CONNECTION_STRING=60.100.89.211:7971/SEVMP # 운영
+ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ # 품질
+
# NON-SAP 인코텀즈, 지불조건, 선적지, 하역지 동기화 관련
PROCUREMENT_SYNC_ON_START=false
-
-
# 기본 DOLCE 동기화 값 (60.100.99.217=dolce 개발, 60.100.98.68=dolce 운영)
-SYNC_DOLCE_URL=http://60.100.99.217:1111/
SYNC_DOLCE_BATCH=150 # 없으면 100으로 fallback
SYNC_DOLCE_TOKEN=
SYNC_DOLCE_ENABLED=true
@@ -70,32 +72,55 @@ SYNC_SWP_ENABLED=true
# DOLCE 설정
IMPORT_DOLCE_ENABLED=true
+
+# DOLCE URL 설정 (운영)
+# SYNC_DOLCE_URL="http://60.100.98.68:1111/"
+# DOLCE_API_URL="http://60.100.98.68:1111"
+# DOLCE_UPLOAD_URL="http://60.100.98.68:1111/PWPUploadService.ashx"
+# DOLCE_DOC_LIST_API_URL="http://60.100.98.68:1111/Services/VDCSWebService.svc/DwgReceiptMgmt"
+# DOLCE_DOC_DETAIL_API_URL="http://60.100.98.68:1111/Services/VDCSWebService.svc/DetailDwgReceiptMgmt"
+# DOLCE_FILE_INFO_API_URL="http://60.100.98.68:1111/Services/VDCSWebService.svc/FileInfoList"
+# DOLCE_DOWNLOAD_URL="http://60.100.98.68:1111/Download.aspx"
+
+# DOLCE URL 설정 (품질)
+SYNC_DOLCE_URL="http://60.100.99.217:1111/"
DOLCE_API_URL="http://60.100.99.217:1111"
DOLCE_UPLOAD_URL="http://60.100.99.217:1111/PWPUploadService.ashx"
DOLCE_DOC_LIST_API_URL="http://60.100.99.217:1111/Services/VDCSWebService.svc/DwgReceiptMgmt"
DOLCE_DOC_DETAIL_API_URL="http://60.100.99.217:1111/Services/VDCSWebService.svc/DetailDwgReceiptMgmt"
-DOLCE_FILE_INFO_API_URL=http://60.100.99.217:1111/Services/VDCSWebService.svc/FileInfoList
-DOLCE_DOWNLOAD_URL=http://60.100.99.217:1111/Download.aspx
+DOLCE_FILE_INFO_API_URL="http://60.100.99.217:1111/Services/VDCSWebService.svc/FileInfoList"
+DOLCE_DOWNLOAD_URL="http://60.100.99.217:1111/Download.aspx"
+
+
+### SHI-API ###
+# 운영
+# SHI_API_BASE_URL="http://www.qa.shi-api.com"
+SHI_API_BASE_URL="http://www.shi-api.com"
+SHI_NONSAP_USER_SEGMENT="/evcp/Common/CMCTB_USR"
+NONSAP_USERSYNC_FIRST_RUN="true"
+
+# Bearer Token 운영
+# SHI_API_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NTM5NzQwMDAsImV4cCI6MzI1MzUxNjE5NDB9.Ec_xP5lrhGQBRP_7rfZCtQXQQ8X1wzzPrEubhCe9fXg"
+# Bearer Token 품질
+SHI_API_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NTM5NzQwMDAsImV4cCI6MzI1MzUxNjE5NDB9.Sb0C5iKzVv3N3GWew22Ivykl4CrXptTJ2J0PojGzmhE"
-### S-GIPS ###
+# S_GIPS_URL="http://shi-api.com/evcp/Common/verifySgipsUser" # 운영
S_GIPS_URL="http://qa.shi-api.com/evcp/Common/verifySgipsUser"
-S_GIPS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NDg2MTcyMDAsImV4cCI6MTc1NjYyNzIwMH0.aMPZn9Et0Q--lC3Av8Sh4VtWW50-Dk05WHzdhbWsr7k"
S_GIPS_RSA_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtHC28Gw0U8taUwI8oJtG1H2JWGJtcsDw8w1oZbs759/Rag7zCF/bBilRtvlOz92wt02RCONetWK9VMgR2cqTJhfSaP92jIx0QQ+W1IrSKAiBxv+WtItsaWFLgYGIYNvrX8+qOnd+rDBvKDP9kk9Zqs1mHF2CbPRmao7/iEfhTb92hCgpFqsj/zU7nV3a8RbyifEMKSXTNanOEK2nTxAjld/csXQayHSaaqoH/lVySK0Qp6A2d2u2gEj/TAQ+Bhe7BsexNs2s5u5rykJqeROqJ7n0UsGgLd+uUDeo2nLqq5KeaXNcmACVcy2AASog78dzKwQmmGuC9Rp3zIoKOGdoQwIDAQAB"
### NHN Cloud OCR KEY
OCR_SECRET_KEY=QVZzbkFtVFV1UWl2THNCY01lYVVGUUxpWmdyUkxHYVA=
# === SSO 설정 ===
-# ! SSO Redirect 주소로 활용되며, 상단에서 적절한 URL을 쓴다면 이 변수는 주석처리할 것
-# NEXTAUTH_URL="http://60.101.108.100"
# SAML 2.0 SP로서 신청할 때 기입하는 사항
# 메타데이터 XML에서 추출 가능하나, 개발 편의성을 위해 추출로직 제거하고 환경변수에 하드코딩함
### sp_metadata.xml ###
-SAML_SP_ENTITY_ID="http://60.101.108.100"
-SAML_SP_CALLBACK_URL="http://60.101.108.100/api/saml/callback"
-# POST
+# SAML_SP_ENTITY_ID="http://evcp.sevcp.com" # 운영
+SAML_SP_ENTITY_ID="http://60.101.108.100" # 개발
+# SAML_SP_CALLBACK_URL="http://evcp.sevcp.com/api/saml/callback" # 운영
+SAML_SP_CALLBACK_URL="http://60.101.108.100/api/saml/callback" # 개발
+# POST & Redirect
SAML_SP_ACS_BINDING_PRIMARY="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
-# Redirect
SAML_SP_ACS_BINDING_SECONDARY="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
SAML_SP_AUTHN_REQUESTS_SIGNED=false
SAML_SP_WANT_ASSERTIONS_SIGNED=false
diff --git a/.env.production b/.env.production
index b49de50b..47459dc3 100644
--- a/.env.production
+++ b/.env.production
@@ -22,7 +22,6 @@ Email_From_Address=dujin.kim@dtsolution.co.kr
# MUI
NEXT_PUBLIC_MUI_KEY=da30586e1f20b93856a9783012fc9258Tz04ODI0MyxFPTE3NDQ0NTM2NzgwMDAsUz1wcmVtaXVtLExNPXN1YnNjcmlwdGlvbixLVj0y
# PDFTRON KEYS
-# NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd
NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1712033365211:7f00e5e80300000000ef1ecbafeaacf47ddd5a5ba2430f4c4fa58b2d09
NEXT_PUBLIC_PDFTRON_SERVER_KEY=demo:1740034881027:6175a0fc0300000000f155d153480e5ba091f17922a109cbd7cf6e40b3
# 메시어스 SPREAD JS
@@ -32,11 +31,15 @@ NEXT_PUBLIC_DESIGNER_LICENSE="43.203.251.114|60.101.108.100|evcp.dtsolution.io,2
# NEXT_PUBLIC_SPREAD_LICENSE="60.101.108.100,674672615555322#B1dbvNkSiJXZDRFRYJVQHFWa6Y6KTVGV5cVWSRVWVlHejFlcvFWUFdGVzVVZVtEcsNjNvo5aHhjcSNVd6kzNvQUT9tCSxEXU6RzRrh5SxsUYqZjertEU7RWQu3yaDNXT5JmRIh7R6YnSGZlMDhkRqB7MIlTYvUWQFFzYulTTm3ENINEV7FWZMl4Q5cXSy96KthkVC3USvYXa8FnbtJWZFdlVSFmYwsEMKRkQxp6TRdGMLdVOTR7TMJEWiRGa6JncDRlWShTN9glc8FmQkBzdvMkUthHUoJGbOJGatVmUxtkRTVmeUlVWxJDN7kXQ6oHUwhEciZXNNJVOPBzc83UaTNmZVZ6aIxUcQdmcOJiOiMlIsICNzgTN5YTQyIiOigkIsYDM6gDN6YjM0IicfJye35XX3JCSJpkQiojIDJCLigTMuYHITpEIkFWZyB7UiojIOJyebpjIkJHUiwiIyQzMxkDMgQjM7ATNyAjMiojI4J7QiwiIwATMugDMx8SMwEjLwYjI0IyctRkIsIShXyetzqekkyesEyOvCyuI0ISYONkIsIiMyMTN5UTNxYjM7YDN7YjI0ICZJJCL35lI4VWZoNFdy3GclJlIsICdyFGaDFGdhRkIsISZsJWYUR7b6lGUiwiI4VWZoNFd49WYHJyW0IyZsZmIsU6csFmZ0IiczRmI1pjIs9WQisnOiQkIsISP3cXVw2meRZ4Yys4YB3UeaJkck9GWjhHUMVlU4gUcndlS63EWCB7YZh7bHBlVwBHe5kVcvEzc5N5aBZUZlJ6SpZHTHRFVjd5dxs6Yuh"
# NEXT_PUBLIC_DESIGNER_LICENSE="Designer-514482759413237#B1IdxRUVvQnMkFkYVBzLjRzZohUVWZnSiJWUO9WS4pnMLp5KJZ7dX3CelFlW53STTlkdLlzdYBFV6lzTLRGUKVWOU3UbR3GUXFWZxJ5K8lzTnpVcEBHT5p4Yqt6RvEXaTtWMrRmUWpGW5x6dZlzVM5GRjZXMNVGdKxUZptGVUlUWiRnZ7cnTndkWsRGZllTcDpXeVpWRIV5M9BDVkBFNElWUCd5ZzcUWLNjYPNXOl9ESVJTQ756MFlFWzcmcGFDcXt6dDdnV4YmejJHSnNUc6t4MxcXNzQkU9kFSBRGa73WNEtyR6MkZzsEbvRVVHdHWYVlMr2UTGFmZI3mWIdUTihWb43WY78Eaz3SV9d6UzU4R7V5YjJiOiMlIsISMFBjQ8kDRiojIIJCLxITMzUjN5cTN0IicfJye35XX3JCSJpkQiojIDJCLigTMuYHITpEIkFWZyB7UiojIOJyebpjIkJHUiwiI9QTOwMDMgEjM7ATNyAjMiojI4J7QiwiIw8CMuAjLw2icl96ZpNXZkJiOiMXbEJCLikCjGyOoEyOshyOngyOsxqOKFeJ15Or0RSK1xSI12KI1iojIh94QiwiI7MjMzEDN9UzNygDN4ETNiojIklkIs4XXiQXZlh6U4J7bwVmUiwiI4JXYoNUY4FGRiwiIlxmYhRFdvZXaQJCLiQXZlh6U4RnbhdkIbpjInxmZiwSZ5JHd0IiczRmI1pjIs9WQisnOiQkIsISP3E4N82mcKVkdBZ7butkQQNXcMJWNnVVMxE6aUZ4QXBldnZWcrAXM9lmS9FDbp9ERUV7Q9IndiNHd0plb7pmd5debKh"
+# SPREAD JS 내부망 eVCP 운영 (개발 배포시 이 키로 대체)
+# NEXT_PUBLIC_SPREAD_LICENSE=""
+# NEXT_PUBLIC_DESIGNER_LICENSE=""
+
# === 기간계 시스템 연동 설정 ===
ERP_API_URL=https://erp.example.com/api/vendors
ERP_API_KEY=your-erp-api-key
ERP_HEALTH_CHECK_URL=https://erp.example.com/api/health
-# S-EDP (설계정보)
+# S-EDP (설계정보) (품질 및 개발은 포트가 다른 것으로 변경됨. 전부 운영 연결)
SEDP_API_BASE_URL=http://sedpwebapi.ship.samsung.co.kr/api
SEDP_API_USER_ID=EVCPUSER
SEDP_API_PASSWORD=evcpusr@2025
@@ -46,15 +49,16 @@ SEDP_API_PASSWORD=evcpusr@2025
# ORACLE_USER=system
# ORACLE_PASSWORD=oracle
# ORACLE_CONNECTION_STRING=localhost:1521/XEPDB1
-# Oracle DB 연결 설정 (SHI 품질)
+# Oracle DB 연결 설정
ORACLE_USER=shievcp
ORACLE_PASSWORD=evp_2025
-ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ
+# ORACLE_CONNECTION_STRING=60.100.89.211:7971/SEVMP # 운영
+ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ # 품질
+
# NON-SAP 인코텀즈, 지불조건, 선적지, 하역지 동기화 관련
PROCUREMENT_SYNC_ON_START=false
# 기본 DOLCE 동기화 값 (60.100.99.217=dolce 개발, 60.100.98.68=dolce 운영)
-SYNC_DOLCE_URL=http://60.100.99.217:1111/
SYNC_DOLCE_BATCH=150 # 없으면 100으로 fallback
SYNC_DOLCE_TOKEN=
SYNC_DOLCE_ENABLED=true
@@ -64,36 +68,59 @@ DOLCE_UPLOAD_ENABLED=true
SYNC_SWP_URL=https://swp.example.com/api/documents
SYNC_SWP_BATCH=200
SYNC_SWP_TOKEN=
-# SYNC_SWP_ENABLED=true # production에는 선언되어 있지 않아서 주석처리해 추가
+SYNC_SWP_ENABLED=true
# DOLCE 설정
IMPORT_DOLCE_ENABLED=true
-DOLCE_API_URL=http://60.100.99.217:1111
-DOLCE_UPLOAD_URL=http://60.100.99.217:1111/PWPUploadService.ashx
-DOLCE_DOC_LIST_API_URL=http://60.100.99.217:1111/Services/VDCSWebService.svc/DwgReceiptMgmt
-DOLCE_DOC_DETAIL_API_URL=http://60.100.99.217:1111/Services/VDCSWebService.svc/DetailDwgReceiptMgmt
-DOLCE_FILE_INFO_API_URL=http://60.100.99.217:1111/Services/VDCSWebService.svc/FileInfoList
-DOLCE_DOWNLOAD_URL=http://60.100.99.217:1111/Download.aspx
-
-### S-GIPS ###
+
+# DOLCE URL 설정 (운영)
+# SYNC_DOLCE_URL="http://60.100.98.68:1111/"
+# DOLCE_API_URL="http://60.100.98.68:1111"
+# DOLCE_UPLOAD_URL="http://60.100.98.68:1111/PWPUploadService.ashx"
+# DOLCE_DOC_LIST_API_URL="http://60.100.98.68:1111/Services/VDCSWebService.svc/DwgReceiptMgmt"
+# DOLCE_DOC_DETAIL_API_URL="http://60.100.98.68:1111/Services/VDCSWebService.svc/DetailDwgReceiptMgmt"
+# DOLCE_FILE_INFO_API_URL="http://60.100.98.68:1111/Services/VDCSWebService.svc/FileInfoList"
+# DOLCE_DOWNLOAD_URL="http://60.100.98.68:1111/Download.aspx"
+
+# DOLCE URL 설정 (품질)
+SYNC_DOLCE_URL="http://60.100.99.217:1111/"
+DOLCE_API_URL="http://60.100.99.217:1111"
+DOLCE_UPLOAD_URL="http://60.100.99.217:1111/PWPUploadService.ashx"
+DOLCE_DOC_LIST_API_URL="http://60.100.99.217:1111/Services/VDCSWebService.svc/DwgReceiptMgmt"
+DOLCE_DOC_DETAIL_API_URL="http://60.100.99.217:1111/Services/VDCSWebService.svc/DetailDwgReceiptMgmt"
+DOLCE_FILE_INFO_API_URL="http://60.100.99.217:1111/Services/VDCSWebService.svc/FileInfoList"
+DOLCE_DOWNLOAD_URL="http://60.100.99.217:1111/Download.aspx"
+
+
+### SHI-API ###
+# 운영
+# SHI_API_BASE_URL="http://www.qa.shi-api.com"
+SHI_API_BASE_URL="http://www.shi-api.com"
+SHI_NONSAP_USER_SEGMENT="/evcp/Common/CMCTB_USR"
+NONSAP_USERSYNC_FIRST_RUN="true"
+
+# Bearer Token 운영
+# SHI_API_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NTM5NzQwMDAsImV4cCI6MzI1MzUxNjE5NDB9.Ec_xP5lrhGQBRP_7rfZCtQXQQ8X1wzzPrEubhCe9fXg"
+# Bearer Token 품질
+SHI_API_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NTM5NzQwMDAsImV4cCI6MzI1MzUxNjE5NDB9.Sb0C5iKzVv3N3GWew22Ivykl4CrXptTJ2J0PojGzmhE"
+
+# S_GIPS_URL="http://shi-api.com/evcp/Common/verifySgipsUser" # 운영
S_GIPS_URL="http://qa.shi-api.com/evcp/Common/verifySgipsUser"
-S_GIPS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NDg2MTcyMDAsImV4cCI6MTc1NjYyNzIwMH0.aMPZn9Et0Q--lC3Av8Sh4VtWW50-Dk05WHzdhbWsr7k"
S_GIPS_RSA_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtHC28Gw0U8taUwI8oJtG1H2JWGJtcsDw8w1oZbs759/Rag7zCF/bBilRtvlOz92wt02RCONetWK9VMgR2cqTJhfSaP92jIx0QQ+W1IrSKAiBxv+WtItsaWFLgYGIYNvrX8+qOnd+rDBvKDP9kk9Zqs1mHF2CbPRmao7/iEfhTb92hCgpFqsj/zU7nV3a8RbyifEMKSXTNanOEK2nTxAjld/csXQayHSaaqoH/lVySK0Qp6A2d2u2gEj/TAQ+Bhe7BsexNs2s5u5rykJqeROqJ7n0UsGgLd+uUDeo2nLqq5KeaXNcmACVcy2AASog78dzKwQmmGuC9Rp3zIoKOGdoQwIDAQAB"
### NHN Cloud OCR KEY
OCR_SECRET_KEY=QVZzbkFtVFV1UWl2THNCY01lYVVGUUxpWmdyUkxHYVA=
# === SSO 설정 ===
-# ! SSO Redirect 주소로 활용되며, 상단에서 적절한 URL을 쓴다면 이 변수는 주석처리할 것
-# NEXTAUTH_URL="http://60.101.108.100"
# SAML 2.0 SP로서 신청할 때 기입하는 사항
# 메타데이터 XML에서 추출 가능하나, 개발 편의성을 위해 추출로직 제거하고 환경변수에 하드코딩함
### sp_metadata.xml ###
-SAML_SP_ENTITY_ID="http://60.101.108.100"
-SAML_SP_CALLBACK_URL="http://60.101.108.100/api/saml/callback"
-# POST
+# SAML_SP_ENTITY_ID="http://evcp.sevcp.com" # 운영
+SAML_SP_ENTITY_ID="http://60.101.108.100" # 개발
+# SAML_SP_CALLBACK_URL="http://evcp.sevcp.com/api/saml/callback" # 운영
+SAML_SP_CALLBACK_URL="http://60.101.108.100/api/saml/callback" # 개발
+# POST & Redirect
SAML_SP_ACS_BINDING_PRIMARY="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
-# Redirect
SAML_SP_ACS_BINDING_SECONDARY="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
SAML_SP_AUTHN_REQUESTS_SIGNED=false
SAML_SP_WANT_ASSERTIONS_SIGNED=false
diff --git a/app/[lng]/evcp/(evcp)/system/page.tsx b/app/[lng]/evcp/(evcp)/system/page.tsx
index fe0a262c..25651b2f 100644
--- a/app/[lng]/evcp/(evcp)/system/page.tsx
+++ b/app/[lng]/evcp/(evcp)/system/page.tsx
@@ -43,9 +43,6 @@ export default async function SystemUserPage(props: IndexPageProps) {
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">SHI Users</h3>
- <p className="text-sm text-muted-foreground">
- 시스템 전체 사용자들을 조회하고 관리할 수 있는 페이지입니다. 사용자에게 롤을 할당하는 것으로 메뉴별 권한을 관리할 수 있습니다.
- </p>
</div>
<Separator />
<UserTable promises={promises} />
diff --git a/components/data-table/data-table.tsx b/components/data-table/data-table.tsx
index b898c2ea..07e4dcd2 100644
--- a/components/data-table/data-table.tsx
+++ b/components/data-table/data-table.tsx
@@ -90,7 +90,7 @@ export function DataTable<TData>({
key={header.id}
colSpan={header.colSpan}
data-column-id={header.column.id}
- className={compactStyles.header}
+ className={cn(compactStyles.header, "whitespace-normal break-words")}
style={{
...getCommonPinningStylesWithBorder({
column: header.column,
@@ -161,7 +161,7 @@ export function DataTable<TData>({
</button>
)}
- <span className="font-semibold">
+ <span className="font-semibold whitespace-normal break-words">
{columnLabel}: {row.getValue(groupingColumnId)}
</span>
<span className="ml-2 text-xs text-muted-foreground">
@@ -188,7 +188,7 @@ export function DataTable<TData>({
<TableCell
key={cell.id}
data-column-id={cell.column.id}
- className={compactStyles.cell}
+ className={cn(compactStyles.cell, "whitespace-normal break-words")}
style={{
...getCommonPinningStylesWithBorder({ column: cell.column }),
width: cell.column.getSize(),
diff --git a/components/knox/approval/ApprovalManager.tsx b/components/knox/approval/ApprovalManager.tsx
index 89450445..554e7680 100644
--- a/components/knox/approval/ApprovalManager.tsx
+++ b/components/knox/approval/ApprovalManager.tsx
@@ -15,10 +15,17 @@ import ApprovalList from './ApprovalList';
interface ApprovalManagerProps {
defaultTab?: string;
+ currentUser?: {
+ id: number | string;
+ name: string | null;
+ email: string;
+ epId: string | null;
+ } | null;
}
export default function ApprovalManager({
- defaultTab = 'submit'
+ defaultTab = 'submit',
+ currentUser,
}: ApprovalManagerProps) {
const [currentTab, setCurrentTab] = useState(defaultTab);
const [selectedApInfId, setSelectedApInfId] = useState<string>('');
@@ -95,7 +102,7 @@ export default function ApprovalManager({
{/* 결재 상신 탭 */}
<TabsContent value="submit" className="space-y-6">
<div className="w-full">
- <ApprovalSubmit onSubmitSuccess={handleSubmitSuccess} />
+ <ApprovalSubmit onSubmitSuccess={handleSubmitSuccess} currentUser={currentUser ?? undefined} />
</div>
</TabsContent>
diff --git a/components/knox/approval/ApprovalSubmit.tsx b/components/knox/approval/ApprovalSubmit.tsx
index bfe66981..d9ccc785 100644
--- a/components/knox/approval/ApprovalSubmit.tsx
+++ b/components/knox/approval/ApprovalSubmit.tsx
@@ -104,7 +104,7 @@ import {
UserSelector,
type UserSelectItem,
} from "@/components/common/user/user-selector";
-import { useSession } from "next-auth/react";
+// next-auth 세션 의존 제거
// UserSelector에서 반환되는 사용자에 epId가 포함될 수 있으므로 확장 타입 정의
interface ExtendedUserSelectItem extends UserSelectItem {
@@ -531,8 +531,8 @@ function SortableApprovalGroup({
export default function ApprovalSubmit({
onSubmitSuccess,
-}: ApprovalSubmitProps) {
- const { data: session } = useSession();
+ currentUser,
+}: ApprovalSubmitProps & { currentUser?: { id: number | string; name: string | null | undefined; email: string; epId?: string | null } }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitResult, setSubmitResult] = useState<{
apInfId: string;
@@ -687,29 +687,28 @@ export default function ApprovalSubmit({
});
};
- // 로그인 사용자를 첫 번째 결재자로 보장하는 effect
+ // 로그인 사용자를 첫 번째 결재자로 보장하는 effect (prop만 사용)
useEffect(() => {
- if (!session?.user) return;
-
- const currentEmail = session.user.email ?? "";
- const currentEpId = (session.user as { epId?: string }).epId;
- const currentUserId = session.user.id ?? undefined;
+ if (!currentUser?.email) return;
+ const effectiveEmail = currentUser.email;
+ const effectiveEpId = currentUser.epId ?? undefined;
+ const effectiveUserId = currentUser.id as string | number | undefined;
let currentAplns = form.getValues("aplns");
// 이미 포함되어 있는지 확인 (epId 또는 email 기준)
const selfIndex = currentAplns.findIndex(
- (a) => (currentEpId && a.epId === currentEpId) || a.emailAddress === currentEmail,
+ (a) => (effectiveEpId && a.epId === effectiveEpId) || a.emailAddress === effectiveEmail,
);
if (selfIndex === -1) {
// 맨 앞에 상신자 추가
const newSelf: FormData["aplns"][number] = {
id: generateUniqueId(),
- epId: currentEpId,
- userId: currentUserId ? currentUserId.toString() : undefined,
- emailAddress: currentEmail,
- name: session.user.name ?? undefined,
+ epId: effectiveEpId,
+ userId: effectiveUserId ? effectiveUserId.toString() : undefined,
+ emailAddress: effectiveEmail,
+ name: currentUser?.name ?? undefined,
role: "0", // 기안
seq: "0",
opinion: "",
@@ -722,7 +721,7 @@ export default function ApprovalSubmit({
currentAplns = currentAplns.map((apln, idx) => ({ ...apln, seq: idx.toString() }));
form.setValue("aplns", currentAplns, { shouldValidate: false, shouldDirty: true });
- }, [session, form]);
+ }, [currentUser, form]);
// dnd-kit sensors
const sensors = useSensors(
@@ -856,19 +855,19 @@ export default function ApprovalSubmit({
setSubmitResult(null);
try {
- // 세션 정보 확인
- if (!session?.user) {
- toast.error("로그인이 필요합니다.");
+ // 사용자 정보 (prop 전용)
+ if (!currentUser) {
+ toast.error("사용자 정보를 불러올 수 없습니다.");
return;
}
- const currentEmail = session.user.email ?? "";
- const currentEpId = (session.user as { epId?: string }).epId;
- const currentUserId = session.user.id ?? "";
+ const effectiveEmail = currentUser.email;
+ const effectiveEpId = currentUser.epId ?? undefined;
+ const effectiveUserId = String(currentUser.id ?? "");
- debugLog("Current User", session.user);
+ debugLog("Current User", { email: effectiveEmail, epId: effectiveEpId, userId: effectiveUserId });
- if (!currentEpId) {
+ if (!effectiveEpId) {
toast.error("사용자 정보가 올바르지 않습니다.");
return;
}
@@ -920,9 +919,9 @@ export default function ApprovalSubmit({
const response = isSecure
? await submitSecurityApproval(submitRequest)
: await submitApproval(submitRequest, {
- userId: currentUserId,
- epId: currentEpId,
- emailAddress: currentEmail,
+ userId: effectiveUserId,
+ epId: effectiveEpId,
+ emailAddress: effectiveEmail,
});
debugLog("Submit Response", response);
diff --git a/config/euserColumnsConfig.ts b/config/euserColumnsConfig.ts
index faa49024..a73b5ba9 100644
--- a/config/euserColumnsConfig.ts
+++ b/config/euserColumnsConfig.ts
@@ -27,32 +27,30 @@ export interface UserColumnConfig {
* 어떤 컬럼들을 어떤 순서로 표시할 것인지 정의.
*/
export const euserColumnsConfig: UserColumnConfig[] = [
- {
- id: "user_name",
- label: "User Name",
- excelHeader: "User Name",
- },
- {
- id: "user_email",
- label: "Email",
- excelHeader: "Email",
- },
- // {
- // id: "company_name",
- // label: "Company Name",
- // excelHeader: "Company Name",
- // },
- {
- id: "roles",
- label: "Roles",
- excelHeader: "Roles",
- // type: "string[]", // 필요하면 추가
- },
- // 필요 시 createdAt도 조인해서 가져왔다면 아래처럼 추가
- {
- id: "created_at",
- label: "Created At",
- excelHeader: "Created At",
- // group: "Metadata",
- },
+ // 성명
+ { id: "user_name", label: "성명", excelHeader: "성명" },
+ // 사번
+ { id: "employee_number", label: "사번", excelHeader: "사번" },
+ // 부서
+ { id: "dept_name", label: "부서", excelHeader: "부서" },
+ // 녹스ID
+ { id: "knox_id", label: "녹스ID", excelHeader: "녹스ID" },
+ // E-Mail
+ { id: "user_email", label: "E-Mail", excelHeader: "E-Mail" },
+ // 잠금여부
+ { id: "is_locked", label: "잠금여부", excelHeader: "잠금여부" },
+ // 휴직여부
+ { id: "is_absent", label: "휴직여부", excelHeader: "휴직여부" },
+ // 삭제여부
+ { id: "is_deleted_on_non_sap", label: "삭제여부", excelHeader: "삭제여부" },
+ // 임직원여부
+ { id: "is_regular_employee", label: "임직원여부", excelHeader: "임직원여부" },
+ // 생성일자
+ { id: "created_at", label: "생성일자", excelHeader: "생성일자", type: "date" },
+ // 수정일자
+ { id: "updated_at", label: "수정일자", excelHeader: "수정일자", type: "date" },
+ // 삭제일자
+ { id: "deactivated_at", label: "삭제일자", excelHeader: "삭제일자", type: "date" },
+ // Role
+ { id: "roles", label: "Role", excelHeader: "Role" },
]; \ No newline at end of file
diff --git a/db/db.ts b/db/db.ts
index 95e3d89a..4a51d870 100644
--- a/db/db.ts
+++ b/db/db.ts
@@ -3,8 +3,8 @@ import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({
- // connectionString: process.env.DATABASE_URL as string,
- connectionString: "postgresql://dts:dujinDTS2@localhost:5432/evcp",
+ connectionString: process.env.DATABASE_URL as string,
+ // connectionString: "postgresql://dts:dujinDTS2@localhost:5432/evcp",
max: Number(process.env.DB_POOL_MAX) || 4,
});
diff --git a/db/schema/NONSAP/nonsap-user.ts b/db/schema/NONSAP/nonsap-user.ts
new file mode 100644
index 00000000..c18244ba
--- /dev/null
+++ b/db/schema/NONSAP/nonsap-user.ts
@@ -0,0 +1,101 @@
+import { nonsapSchema } from './nonsap';
+import { varchar } from 'drizzle-orm/pg-core';
+
+export const nonsapUser = nonsapSchema.table('nonsap_user', {
+ // "USR_ID": "string",
+ USR_ID: varchar({ length: 255 }).primaryKey(),
+ // "USR_NM": "string",
+ USR_NM: varchar({ length: 255 }),
+ // "USR_ENM": "string",
+ USR_ENM: varchar({ length: 255 }),
+ // "EMPNO": "string",
+ EMPNO: varchar({ length: 255 }),
+ // "CO_CD": "string",
+ CO_CD: varchar({ length: 255 }),
+ // "CO_NM": "string",
+ CO_NM: varchar({ length: 255 }),
+ // "DEPTCD": "string",
+ DEPTCD: varchar({ length: 255 }),
+ // "DEPTNM": "string",
+ DEPTNM: varchar({ length: 255 }),
+ // "MAST_DEPTCD": "string",
+ MAST_DEPTCD: varchar({ length: 255 }),
+ // "MAST_DEPTNM": "string",
+ MAST_DEPTNM: varchar({ length: 255 }),
+ // "VNDRCD": "string",
+ VNDRCD: varchar({ length: 255 }),
+ // "VNDRNM": "string",
+ VNDRNM: varchar({ length: 255 }),
+ // "REGL_ORORD_GB": "string",
+ REGL_ORORD_GB: varchar({ length: 255 }),
+ // "JG_CD": "string",
+ JG_CD: varchar({ length: 255 }),
+ // "JG_NM": "string",
+ JG_NM: varchar({ length: 255 }),
+ // "JK_CD": "string",
+ JK_CD: varchar({ length: 255 }),
+ // "JK_NM": "string",
+ JK_NM: varchar({ length: 255 }),
+ // "EMAIL_ADR": "string",
+ EMAIL_ADR: varchar({ length: 255 }),
+ // "TELNO": "string",
+ TELNO: varchar({ length: 255 }),
+ // "HP_NO": "string",
+ HP_NO: varchar({ length: 255 }),
+ // "ADR": "string",
+ ADR: varchar({ length: 255 }),
+ // "MYSNG_ID": "string",
+ MYSNG_ID: varchar({ length: 255 }),
+ // "MYSNG_USR_ID": "string", // 별도 고유값
+ MYSNG_USR_ID: varchar({ length: 255 }),
+ // "MYSNG_USE_YN": "string",
+ MYSNG_USE_YN: varchar({ length: 255 }),
+ // "CHRG_BIZ_NM": "string",
+ CHRG_BIZ_NM: varchar({ length: 255 }),
+ // "FIN_PWD_CHG_DTM": "string",
+ FIN_PWD_CHG_DTM: varchar({ length: 255 }),
+ // "FIN_LGN_DTM": "string",
+ FIN_LGN_DTM: varchar({ length: 255 }),
+ // "FIN_LOGOUT_DTM": "string",
+ FIN_LOGOUT_DTM: varchar({ length: 255 }),
+ // "FIN_LGN_FAIL_TMS": "string",
+ FIN_LGN_FAIL_TMS: varchar({ length: 255 }),
+ // "FIN_USEIP": "string",
+ FIN_USEIP: varchar({ length: 255 }),
+ // "UNLOCK_DTM": "string",
+ UNLOCK_DTM: varchar({ length: 255 }),
+ // "LOCK_YN": "string",
+ LOCK_YN: varchar({ length: 255 }),
+ // "AGR_YN": "string",
+ AGR_YN: varchar({ length: 255 }),
+ // "DEL_YN": "string",
+ DEL_YN: varchar({ length: 255 }),
+ // "BIZLOC_GB_CD": "string",
+ BIZLOC_GB_CD: varchar({ length: 255 }),
+ // "BIZLOC_GB_NM": "string",
+ BIZLOC_GB_NM: varchar({ length: 255 }),
+ // "GRD_NM": "string",
+ GRD_NM: varchar({ length: 255 }),
+ // "CH_DEPTCD": "string",
+ CH_DEPTCD: varchar({ length: 255 }),
+ // "CH_DEPTNM": "string",
+ CH_DEPTNM: varchar({ length: 255 }),
+ // "ORG_OTHER_NAME": "string",
+ ORG_OTHER_NAME: varchar({ length: 255 }),
+ // "GRADE_OTHER_NAME": "string",
+ GRADE_OTHER_NAME: varchar({ length: 255 }),
+ // "FAX_NO": "string",
+ FAX_NO: varchar({ length: 255 }),
+ // "FS_INPR_ID": "string",
+ FS_INPR_ID: varchar({ length: 255 }),
+ // "FS_INP_DTM": "string",
+ FS_INP_DTM: varchar({ length: 255 }),
+ // "FIN_CHGR_ID": "string",
+ FIN_CHGR_ID: varchar({ length: 255 }),
+ // "FIN_CHG_DTM": "string",
+ FIN_CHG_DTM: varchar({ length: 255 }),
+ // "LOFF_GB": "string",
+ LOFF_GB: varchar({ length: 255 }),
+ // "DEL_DTM": "string",
+ DEL_DTM: varchar({ length: 255 }),
+});
diff --git a/db/schema/index.ts b/db/schema/index.ts
index 5b712b40..9cd71197 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -47,6 +47,7 @@ export * from './SOAP/soap';
// NONSAP Oracle DB 스키마
export * from './NONSAP/nonsap';
+export * from './NONSAP/nonsap-user'; // 김희은 프로 요청사항(모든 유저 데이터는 shi-api를 통한 nonspa 기준으로 처리)
// ECC SOAP 수신용 (RFQ, PO, PR 데이터)
export * from './ECC/ecc';
diff --git a/db/schema/users.ts b/db/schema/users.ts
index 0d727bb4..9977a442 100644
--- a/db/schema/users.ts
+++ b/db/schema/users.ts
@@ -59,6 +59,14 @@ export const users = pgTable("users", {
// emailVerifiedAt: timestamp("email_verified_at", { withTimezone: true }),
// registrationCompleted: boolean("registration_completed").default(false).notNull(),
+ // 김희은 프로 요구사항으로 추가
+ employeeNumber: varchar("employee_number", { length: 50 }),
+ knoxId: varchar("knox_id", { length: 50 }),
+ nonsapUserId: varchar("nonsap_user_id", { length: 50 }).unique(),
+ isAbsent: boolean("is_absent"), // 휴직여부 (SHI-API LOFF_GB (Y/N))
+ isDeletedOnNonSap: boolean("is_deleted_on_non_sap"), // 퇴직여부 (SHI-API DEL_YN (Y/N))
+ isRegularEmployee: boolean("is_regular_employee"), // 정직원여부 (SHI-API REGL_ORORD_GB (S/N))
+
}, (table) => {
return {
emailIdx: uniqueIndex("users_email_idx").on(table.email),
@@ -286,6 +294,17 @@ export const userView = pgView("user_view").as((qb) => {
user_image: sql<string>`${users.imageUrl}`.as("user_image"),
+ // 추가: 사번, 부서, 녹스ID
+ employee_number: sql<string | null>`${users.employeeNumber}`.as("employee_number"),
+ dept_name: sql<string | null>`${users.deptName}`.as("dept_name"),
+ knox_id: sql<string | null>`${users.knoxId}`.as("knox_id"),
+
+ // 추가: 계정 상태 플래그
+ is_locked: sql<boolean>`${users.isLocked}`.as("is_locked"),
+ is_absent: sql<boolean | null>`${users.isAbsent}`.as("is_absent"),
+ is_deleted_on_non_sap: sql<boolean | null>`${users.isDeletedOnNonSap}`.as("is_deleted_on_non_sap"),
+ is_regular_employee: sql<boolean | null>`${users.isRegularEmployee}`.as("is_regular_employee"),
+
// 4) companyId: number | null
company_id: sql<number | null>`${vendors.id}`.as("company_id"),
@@ -297,8 +316,10 @@ export const userView = pgView("user_view").as((qb) => {
roles: sql<string[]>`
array_agg(${roles.name})
`.as("roles"),
- // 7) createdAt: Date
+ // 7) created/updated/deactivated dates
created_at: sql<Date>`${users.createdAt}`.as("created_at"),
+ updated_at: sql<Date>`${users.updatedAt}`.as("updated_at"),
+ deactivated_at: sql<Date | null>`${users.deactivatedAt}`.as("deactivated_at"),
})
.from(users)
.leftJoin(vendors, eq(users.companyId, vendors.id))
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 93b6e10d..6da96b8e 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -5,8 +5,8 @@ export default defineConfig({
schema: "./db/schema/index.ts",
dialect: 'postgresql',
dbCredentials: {
- // url: process.env.DATABASE_URL!,
- url: "postgresql://dts:dujinDTS2@localhost:5432/evcp"
+ url: process.env.DATABASE_URL!,
+ // url: "postgresql://dts:dujinDTS2@localhost:5432/evcp"
},
});
diff --git a/instrumentation.ts b/instrumentation.ts
index db8da371..a98cda12 100644
--- a/instrumentation.ts
+++ b/instrumentation.ts
@@ -30,7 +30,7 @@ export async function register() {
}
try {
- // Procurement 동기화 스케줄러 시작 (지불조건, 인코텀즈, 선적/하역지)
+ // Procurement 동기화 스케줄러 시작 (지불조건, 인코텀즈, 선적지, 하역지)
const { startProcurementSyncScheduler } = await import(
'./lib/nonsap-sync/procurement-sync-service'
);
@@ -40,5 +40,14 @@ export async function register() {
console.error('Failed to start Procurement sync scheduler.');
// 스케줄러 실패해도 애플리케이션은 계속 실행
}
+
+ try {
+ // SHI-API NONSAP 사용자 동기화 - 1일 1회 CRON 등록
+ const { startShiApiUsersDailySyncScheduler } = await import('./lib/shi-api/users-sync-scheduler');
+ await startShiApiUsersDailySyncScheduler();
+ } catch {
+ console.error('Failed to start SHI-API users daily cron scheduler.');
+ // 스케줄러 실패해도 애플리케이션은 계속 실행
+ }
}
}
diff --git a/lib/knox-sync/master-sync-service.ts b/lib/knox-sync/master-sync-service.ts
index ed77a3fd..8950f514 100644
--- a/lib/knox-sync/master-sync-service.ts
+++ b/lib/knox-sync/master-sync-service.ts
@@ -34,9 +34,10 @@ export async function syncAllKnoxData(): Promise<void> {
console.log('[KNOX-SYNC] 2/3: 조직 동기화 완료 ✅');
// 3단계: 임직원 동기화 (조직 완료 후)
- console.log('[KNOX-SYNC] 3/3: 임직원 동기화 시작');
- await syncKnoxEmployees();
- console.log('[KNOX-SYNC] 3/3: 임직원 동기화 완료 ✅');
+ console.log('[KNOX-SYNC] 3/3: 임직원 동기화는 생략 (SHI-API를 통한 nonsap 사용자로 동기화함');
+ // console.log('[KNOX-SYNC] 3/3: 임직원 동기화 시작');
+ // await syncKnoxEmployees();
+ // console.log('[KNOX-SYNC] 3/3: 임직원 동기화 완료 ✅');
const overallDuration = Math.round((Date.now() - overallStartTime) / 1000);
console.log(`[KNOX-SYNC] 🎉 Knox 통합 동기화 완료 - 총 ${overallDuration}초 소요`);
diff --git a/lib/sedp/sedp-token.ts b/lib/sedp/sedp-token.ts
index 9335a74e..0aa3b185 100644
--- a/lib/sedp/sedp-token.ts
+++ b/lib/sedp/sedp-token.ts
@@ -36,8 +36,8 @@ export async function getSEDPToken(): Promise<string> {
const jsonData = JSON.parse(tokenData);
if (typeof jsonData === 'string') {
return jsonData; // JSON 문자열이지만 내용물이 토큰 문자열인 경우
- } else if (jsonData.token) {
- return jsonData.token; // { token: "..." } 형태인 경우
+ } else if (jsonData.Token) {
+ return jsonData.Token; // { Token: "..." } 형태인 경우
} else {
console.warn('예상치 못한 토큰 응답 형식:', jsonData);
// 가장 가능성 있는 필드를 찾아봄
diff --git a/lib/shi-api/shi-api-utils.ts b/lib/shi-api/shi-api-utils.ts
new file mode 100644
index 00000000..ddbc186f
--- /dev/null
+++ b/lib/shi-api/shi-api-utils.ts
@@ -0,0 +1,125 @@
+'use server';
+
+import { nonsapUser, users } from '@/db/schema';
+import db from '@/db/db';
+import { sql } from 'drizzle-orm';
+import { debugError, debugLog, debugWarn, debugSuccess } from '@/lib/debug-utils';
+
+const shiApiBaseUrl = process.env.SHI_API_BASE_URL;
+const shiNonsapUserSegment = process.env.SHI_NONSAP_USER_SEGMENT;
+const shiApiJwtToken = process.env.SHI_API_JWT_TOKEN;
+
+type NonsapUser = typeof nonsapUser.$inferSelect;
+type NonsapUserInsert = typeof nonsapUser.$inferInsert;
+type InsertUser = typeof users.$inferInsert;
+
+export const getAllNonsapUser = async () => {
+ try{
+ debugLog('Starting NONSAP user sync via SHI-API');
+ if (!shiApiBaseUrl || !shiNonsapUserSegment || !shiApiJwtToken) {
+ throw new Error('SHI API 환경변수가 설정되지 않았습니다. (SHI_API_BASE_URL, SHI_NONSAP_USER_SEGMENT, SHI_API_JWT_TOKEN)');
+ }
+
+ const ynToBool = (value: string | null | undefined) => (value || '').toUpperCase() === 'Y';
+
+ // ** 1. 전체 데이터 조회해 응답 받음 (js 배열) **
+ const response = await fetch(`${shiApiBaseUrl}${shiNonsapUserSegment}`, {
+ headers: {
+ Authorization: `Bearer ${shiApiJwtToken}`,
+ },
+ cache: 'no-store',
+ });
+
+ if (!response.ok) {
+ const text = await response.text().catch(() => '');
+ throw new Error(`SHI-API 요청 실패: ${response.status} ${response.statusText} ${text}`);
+ }
+
+ const data: NonsapUser[] = await response.json();
+ debugSuccess(`[SHI-API] fetched ${Array.isArray(data) ? data.length : 0} users`);
+
+ // ** 2. 받은 데이터를 DELETE & INSERT 방식으로 수신 테이블 (nonsap-user) 에 저장 **
+ await db.delete(nonsapUser); // 전체 정리
+ if (Array.isArray(data) && data.length > 0) {
+ await db.insert(nonsapUser).values(data as unknown as NonsapUserInsert[]); // 데이터 저장 (스키마 컬럼 그대로)
+ debugSuccess(`[STAGE] nonsap_user refreshed with ${data.length} records`);
+ }
+
+ // ** 3. 데이터 저장 이후, 비즈니스 테이블인 "public"."users" 에 동기화 시킴 (매핑 필요) **
+ const now = new Date();
+
+ const mappedRaw: Partial<InsertUser>[] = (Array.isArray(data) ? data : [])
+ .map((u: NonsapUser): Partial<InsertUser> => {
+ const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부
+ const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부
+ const notApproved = (u.AGR_YN || '').toUpperCase() === 'N'; // nonsap user 테이블에서 승인여부
+ const isActive = !(isDeleted || isAbsent || notApproved); // eVCP 내에서 활성화 여부
+ // S = 정직원
+ const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S';
+
+ return {
+ // upsert key = USR_ID
+ nonsapUserId: u.USR_ID || undefined,
+
+
+ // mapped fields
+ employeeNumber: u.EMPNO || undefined,
+ knoxId: u.MYSNG_ID || undefined,
+ name: u.USR_NM || undefined,
+ email: u.EMAIL_ADR || undefined,
+ epId: u.MYSNG_ID || undefined,
+ deptCode: u.CH_DEPTCD || undefined,
+ deptName: u.CH_DEPTNM || undefined,
+ phone: u.TELNO || undefined,
+ isAbsent,
+ isDeletedOnNonSap: isDeleted,
+ isActive,
+ isRegularEmployee,
+ };
+ });
+ // users 테이블 제약조건 대응: email, name 은 not null + nonsapUserId 존재
+ //.filter((u) => typeof u.email === 'string' && !!u.email && typeof u.name === 'string' && !!u.name && typeof u.nonsapUserId === 'string' && u.nonsapUserId.length > 0);
+
+ const mappedUsers = mappedRaw as InsertUser[];
+
+ if (mappedUsers.length > 0) {
+ await db.insert(users)
+ .values(mappedUsers)
+ .onConflictDoUpdate({
+ target: users.nonsapUserId,
+ set: {
+ name: sql`excluded.name`,
+ employeeNumber: sql`excluded.employeeNumber`,
+ knoxId: sql`excluded.knoxId`,
+ epId: sql`excluded."epId"`,
+ deptCode: sql`excluded."deptCode"`,
+ deptName: sql`excluded."deptName"`,
+ phone: sql`excluded.phone`,
+ nonsapUserId: sql`excluded."nonsapUserId"`,
+ isAbsent: sql`excluded."isAbsent"`,
+ isDeletedOnNonSap: sql`excluded."isDeletedOnNonSap"`,
+ isActive: sql`excluded."isActive"`,
+ isRegularEmployee: sql`excluded."isRegularEmployee"`,
+ updatedAt: sql`now()`,
+ },
+ });
+ debugSuccess(`[UPSERT] users upserted=${mappedUsers.length} using key=nonsapUserId`);
+ } else {
+ debugWarn('[UPSERT] No users mapped for upsert (missing name/email or invalid USR_ID)');
+ }
+
+ // 휴직 사용자도 API에서 수신하므로, 기존 사용자와의 비교를 통한 휴직 처리 로직은 더 이상 필요하지 않음
+
+ return {
+ fetched: Array.isArray(data) ? data.length : 0,
+ staged: Array.isArray(data) ? data.length : 0,
+ upserted: mappedUsers.length,
+ skippedDueToMissingRequiredFields: (Array.isArray(data) ? data.length : 0) - mappedUsers.length,
+ ranAt: now.toISOString(),
+ };
+ } catch(error){
+ debugError('SHI-API 동기화 실패', error);
+ console.error("SHI-API 를 통한 유저 동기화 프로세스 간 실패 발생: ", error);
+ throw error;
+ }
+};
diff --git a/lib/shi-api/users-sync-scheduler.ts b/lib/shi-api/users-sync-scheduler.ts
new file mode 100644
index 00000000..1cca3441
--- /dev/null
+++ b/lib/shi-api/users-sync-scheduler.ts
@@ -0,0 +1,42 @@
+'use server';
+
+import * as cron from 'node-cron';
+import { getAllNonsapUser } from './shi-api-utils';
+
+// 기본: 매일 01:00 KST 실행. 환경변수로 오버라이드 가능
+const CRON_STRING = process.env.SHI_API_USERS_SYNC_CRON || '0 1 * * *';
+
+/**
+ * SHI-API NONSAP 사용자 동기화 - 일일 스케줄러 등록
+ */
+export async function startShiApiUsersDailySyncScheduler(): Promise<void> {
+ try {
+ cron.schedule(
+ CRON_STRING,
+ async () => {
+ try {
+ console.log('[SHI-API] CRON 실행: NONSAP 사용자 동기화 시작');
+ await getAllNonsapUser();
+ console.log('[SHI-API] CRON 완료: NONSAP 사용자 동기화 성공');
+ } catch (error) {
+ console.error('[SHI-API] CRON 실패: NONSAP 사용자 동기화 오류', error);
+ }
+ },
+ { timezone: 'Asia/Seoul' },
+ );
+
+ console.log('[SHI-API] Daily NONSAP user sync cron registered:', CRON_STRING);
+ } catch (error) {
+ console.error('Failed to set up SHI-API users daily cron scheduler.', error);
+ }
+
+ try {
+ if(process.env.NONSAP_USERSYNC_FIRST_RUN === 'true') {
+ await getAllNonsapUser();
+ }
+ } catch (error) {
+ console.error('Failed to sync NONSAP users in first run mode.', error);
+ }
+}
+
+
diff --git a/lib/users/auth/verifyCredentails.ts b/lib/users/auth/verifyCredentails.ts
index a5dbab41..5cb9c24f 100644
--- a/lib/users/auth/verifyCredentails.ts
+++ b/lib/users/auth/verifyCredentails.ts
@@ -510,7 +510,7 @@ export async function verifySGipsCredentials(
method: 'GET',
headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${process.env.S_GIPS_TOKEN}`,
+ 'Authorization': `Bearer ${process.env.SHI_API_JWT_TOKEN}`,
},
});
diff --git a/lib/users/table/users-table-columns.tsx b/lib/users/table/users-table-columns.tsx
index 217fefcf..d4c5c78a 100644
--- a/lib/users/table/users-table-columns.tsx
+++ b/lib/users/table/users-table-columns.tsx
@@ -6,7 +6,6 @@ import { type ColumnDef } from "@tanstack/react-table"
import { Ellipsis } from "lucide-react"
import { userRoles, type UserView } from "@/db/schema/users"
-import { formatDate } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
@@ -96,10 +95,28 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<UserVie
type: cfg.type,
},
cell: ({ row, cell }) => {
+ // 날짜 컬럼: YYYY-MM-DD HH:mm
+ if (cfg.id === "created_at" || cfg.id === "updated_at" || cfg.id === "deactivated_at") {
+ const v = cell.getValue() as Date | string | null | undefined
+ if (!v) return ""
+ const d = new Date(v as any)
+ const y = d.getFullYear()
+ const m = String(d.getMonth() + 1).padStart(2, "0")
+ const day = String(d.getDate()).padStart(2, "0")
+ const hh = String(d.getHours()).padStart(2, "0")
+ const mm = String(d.getMinutes()).padStart(2, "0")
+ return `${y}-${m}-${day} ${hh}:${mm}`
+ }
- if (cfg.id === "created_at") {
- const dateVal = cell.getValue() as Date
- return formatDate(dateVal, "KR")
+ // 불리언 컬럼: Y/N
+ if (
+ cfg.id === "is_locked" ||
+ cfg.id === "is_absent" ||
+ cfg.id === "is_deleted_on_non_sap" ||
+ cfg.id === "is_regular_employee"
+ ) {
+ const v = row.getValue(cfg.id) as boolean | null | undefined
+ return v === true ? "Y" : v === false ? "N" : ""
}
if (cfg.id === "roles") {
diff --git a/public/wsdl/IF_ECC_EVCP_PO_INFORMATION.wsdl b/public/wsdl/IF_ECC_EVCP_PO_INFORMATION.wsdl
index 38b5f43d..02b3276a 100644
--- a/public/wsdl/IF_ECC_EVCP_PO_INFORMATION.wsdl
+++ b/public/wsdl/IF_ECC_EVCP_PO_INFORMATION.wsdl
@@ -20,15 +20,6 @@
<xs:sequence>
<!-- Header 레코드 집합 -->
<xs:element name="ZMM_HD" type="tns:ZMM_HD" maxOccurs="unbounded" minOccurs="0"/>
- <!-- 지불방법 레코드 집합 (ZMM_HD의 하위 테이블) -->
- <xs:element name="ZMM_PAY" type="tns:ZMM_PAY" maxOccurs="unbounded" minOccurs="0"/>
- <!-- PO Detail 레코드 집합 (ZMM_HD의 하위 테이블) -->
- <xs:element name="ZMM_DT" type="tns:ZMM_DT" maxOccurs="unbounded" minOccurs="0"/>
- <!-- KN 은 DT의 하위 테이블이므로 생략 -->
- <!-- PO Note 1 (ZMM_HD의 하위 테이블) -->
- <xs:element name="ZMM_NOTE" type="tns:ZMM_NOTE" maxOccurs="unbounded" minOccurs="0"/>
- <!-- PO Note 2 (ZMM_HD의 하위 테이블) -->
- <xs:element name="ZMM_NOTE2" type="tns:ZMM_NOTE2" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
@@ -171,6 +162,18 @@
<xs:element name="ZWEBELN" type="xs:string" minOccurs="0"/>
<!-- SEQ:68, Table:ZMM_HD, Field:ZVER_NO, M/O:, Type:NUMC, Size:3, Description:서면계약차수 -->
<xs:element name="ZVER_NO" type="xs:string" minOccurs="0"/>
+
+ <!-- 하위 테이블 레코드 집합 -->
+ <!-- 지불방법 레코드 집합 (ZMM_HD의 하위 테이블) -->
+ <xs:element name="ZMM_PAY" type="tns:ZMM_PAY" maxOccurs="unbounded" minOccurs="0"/>
+ <!-- PO Detail 레코드 집합 (ZMM_HD의 하위 테이블) -->
+ <xs:element name="ZMM_DT" type="tns:ZMM_DT" maxOccurs="unbounded" minOccurs="0"/>
+ <!-- KN 은 DT의 하위 테이블이므로 생략 -->
+ <!-- PO Note 1 (ZMM_HD의 하위 테이블) -->
+ <xs:element name="ZMM_NOTE" type="tns:ZMM_NOTE" maxOccurs="unbounded" minOccurs="0"/>
+ <!-- PO Note 2 (ZMM_HD의 하위 테이블) -->
+ <xs:element name="ZMM_NOTE2" type="tns:ZMM_NOTE2" maxOccurs="unbounded" minOccurs="0"/>
+
</xs:sequence>
</xs:complexType>