summaryrefslogtreecommitdiff
path: root/lib/docuSign
diff options
context:
space:
mode:
Diffstat (limited to 'lib/docuSign')
-rw-r--r--lib/docuSign/docuSignFns.ts383
-rw-r--r--lib/docuSign/jwtConfig/README.md54
-rw-r--r--lib/docuSign/jwtConfig/jwtConfig.json6
-rw-r--r--lib/docuSign/jwtConfig/private.key29
-rw-r--r--lib/docuSign/types.ts37
5 files changed, 509 insertions, 0 deletions
diff --git a/lib/docuSign/docuSignFns.ts b/lib/docuSign/docuSignFns.ts
new file mode 100644
index 00000000..87977a0b
--- /dev/null
+++ b/lib/docuSign/docuSignFns.ts
@@ -0,0 +1,383 @@
+"use server";
+
+import docusign from "docusign-esign";
+import fs from "fs";
+import path from "path";
+import jwtConfig from "./jwtConfig/jwtConfig.json";
+import dayjs from "dayjs";
+import { ContractInfo, ContractorInfo } from "./types";
+
+const SCOPES = ["signature", "impersonation"];
+
+//DocuSign 인증 정보
+async function authenticate(): Promise<
+ | undefined
+ | {
+ accessToken: string;
+ apiAccountId: string;
+ basePath: string;
+ }
+> {
+ const jwtLifeSec = 10 * 60;
+ const dsApi = new docusign.ApiClient();
+ dsApi.setOAuthBasePath(jwtConfig.dsOauthServer.replace("https://", ""));
+ const privateKeyPath = path.resolve(
+ process.cwd(),
+ jwtConfig.privateKeyLocation
+ );
+
+ let rsaKey: Buffer = fs.readFileSync(privateKeyPath);
+
+ try {
+ const results = await dsApi.requestJWTUserToken(
+ jwtConfig.dsJWTClientId,
+ jwtConfig.impersonatedUserGuid,
+ SCOPES,
+ rsaKey,
+ jwtLifeSec
+ );
+ const accessToken = results.body.access_token;
+
+ const userInfoResults = await dsApi.getUserInfo(accessToken);
+ let userInfo = userInfoResults.accounts.find(
+ (account: Partial<{ isDefault: string }>) => account.isDefault === "true"
+ );
+
+ return {
+ accessToken: results.body.access_token,
+ apiAccountId: userInfo.accountId,
+ basePath: `${userInfo.baseUri}/restapi`,
+ };
+ } catch (e) {
+ console.error("❌ 인증 실패:", e);
+ }
+}
+
+async function getSignerId(
+ basePath: string,
+ accountId: string,
+ accessToken: string,
+ envelopeId: string,
+ roleName: string
+): Promise<string | null> {
+ const apiClient = new docusign.ApiClient();
+ apiClient.setBasePath(basePath);
+ apiClient.addDefaultHeader("Authorization", "Bearer " + accessToken);
+
+ const envelopesApi = new docusign.EnvelopesApi(apiClient);
+
+ try {
+ const recipients = await envelopesApi.listRecipients(accountId, envelopeId);
+
+ const singers = recipients?.signers ?? [];
+
+ // 🔹 특정 서명자(Role Name 기준)의 Recipient ID 찾기
+ const signer = singers.find((s) => s.roleName === roleName);
+ if (!signer) {
+ console.error("❌ 해당 Role Name을 가진 서명자를 찾을 수 없습니다.");
+ return null;
+ }
+
+ return signer.recipientId as string;
+ } catch (error) {
+ console.error("❌ 서명자 ID 조회 실패:", error);
+ return null;
+ }
+}
+
+//계약서 서명 요청
+export async function requestContractSign(
+ contractTemplateId: string,
+ contractInfo: ContractInfo[],
+ subcontractorinfo: ContractorInfo,
+ contractorInfo: ContractorInfo,
+ ccInfo: ContractorInfo[],
+ brandId: string | undefined = undefined
+): Promise<
+ Partial<{
+ result: boolean;
+ envelopeId: string;
+ error: any;
+ }>
+> {
+ let accountInfo = await authenticate();
+ if (accountInfo) {
+ const { accessToken, basePath, apiAccountId } = accountInfo;
+ const {
+ email: subEmail,
+ name: subConName,
+ roleName: subRoleName,
+ } = subcontractorinfo;
+
+ const {
+ email: conEmail,
+ name: conName,
+ roleName: roleName,
+ } = contractorInfo;
+
+ const apiClient = new docusign.ApiClient();
+ apiClient.setBasePath(basePath);
+ apiClient.addDefaultHeader("Authorization", "Bearer " + accessToken);
+ const envelopesApi = new docusign.EnvelopesApi(apiClient);
+
+ const signer1: docusign.TemplateRole = {
+ email: subEmail,
+ name: subConName,
+ roleName: subRoleName,
+ };
+
+ const signer1Tabs: docusign.Tabs = {
+ textTabs: [
+ ...contractInfo.map((c): docusign.Text => {
+ const textField: docusign.Text = {
+ tabLabel: c.tabLabel,
+ value: c.value,
+ locked: "true",
+ };
+ return textField;
+ }),
+ ],
+ };
+
+ const signer2: docusign.TemplateRole = {
+ email: conEmail,
+ name: conName,
+ roleName: roleName,
+ };
+
+ const signer2Tabs: docusign.Tabs = {
+ dateSignedTabs: [
+ {
+ tabLabel: "contract_complete_date",
+ },
+ ],
+ };
+
+ signer1.tabs = signer1Tabs;
+ signer2.tabs = signer2Tabs;
+
+ const envelopeDefinition: docusign.EnvelopeDefinition = {
+ templateId: contractTemplateId,
+ templateRoles: [signer1, signer2, ...ccInfo], // 두 명의 서명자 추가
+ status: "sent", // 즉시 발송
+ };
+
+ if (brandId) {
+ envelopeDefinition.brandId = brandId;
+ }
+
+ try {
+ let envelopeSummary = await envelopesApi.createEnvelope(apiAccountId, {
+ envelopeDefinition,
+ });
+
+ // console.log("✅ 서명 요청 완료, Envelope ID:", envelopeSummary);
+ return {
+ result: true,
+ envelopeId: envelopeSummary.envelopeId,
+ };
+ } catch (error) {
+ console.dir(error);
+ return {
+ result: false,
+ error,
+ };
+ }
+ } else {
+ return {
+ result: false,
+ };
+ }
+}
+
+//서명된 계약서 다운로드
+export async function downloadContractFile(envelopeId: string): Promise<
+ Partial<{
+ result: boolean;
+ fileName: string;
+ buffer: Buffer;
+ envelopeId: string;
+ error: any;
+ }>
+> {
+ let accountInfo = await authenticate();
+
+ if (accountInfo) {
+ const { accessToken, apiAccountId, basePath } = accountInfo;
+
+ const apiClient = new docusign.ApiClient();
+ apiClient.setBasePath(basePath);
+ apiClient.addDefaultHeader("Authorization", "Bearer " + accessToken);
+
+ const envelopesApi = new docusign.EnvelopesApi(apiClient);
+
+ try {
+ //Document ID 등 파일 정보를 호출
+ const response = await envelopesApi.listDocuments(
+ apiAccountId,
+ envelopeId,
+ null
+ );
+
+ const { envelopeDocuments } = response || { envelopeDocuments: [] };
+
+ if (Array.isArray(envelopeDocuments) && envelopeDocuments.length > 0) {
+ const { documentId, name } = envelopeDocuments[0] as {
+ documentId: string;
+ name: string;
+ };
+
+ //Document Buffer 호출
+ const downloadFile = await envelopesApi.getDocument(
+ apiAccountId,
+ envelopeId,
+ documentId,
+ {}
+ );
+
+ if (documentId && documentId !== "certificate") {
+ const bufferData: Buffer = downloadFile as unknown as Buffer;
+ return {
+ result: true,
+ fileName: name,
+ buffer: bufferData,
+ envelopeId,
+ };
+ }
+ }
+
+ return {
+ result: false,
+ };
+ } catch (error) {
+ return {
+ result: false,
+ error,
+ };
+ }
+ } else {
+ return {
+ result: false,
+ };
+ }
+}
+
+//최종 서명 날짜 찾기
+export async function findContractCompleteTime(
+ envelopeId: string,
+ lastSignerRoleName: string
+): Promise<{
+ completedDateTime: string;
+ year: string;
+ month: string;
+ day: string;
+ time: string;
+} | null> {
+ let accountInfo = await authenticate();
+
+ if (!accountInfo) {
+ console.error("❌ 인증 실패: API 요청을 중단합니다.");
+ return null;
+ }
+
+ const { accessToken, apiAccountId: accountId, basePath } = accountInfo;
+
+ const apiClient = new docusign.ApiClient();
+ apiClient.setBasePath(basePath);
+ apiClient.addDefaultHeader("Authorization", "Bearer " + accessToken);
+
+ const envelopesApi = new docusign.EnvelopesApi(apiClient);
+
+ try {
+ const envelope = await envelopesApi.getEnvelope(accountId, envelopeId);
+ if (!envelope.completedDateTime) {
+ console.error("❌ 서명 완료 날짜가 없습니다.");
+ return null;
+ }
+
+ // 🔹 `SIGNER_ID` 가져오기
+ const signerId = await getSignerId(
+ basePath,
+ accountId,
+ accessToken,
+ envelopeId,
+ lastSignerRoleName
+ );
+ if (!signerId) {
+ console.error("❌ 서명자 ID를 찾을 수 없습니다.");
+ return null;
+ }
+
+ const completedDate = dayjs(envelope.completedDateTime);
+ const year = completedDate.format("YYYY").toString();
+ const month = completedDate.format("MM").toString();
+ const day = completedDate.format("DD").toString();
+ const time = completedDate.format("HH:mm").toString();
+
+ return {
+ completedDateTime: envelope.completedDateTime,
+ year,
+ month,
+ day,
+ time,
+ };
+ } catch (error) {
+ console.error("❌ 서명 완료 후 날짜 추가 실패:", error);
+ return null;
+ }
+}
+
+export async function getRecipients(
+ envelopeId: string,
+ recipientId: string
+): Promise<{ result: boolean; message?: string }> {
+ try {
+ let accountInfo = await authenticate();
+
+ if (!accountInfo) {
+ console.error("❌ 인증 실패: API 요청을 중단합니다.");
+ return {
+ result: false,
+ message: "인증 실패: API 요청을 중단합니다.",
+ };
+ }
+
+ const { accessToken, apiAccountId: accountId, basePath } = accountInfo;
+
+ const apiClient = new docusign.ApiClient();
+ apiClient.setBasePath(basePath);
+ apiClient.addDefaultHeader("Authorization", "Bearer " + accessToken);
+
+ const envelopesApi = new docusign.EnvelopesApi(apiClient);
+
+ const response = await envelopesApi.listRecipients(accountId, envelopeId);
+
+ const singers: { [key: string]: any }[] = response?.signers ?? [];
+
+ // 🔹 특정 서명자(Role Name 기준)의 Recipient ID 찾기
+ const signer = singers.find((s) => s.recipientId === recipientId);
+ if (!signer) {
+ console.error("❌ 해당 Role Name을 가진 서명자를 찾을 수 없습니다.");
+ return {
+ result: false,
+ message: "해당 Recipient id를 가진 서명자를 찾을 수 없습니다.",
+ };
+ }
+
+ const { autoRespondedReason, status } = signer;
+
+ if (autoRespondedReason || status === "status") {
+ return {
+ result: false,
+ message: autoRespondedReason,
+ };
+ }
+
+ return {
+ result: true,
+ };
+ } catch (error) {
+ console.error("Error retrieving recipients:", error);
+ return { result: false, message: (error as Error).message };
+ }
+}
diff --git a/lib/docuSign/jwtConfig/README.md b/lib/docuSign/jwtConfig/README.md
new file mode 100644
index 00000000..7c997d07
--- /dev/null
+++ b/lib/docuSign/jwtConfig/README.md
@@ -0,0 +1,54 @@
+# DocuSign
+
+## DocuSign Contract Template
+
+### DocuSign Delveloper Account
+
+1. ID: kiman.kim@dtsolution.co.kr
+2. PW: rlaks!153
+
+### jwtConfig.json
+
+1. DocuSign Developer 로그인
+2. DocuSign Developer Admin 메뉴 이동
+3. DocuSign 좌측 메뉴 바에서 INTERGRATIONS > Apps and Keys 이동
+
+```jwtConfig.json
+{
+ //Add App and Intergraion Key 시 private.key 파일 생성 (처음 key를 만들때만 저장 가능함.)
+ "privateKeyLocation": private.key 파일 경로,
+ "dsJWTClientId": Apps and Intergration Keys 내 Intergration Kzey,
+ "impersonatedUserGuid": My Account Information 내 User ID,
+ //개발환경: https://account-d.docusign.com
+ //운영환경: https://account.docusign.com
+ "dsOauthServer": "https://account-d.docusign.com"
+}
+```
+
+### DocuSign Web Hook
+
+1. DocuSign Developer 로그인
+2. DocuSign Developer Admin 메뉴 이동
+3. DocuSign 좌측 메뉴 바에서 INTERGRATIONS > Connect 이동
+4. Add Configuration > Custom
+5. Web Hook Url 입력
+6. Trigger Events
+ 6.1. Envelope Signed/Completed - Check
+ 6.2. Envelope Declined - Check
+ 6.3. Recipient Sent - Check
+ 6.4. Recipient Delivered - Check
+ 6.5. Recipient Signed/Completed - Check
+ 6.6. Recipient Declined - Check
+
+### DocuSign Mail Sender Info Change
+
+1. DocuSign Developer 로그인
+2. 우측 상단 유저 아이콘 클릭 후 Manage Profile Menu로 이동
+3. My Profile에서 Name 변경
+
+### DocuSign Mail Templete Change
+
+1. DocuSign Developer 로그인
+2. DocuSign Developer Admin 메뉴 이동
+3. DocuSign 좌측 메뉴 바에서 ACCOUNT > Brands 이동
+4. 사용하고자 하는 Brand 제작 후 BrandId 사용
diff --git a/lib/docuSign/jwtConfig/jwtConfig.json b/lib/docuSign/jwtConfig/jwtConfig.json
new file mode 100644
index 00000000..756ca9dd
--- /dev/null
+++ b/lib/docuSign/jwtConfig/jwtConfig.json
@@ -0,0 +1,6 @@
+{
+ "dsJWTClientId": "4ecf089f-9134-4c6c-9657-d8f8c41b5965",
+ "impersonatedUserGuid": "de8ef3a2-9498-4855-a571-249a774a3905",
+ "privateKeyLocation": "./lib/docuSign/jwtConfig/private.key",
+ "dsOauthServer": "https://account-d.docusign.com"
+}
diff --git a/lib/docuSign/jwtConfig/private.key b/lib/docuSign/jwtConfig/private.key
new file mode 100644
index 00000000..73c4291a
--- /dev/null
+++ b/lib/docuSign/jwtConfig/private.key
@@ -0,0 +1,29 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAnnjspqTabuuPLPi9Iga8U/chJRNmyr1PTbJC/Il0jse4ps/C
+KGQdmVOsDzPW//dopMLVc5OmJ7I3y7lw2+TuJ0G7Ip7s6epV2dzqH9aA/yvHDwvj
+2W9ZRH8pNx5AjNDCscwBF3NCK8CoGqK3+ukvuErVK8XQHnzOtAF2uyd2JLodT0fE
+I+uyvIL1E5pzU5zHzxHoWCsrjKAVaHhWUiTP0migFYrMBMVWC30slvhrNg1qc4uT
+Of3rkOGAUK+MFqCbaUm4qKBest9hDgSSw1h8Wv3cKD90KlRgZRSLSRxFwxzhj0ft
+1ip+JIc8dLcax1+xhX0dKBW2GARchojxEAzhDQIDAQABAoIBABvVuyF5JsnhU7xv
+M09Q9g7cg0SfIAi/0DhiNYxke2Xh1D/ukZilHyLRlND1xs+ebhG0jCf5GO/ziIPe
+3mEtWJxqGfvWhOAAUlSKTlBJzc4kKxpsOPj16yzSFhPxmx5ww6XVoqJzEv4a4JwP
+FTg78a8R69f8rpXQT8FD2Y49e+2uwVZVJfCjyaLcS2jh0wfaf7YiztSfyeAZNU2z
+YIL05wDm6Kw8fsdgZ5tF+tEEx0xBelNh+g4fNVVYdQmUhTM0GHePH5KvLc7LQyxD
+z/8ymU5fxikJGFmSS4ncI8ZpmCjV36tkUfZ03n5fW+76Q+gncc+ZKtXRZLgqBdsK
+q9ZDTuECgYEAzXMpmOnZh6Mzw6js5WZ2jSw1vuHjEDBOxpKon9UXZD5wZh9bcuxr
+ARQy+9/UETppumIW8L+zpmrpZISyriywEkleIjQhDqA9HJGR1lSukhMTyt4bj6ER
+f3uyJUzFun5c/QTJEBEJneTFY/Zc4pB+KIdTf3EosVGbtBfkfUXvyyECgYEAxXbA
+lg6gmo7ZpGZuPdhMrGiSI8rmGsvIo8Bw7jqdb6E/ksl5nBIxsLcM2lJw8Qe/fvei
+g+4Zmc5NOzyOKO1L84ekOC6jfvnGR2jzS2hF/qcNLUEEOKEyzBeniWrAqt80fgeK
+cH3zSAXCyLaGJPfdPPqEDYtVBN+zTwNJvHDK5G0CgYEAq1Lcnlpr/vL2iLQGkKno
+NINocjw2OFrAZlEIcvik4AA9hLuja+uAs86fUXDujEtUvYtsq+iArEc9R4hs5Ff5
+n9Y0vHsSEftH2tn9bmkBhmiIOcUL4LMlP1TsUrR5srILYycpb891YIjUni5keL6b
+pbprw7uefneaSw0dieXXOGECgYBrnmsb3WD+m3hWt1TB9A7lsCBlzYFXfVUemhVy
+YRPI8TL6xz+2JdxbGYixvFi9pKFji4dRLAVb5CoHbNt1xs6sLXL9A74rx+mepb5j
+jLMJNPZjgZnRW1maDhJLPJlBB2FOhsGWya47xJgCWCgIIea8AzTRROzTOTA6keov
+/7E0iQKBgFUWjpHIC0wkBFQFAV1uji3P0Bp6/hCOq9hZNxiaS41AlrhrPDRcIqss
+rMrW0Wf0OGDv0+aQXdMkk+nKBjQO3uS6EIj2oDUY/hTFXAKqvDPbHEx3rbtR7NdJ
+Sx9/raUX3YoYSNbPwwKcIWiHVnqY/hI8zIb+RFZgwt+mEoLS9/a2
+-----END RSA PRIVATE KEY-----
+
+
diff --git a/lib/docuSign/types.ts b/lib/docuSign/types.ts
new file mode 100644
index 00000000..450199ce
--- /dev/null
+++ b/lib/docuSign/types.ts
@@ -0,0 +1,37 @@
+export interface ContractInfo {
+ tabLabel: string;
+ value: string;
+}
+
+export interface ContractorInfo {
+ email: string;
+ name: string;
+ roleName: string;
+}
+
+export type poTabLabes =
+ | "po_no"
+ | "vendor_name"
+ | "po_date"
+ | "project_name"
+ | "vendor_location"
+ | "shi_email"
+ | "vendor_email"
+ | "po_desc"
+ | "qty"
+ | "unit_price"
+ | "total"
+ | "grand_total_amount"
+ | "tax_rate"
+ | "tax_total"
+ | "payment_amount"
+ | "remark";
+
+type ContentMap<T extends string> = {
+ [K in T]: {
+ tabLabel: K;
+ value: string;
+ };
+};
+
+export type POContent = ContentMap<poTabLabes>[poTabLabes][];