diff options
Diffstat (limited to 'lib/approval/README.md')
| -rw-r--r-- | lib/approval/README.md | 478 |
1 files changed, 432 insertions, 46 deletions
diff --git a/lib/approval/README.md b/lib/approval/README.md index 7e62a1d7..5b3c1839 100644 --- a/lib/approval/README.md +++ b/lib/approval/README.md @@ -585,68 +585,438 @@ async function replaceTemplateVariables( 결재 문서 미리보기 및 결재선 설정 다이얼로그 컴포넌트입니다. +**위치:** `lib/approval/approval-preview-dialog.tsx` + +**Import:** +```typescript +// ⚠️ 클라이언트 컴포넌트는 반드시 /client에서 import +import { ApprovalPreviewDialog } from '@/lib/approval/client'; +``` + +#### Props 인터페이스 + ```typescript interface ApprovalPreviewDialogProps { + // 기본 제어 open: boolean; onOpenChange: (open: boolean) => void; + + // 템플릿 설정 templateName: string; // DB에서 조회할 템플릿 이름 - variables: Record<string, string>; // 템플릿 변수 - title: string; // 결재 제목 - description?: string; // 결재 설명 + variables: Record<string, string>; // 템플릿 변수 ({{변수명}} 형태로 치환) + title: string; // 결재 제목 (초기값) + + // 사용자 정보 currentUser: { - id: number; - epId: string; - name?: string; - email?: string; - deptName?: string; + id: number; // 사용자 DB ID + epId: string; // Knox EP ID (필수) + name?: string; // 사용자 이름 + email?: string; // 이메일 + deptName?: string; // 부서명 }; + + // 결재선 설정 defaultApprovers?: string[]; // 초기 결재선 (EP ID 배열) + + // 콜백 onConfirm: (data: { - approvers: string[]; - title: string; - description?: string; + approvers: string[]; // 선택된 결재자 EP ID 배열 + title: string; // (수정 가능한) 결재 제목 + attachments?: File[]; // 첨부파일 (enableAttachments가 true일 때) }) => Promise<void>; + + // 옵션 allowTitleEdit?: boolean; // 제목 수정 허용 (기본: true) - allowDescriptionEdit?: boolean; // 설명 수정 허용 (기본: true) + + // 첨부파일 (선택적 기능) + enableAttachments?: boolean; // 첨부파일 UI 활성화 (기본: false) + maxAttachments?: number; // 최대 첨부파일 개수 (기본: 10) + maxFileSize?: number; // 최대 파일 크기 bytes (기본: 100MB) } ``` -**주요 기능:** -- 템플릿 실시간 미리보기 (변수 자동 치환) -- 결재선 선택 UI (ApprovalLineSelector 통합) -- 제목/설명 수정 -- 반응형 디자인 (Desktop: Dialog, Mobile: Drawer) -- 로딩 상태 자동 처리 +#### 주요 기능 + +- ✅ 템플릿 실시간 미리보기 (변수 자동 치환) +- ✅ 결재선 선택 UI (ApprovalLineSelector 통합) +- ✅ 결재 제목 수정 가능 +- ✅ **선택적 첨부파일 업로드** (드래그 앤 드롭) +- ✅ 반응형 디자인 (Desktop: Dialog, Mobile: Drawer) +- ✅ 로딩 상태 자동 처리 +- ✅ 유효성 검사 및 에러 핸들링 -**사용 예시:** +#### 사용 예시 1: 기본 사용 (첨부파일 없이) ```typescript -// ⚠️ 클라이언트 컴포넌트는 반드시 /client에서 import +'use client'; + +import { useState } from 'react'; import { ApprovalPreviewDialog } from '@/lib/approval/client'; +import { useSession } from 'next-auth/react'; +import { toast } from 'sonner'; -<ApprovalPreviewDialog - open={showPreview} - onOpenChange={setShowPreview} - templateName="벤더 가입 승인 요청" - variables={{ - '업체명': 'ABC 협력업체', - '담당자': '홍길동', - '요청일': '2024-11-06', - }} - title="협력업체 가입 승인" - description="ABC 협력업체의 가입을 승인합니다." - currentUser={{ - id: 1, - epId: 'EP001', - name: '김철수', - email: 'kim@example.com', - }} - onConfirm={async ({ approvers, title, description }) => { - await submitApproval(approvers); - }} -/> +export function VendorApprovalButton({ vendorData }) { + const { data: session } = useSession(); + const [showPreview, setShowPreview] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleOpenPreview = async () => { + // 1. 템플릿 변수 준비 + const variables = { + '업체명': vendorData.companyName, + '사업자번호': vendorData.businessNumber, + '담당자': vendorData.contactPerson, + '요청일': new Date().toLocaleDateString('ko-KR'), + '요청사유': vendorData.reason, + }; + + // 2. 미리보기 열기 + setShowPreview(true); + }; + + const handleConfirm = async ({ approvers, title, attachments }) => { + try { + setIsSubmitting(true); + + // 3. 실제 결재 상신 API 호출 + const result = await fetch('/api/vendor/approval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + vendorId: vendorData.id, + approvers, + title, + }), + }); + + if (result.ok) { + toast.success('결재가 성공적으로 상신되었습니다.'); + setShowPreview(false); + } + } catch (error) { + toast.error('결재 상신에 실패했습니다.'); + } finally { + setIsSubmitting(false); + } + }; + + // EP ID 없으면 버튼 비활성화 + if (!session?.user?.epId) { + return ( + <Button disabled> + 결재 요청 (Knox EP ID 필요) + </Button> + ); + } + + return ( + <> + <Button onClick={handleOpenPreview}> + 정규업체 등록 결재 요청 + </Button> + + {showPreview && ( + <ApprovalPreviewDialog + open={showPreview} + onOpenChange={setShowPreview} + templateName="정규업체 등록" + variables={{ + '업체명': vendorData.companyName, + '사업자번호': vendorData.businessNumber, + '담당자': vendorData.contactPerson, + '요청일': new Date().toLocaleDateString('ko-KR'), + }} + title={`정규업체 등록 - ${vendorData.companyName}`} + currentUser={{ + id: Number(session.user.id), + epId: session.user.epId, + name: session.user.name || undefined, + email: session.user.email || undefined, + }} + onConfirm={handleConfirm} + /> + )} + </> + ); +} +``` + +#### 사용 예시 2: 첨부파일 포함 + +```typescript +'use client'; + +import { useState } from 'react'; +import { ApprovalPreviewDialog } from '@/lib/approval/client'; +import { submitApproval } from '@/lib/knox-api/approval/approval'; + +export function ContractApprovalButton({ contractData }) { + const { data: session } = useSession(); + const [showPreview, setShowPreview] = useState(false); + + const handleConfirm = async ({ approvers, title, attachments }) => { + console.log('결재자:', approvers.length, '명'); + console.log('첨부파일:', attachments?.length || 0, '개'); + + // Knox API로 결재 상신 (첨부파일 포함) + const approvalData = { + // 기본 정보 + subject: title, + contents: `<div>계약서 검토를 요청합니다.</div>`, + contentsType: 'HTML', + + // 결재선 설정 + aplns: approvers.map((epId, index) => ({ + epId: epId, // Knox EP ID + seq: index.toString(), // 결재 순서 (0부터 시작) + role: '1', // 역할: 0=기안, 1=결재, 2=합의, 3=참조 + aplnStatsCode: '0', // 결재 상태: 0=대기 + arbPmtYn: 'Y', // 임의승인 허용 + contentsMdfyPmtYn: 'Y', // 내용수정 허용 + aplnMdfyPmtYn: 'Y', // 결재선수정 허용 + opinion: '', // 의견 + })), + + // 첨부파일 + attachments: attachments || [], // File[] 배열 + + // 문서 설정 + docSecuType: 'PERSONAL', // 보안등급: PERSONAL, COMPANY, SECRET + notifyOption: '0', // 알림옵션: 0=전체, 1=결재완료, 2=없음 + urgYn: 'N', // 긴급여부 + docMngSaveCode: '0', // 문서관리저장코드 + sbmLang: 'ko', // 언어 + }; + + // Knox API 호출 + const result = await submitApproval(approvalData, { + userId: session.user.id.toString(), + epId: session.user.epId!, + emailAddress: session.user.email || '', + }); + + if (result.success) { + toast.success(`결재가 상신되었습니다. (ID: ${result.approvalId})`); + } + }; + + if (!session?.user?.epId) return null; + + return ( + <> + <Button onClick={() => setShowPreview(true)}> + 계약서 검토 결재 요청 + </Button> + + <ApprovalPreviewDialog + open={showPreview} + onOpenChange={setShowPreview} + templateName="계약서 검토" + variables={{ + '계약명': contractData.title, + '계약일': contractData.date, + '계약업체': contractData.vendor, + '계약금액': contractData.amount.toLocaleString(), + }} + title={`계약서 검토 - ${contractData.title}`} + currentUser={{ + id: Number(session.user.id), + epId: session.user.epId, + name: session.user.name || undefined, + email: session.user.email || undefined, + }} + onConfirm={handleConfirm} + // 첨부파일 기능 활성화 + enableAttachments={true} + maxAttachments={5} + maxFileSize={50 * 1024 * 1024} // 50MB + /> + </> + ); +} +``` + +#### Knox API 연동: approvalData 객체 상세 + +Knox 결재 API (`submitApproval`)에 전달하는 전체 객체 구조: + +```typescript +interface KnoxApprovalData { + // ===== 기본 정보 ===== + subject: string; // 결재 제목 (필수) + contents: string; // 결재 내용 HTML (필수) + contentsType: 'HTML' | 'TEXT'; // 내용 타입 (기본: 'HTML') + + // ===== 결재선 설정 ===== + aplns: Array<{ + epId: string; // Knox EP ID (필수) + seq: string; // 결재 순서 "0", "1", "2"... (필수) + role: string; // 역할 (필수) + // "0" = 기안자 + // "1" = 결재자 + // "2" = 합의자 + // "3" = 참조자 + aplnStatsCode: string; // 결재 상태 (필수) + // "0" = 대기 + // "1" = 진행중 + // "2" = 승인 + // "3" = 반려 + arbPmtYn: 'Y' | 'N'; // 임의승인 허용 (기본: 'Y') + contentsMdfyPmtYn: 'Y' | 'N'; // 내용수정 허용 (기본: 'Y') + aplnMdfyPmtYn: 'Y' | 'N'; // 결재선수정 허용 (기본: 'Y') + opinion?: string; // 결재 의견 (선택) + }>; + + // ===== 첨부파일 ===== + attachments?: File[]; // File 객체 배열 (선택) + + // ===== 문서 설정 ===== + docSecuType?: string; // 보안등급 (기본: 'PERSONAL') + // 'PERSONAL' = 개인 + // 'COMPANY' = 회사 + // 'SECRET' = 비밀 + notifyOption?: string; // 알림옵션 (기본: '0') + // '0' = 전체 알림 + // '1' = 결재완료 알림 + // '2' = 알림 없음 + urgYn?: 'Y' | 'N'; // 긴급여부 (기본: 'N') + docMngSaveCode?: string; // 문서관리저장코드 (기본: '0') + // '0' = 개인문서함 + // '1' = 부서문서함 + sbmLang?: 'ko' | 'en'; // 언어 (기본: 'ko') + + // ===== 추가 옵션 ===== + comment?: string; // 기안 의견 (선택) + refLineYn?: 'Y' | 'N'; // 참조선 사용여부 (기본: 'N') + agreementLineYn?: 'Y' | 'N'; // 합의선 사용여부 (기본: 'N') +} + +// 사용자 정보 +interface KnoxUserInfo { + userId: string; // 사용자 ID (필수) + epId: string; // Knox EP ID (필수) + emailAddress: string; // 이메일 (필수) +} +``` + +#### 전체 연동 예시 (Server Action) + +```typescript +// app/api/vendor/approval/route.ts +'use server'; + +import { ApprovalSubmissionSaga } from '@/lib/approval'; +import { mapVendorToTemplateVariables } from '@/lib/vendors/handlers'; + +export async function submitVendorApproval(data: { + vendorId: number; + approvers: string[]; + title: string; + attachments?: File[]; +}) { + // 1. 템플릿 변수 매핑 + const vendor = await getVendorById(data.vendorId); + const variables = await mapVendorToTemplateVariables(vendor); + + // 2. Saga로 결재 상신 + const saga = new ApprovalSubmissionSaga( + 'vendor_registration', // 핸들러 타입 + { + vendorId: data.vendorId, + // 필요한 payload 데이터 + }, + { + title: data.title, + templateName: '정규업체 등록', + variables, + approvers: data.approvers, // EP ID 배열 + currentUser: { + id: user.id, + epId: user.epId, + email: user.email, + }, + } + ); + + return await saga.execute(); +} ``` +#### 첨부파일 처리 + +첨부파일은 Knox API 내부에서 자동으로 FormData로 변환되어 전송됩니다: + +```typescript +// lib/knox-api/approval/approval.ts 내부 (참고용) +export async function submitApproval( + approvalData: KnoxApprovalData, + userInfo: KnoxUserInfo +) { + const formData = new FormData(); + + // 기본 데이터를 JSON으로 직렬화 + formData.append('data', JSON.stringify({ + subject: approvalData.subject, + contents: approvalData.contents, + aplns: approvalData.aplns, + // ... 기타 필드 + })); + + // 첨부파일 추가 + if (approvalData.attachments) { + approvalData.attachments.forEach((file, index) => { + formData.append(`attachment_${index}`, file); + }); + } + + // Knox API 호출 + const response = await fetch(KNOX_APPROVAL_API_URL, { + method: 'POST', + headers: { + 'X-User-Id': userInfo.userId, + 'X-EP-Id': userInfo.epId, + }, + body: formData, + }); + + return response.json(); +} +``` + +#### 주의사항 + +1. **EP ID 필수 체크** + ```typescript + if (!session?.user?.epId) { + return <Button disabled>Knox EP ID 필요</Button>; + } + ``` + +2. **onConfirm 에러 핸들링** + ```typescript + const handleConfirm = async ({ approvers, title, attachments }) => { + try { + await submitApproval(...); + toast.success('성공'); + } catch (error) { + toast.error('실패'); + // ⚠️ throw하지 않으면 다이얼로그가 자동으로 닫힘 + // 에러 시 다이얼로그를 열어두려면 throw 필요 + throw error; + } + }; + ``` + +3. **첨부파일 제한** + - 기본값: 최대 10개, 파일당 100MB + - `maxAttachments`, `maxFileSize`로 커스터마이징 가능 + - 중복 파일 자동 필터링 + +4. **반응형 UI** + - Desktop (≥768px): Dialog 형태 + - Mobile (<768px): Drawer 형태 + - 자동으로 화면 크기에 맞게 렌더링 + ### 캐시 관리 ```typescript @@ -670,9 +1040,9 @@ async function revalidateAllApprovalCaches(): Promise<void> ``` lib/approval/ ├── approval-saga.ts # Saga 클래스 (메인 로직) [서버] -│ ├── ApprovalSubmissionSaga # 결재 상신 -│ ├── ApprovalExecutionSaga # 액션 실행 -│ └── ApprovalRejectionSaga # 반려 처리 +│ ├── ApprovalSubmissionSaga # 결재 상신 (7단계) +│ ├── ApprovalExecutionSaga # 액션 실행 (7단계) +│ └── ApprovalRejectionSaga # 반려 처리 (4단계) │ ├── approval-workflow.ts # 핸들러 레지스트리 [서버] │ ├── registerActionHandler() @@ -695,7 +1065,7 @@ lib/approval/ │ └── htmlDescriptionList() │ ├── approval-preview-dialog.tsx # 결재 미리보기 다이얼로그 [클라이언트] -│ └── ApprovalPreviewDialog # 템플릿 미리보기 + 결재선 설정 +│ └── ApprovalPreviewDialog # 템플릿 미리보기 + 결재선 설정 + 첨부파일 │ ├── cache-utils.ts # 캐시 관리 [서버] │ ├── revalidateApprovalLogs() @@ -709,8 +1079,13 @@ lib/approval/ │ ├── index.ts # 서버 전용 API Export ├── client.ts # 클라이언트 컴포넌트 Export ⚠️ +│ └── ApprovalPreviewDialog # ← 클라이언트에서는 이 파일에서 import │ -└── README.md # 이 문서 +├── README.md # 이 문서 (전체 시스템 가이드) +├── SAGA_PATTERN.md # Saga 패턴 상세 설명 +├── CRONJOB_CONTEXT_FIX.md # Request Context 문제 해결 가이드 +├── USAGE_PATTERN_ANALYSIS.md # 실제 사용 패턴 분석 +└── ARCHITECTURE_REVIEW.md # 아키텍처 평가 ``` ### Import 경로 가이드 @@ -1371,6 +1746,17 @@ setShowPreview(true); ## 📝 변경 이력 +### 2024-11-07 - ApprovalPreviewDialog 컴포넌트 통합 및 첨부파일 기능 추가 +- ✅ 중복 컴포넌트 통합: `components/approval/ApprovalPreviewDialog.tsx` → `lib/approval/approval-preview-dialog.tsx`로 일원화 +- ✅ 선택적 첨부파일 업로드 기능 추가 (`enableAttachments`, `maxAttachments`, `maxFileSize` props) +- ✅ 파일 드래그 앤 드롭 UI 구현 (Dropzone, FileList 컴포넌트 활용) +- ✅ 첨부파일 유효성 검사 (크기 제한, 개수 제한, 중복 파일 필터링) +- ✅ API 인터페이스 개선: `onSubmit` → `onConfirm`으로 변경 +- ✅ 콜백 시그니처 변경: `(approvers: ApprovalLineItem[])` → `({ approvers: string[], title: string, attachments?: File[] })` +- ✅ Knox API 연동 상세 가이드 추가 (approvalData 객체 전체 필드 문서화) +- ✅ 기존 사용처 업데이트 (vendor-regular-registrations, pq-review-table-new) +- ✅ README 상세화: 사용 예시, Props 인터페이스, 주의사항 등 보완 + ### 2024-11-06 - Request Context 호환성 개선 (RFQ 발송) - ✅ Cronjob 환경에서 Request Context 오류 해결 - ✅ `headers()`, `getServerSession()` 호출 문제 수정 |
