diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-14 13:15:21 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-14 13:15:21 +0000 |
| commit | 49d236df3bd2bd976ebc424644f34f5affa1074f (patch) | |
| tree | 7b0f60c399e724847894061fae74876aa1bf5c7e | |
| parent | 969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 (diff) | |
(김준회) 결재 테스트 모듈 수정, 환경병수 eVCP 운영 대응, SGIPS JWT TOKEN 수정, SHI-API 기반 유저 관리 추가, 유저목록 테이블 변경
| -rw-r--r-- | .env.development | 61 | ||||
| -rw-r--r-- | .env.production | 69 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/page.tsx | 3 | ||||
| -rw-r--r-- | components/data-table/data-table.tsx | 6 | ||||
| -rw-r--r-- | components/knox/approval/ApprovalManager.tsx | 11 | ||||
| -rw-r--r-- | components/knox/approval/ApprovalSubmit.tsx | 51 | ||||
| -rw-r--r-- | config/euserColumnsConfig.ts | 54 | ||||
| -rw-r--r-- | db/db.ts | 4 | ||||
| -rw-r--r-- | db/schema/NONSAP/nonsap-user.ts | 101 | ||||
| -rw-r--r-- | db/schema/index.ts | 1 | ||||
| -rw-r--r-- | db/schema/users.ts | 23 | ||||
| -rw-r--r-- | drizzle.config.ts | 4 | ||||
| -rw-r--r-- | instrumentation.ts | 11 | ||||
| -rw-r--r-- | lib/knox-sync/master-sync-service.ts | 7 | ||||
| -rw-r--r-- | lib/sedp/sedp-token.ts | 4 | ||||
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 125 | ||||
| -rw-r--r-- | lib/shi-api/users-sync-scheduler.ts | 42 | ||||
| -rw-r--r-- | lib/users/auth/verifyCredentails.ts | 2 | ||||
| -rw-r--r-- | lib/users/table/users-table-columns.tsx | 25 | ||||
| -rw-r--r-- | public/wsdl/IF_ECC_EVCP_PO_INFORMATION.wsdl | 21 |
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 @@ -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> |
