diff options
Diffstat (limited to 'lib/approval')
| -rw-r--r-- | lib/approval/approval-preview-dialog.tsx | 435 | ||||
| -rw-r--r-- | lib/approval/client.ts | 23 | ||||
| -rw-r--r-- | lib/approval/index.ts | 3 | ||||
| -rw-r--r-- | lib/approval/template-utils.ts | 30 | ||||
| -rw-r--r-- | lib/approval/templates/벤더 가입 승인 요청.html | 233 | ||||
| -rw-r--r-- | lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html | 316 |
6 files changed, 1025 insertions, 15 deletions
diff --git a/lib/approval/approval-preview-dialog.tsx b/lib/approval/approval-preview-dialog.tsx new file mode 100644 index 00000000..bc5a4f65 --- /dev/null +++ b/lib/approval/approval-preview-dialog.tsx @@ -0,0 +1,435 @@ +"use client"; + +import * as React from "react"; +import { Loader2, Eye, Send, X } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, +} from "@/components/ui/drawer"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useMediaQuery } from "@/hooks/use-media-query"; + +import { ApprovalLineSelector } from "@/components/knox/approval/ApprovalLineSelector"; +import { + getApprovalTemplateByName, + replaceTemplateVariables +} from "./template-utils"; + +import type { ApprovalLine } from "@/components/knox/approval/types"; + +/** + * 결재 미리보기 다이얼로그 Props + */ +export interface ApprovalPreviewDialogProps { + /** 다이얼로그 열림 상태 */ + open: boolean; + /** 다이얼로그 열림 상태 변경 핸들러 */ + onOpenChange: (open: boolean) => void; + /** 템플릿 이름 (DB에서 조회) */ + templateName: string; + /** 템플릿 변수 ({{변수명}} 형태로 치환) */ + variables: Record<string, string>; + /** 결재 제목 */ + title: string; + /** 결재 설명 (선택사항) */ + description?: string; + /** 현재 사용자 정보 */ + currentUser: { + id: number; + epId: string; + name?: string; + email?: string; + deptName?: string; + }; + /** 초기 결재선 (선택사항) */ + defaultApprovers?: string[]; + /** 확인 버튼 클릭 시 콜백 */ + onConfirm: (data: { + approvers: string[]; + title: string; + description?: string; + }) => Promise<void>; + /** 제목 수정 가능 여부 (기본: true) */ + allowTitleEdit?: boolean; + /** 설명 수정 가능 여부 (기본: true) */ + allowDescriptionEdit?: boolean; +} + +/** + * 결재 미리보기 다이얼로그 컴포넌트 + * + * **주요 기능:** + * 1. 템플릿 실시간 미리보기 (변수 치환) + * 2. 결재선 선택 (ApprovalLineSelector 활용) + * 3. 제목/설명 수정 + * 4. 반응형 UI (Desktop: Dialog, Mobile: Drawer) + * + * **사용 예시:** + * ```tsx + * <ApprovalPreviewDialog + * open={isOpen} + * onOpenChange={setIsOpen} + * templateName="벤더 가입 승인 요청" + * variables={{ "업체명": "ABC 협력업체" }} + * title="협력업체 가입 승인" + * currentUser={{ id: 1, epId: "EP001", name: "홍길동" }} + * onConfirm={async ({ approvers }) => { + * await submitApproval(approvers); + * }} + * /> + * ``` + */ +export function ApprovalPreviewDialog({ + open, + onOpenChange, + templateName, + variables, + title: initialTitle, + description: initialDescription, + currentUser, + defaultApprovers = [], + onConfirm, + allowTitleEdit = true, + allowDescriptionEdit = true, +}: ApprovalPreviewDialogProps) { + const isDesktop = useMediaQuery("(min-width: 768px)"); + + // 로딩 상태 + const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false); + const [isSubmitting, setIsSubmitting] = React.useState(false); + + // 폼 상태 + const [title, setTitle] = React.useState(initialTitle); + const [description, setDescription] = React.useState(initialDescription || ""); + const [approvalLines, setApprovalLines] = React.useState<ApprovalLine[]>([]); + const [previewHtml, setPreviewHtml] = React.useState<string>(""); + + // 템플릿 로딩 및 미리보기 생성 + React.useEffect(() => { + if (!open) return; + + async function loadTemplatePreview() { + try { + setIsLoadingTemplate(true); + + // 1. 템플릿 조회 + const template = await getApprovalTemplateByName(templateName); + if (!template) { + toast.error(`템플릿을 찾을 수 없습니다: ${templateName}`); + return; + } + + // 2. 변수 치환 + const renderedHtml = await replaceTemplateVariables( + template.content || "", + variables + ); + + setPreviewHtml(renderedHtml); + } catch (error) { + console.error("[ApprovalPreviewDialog] 템플릿 로딩 실패:", error); + toast.error("템플릿을 불러오는 중 오류가 발생했습니다."); + } finally { + setIsLoadingTemplate(false); + } + } + + loadTemplatePreview(); + }, [open, templateName, variables]); + + // 초기 결재선 설정 + React.useEffect(() => { + if (!open) return; + + // 상신자 추가 + const submitter: ApprovalLine = { + id: `submitter-${currentUser.id}`, + epId: currentUser.epId, + userId: currentUser.id.toString(), + emailAddress: currentUser.email || "", + name: currentUser.name || "상신자", + deptName: currentUser.deptName, + role: "0", // 상신자 + seq: "0", + opinion: "", + }; + + // 기본 결재자들 추가 (있는 경우) + const defaultLines: ApprovalLine[] = defaultApprovers.map((epId, index) => ({ + id: `approver-${index}`, + epId: epId, + userId: "", // EP ID로만 식별 + emailAddress: "", + name: `결재자 ${index + 1}`, + role: "1", // 결재 + seq: (index + 1).toString(), + opinion: "", + })); + + setApprovalLines([submitter, ...defaultLines]); + }, [open, currentUser, defaultApprovers]); + + // 결재선 변경 핸들러 + const handleApprovalLinesChange = (lines: ApprovalLine[]) => { + setApprovalLines(lines); + }; + + // 제출 핸들러 + const handleSubmit = async () => { + try { + // 검증: 결재선 확인 + const approvers = approvalLines + .filter((line) => line.role === "1" && line.seq !== "0") + .sort((a, b) => parseInt(a.seq) - parseInt(b.seq)); + + if (approvers.length === 0) { + toast.error("최소 1명의 결재자를 선택해주세요."); + return; + } + + // 검증: 제목 확인 + if (!title.trim()) { + toast.error("결재 제목을 입력해주세요."); + return; + } + + setIsSubmitting(true); + + // EP ID 목록 추출 + const approverEpIds = approvers + .map((line) => line.epId) + .filter((epId): epId is string => !!epId); + + // 상위 컴포넌트로 데이터 전달 + await onConfirm({ + approvers: approverEpIds, + title: title.trim(), + description: description.trim() || undefined, + }); + + // 성공 시 다이얼로그 닫기 + onOpenChange(false); + } catch (error) { + console.error("[ApprovalPreviewDialog] 제출 실패:", error); + // 에러는 상위 컴포넌트에서 처리 (toast 등) + } finally { + setIsSubmitting(false); + } + }; + + // 취소 핸들러 + const handleCancel = () => { + onOpenChange(false); + }; + + // 폼 내용 + const FormContent = () => ( + <div className="space-y-6"> + {/* 탭: 미리보기 / 결재선 설정 */} + <Tabs defaultValue="preview" className="w-full"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="preview" className="gap-2"> + <Eye className="size-4" /> + 미리보기 + </TabsTrigger> + <TabsTrigger value="approvers" className="gap-2"> + <Send className="size-4" /> + 결재선 설정 + </TabsTrigger> + </TabsList> + + {/* 미리보기 탭 */} + <TabsContent value="preview" className="space-y-4 mt-4"> + {/* 제목 입력 */} + <div className="space-y-2"> + <Label htmlFor="title">결재 제목</Label> + <Input + id="title" + value={title} + onChange={(e) => setTitle(e.target.value)} + placeholder="결재 제목을 입력하세요" + disabled={!allowTitleEdit || isSubmitting} + /> + </div> + + {/* 설명 입력 */} + <div className="space-y-2"> + <Label htmlFor="description">결재 설명 (선택사항)</Label> + <Textarea + id="description" + value={description} + onChange={(e) => setDescription(e.target.value)} + placeholder="결재 설명을 입력하세요" + rows={3} + disabled={!allowDescriptionEdit || isSubmitting} + /> + </div> + + {/* 템플릿 미리보기 */} + <div className="space-y-2"> + <Label>문서 미리보기</Label> + <ScrollArea className="h-[300px] w-full rounded-md border bg-gray-50 p-4"> + {isLoadingTemplate ? ( + <div className="flex items-center justify-center h-full"> + <Loader2 className="size-6 animate-spin text-muted-foreground" /> + <span className="ml-2 text-sm text-muted-foreground"> + 템플릿을 불러오는 중... + </span> + </div> + ) : ( + <div + className="prose prose-sm max-w-none" + dangerouslySetInnerHTML={{ __html: previewHtml }} + /> + )} + </ScrollArea> + </div> + </TabsContent> + + {/* 결재선 설정 탭 */} + <TabsContent value="approvers" className="space-y-4 mt-4"> + <div className="space-y-2"> + <Label>결재선</Label> + <p className="text-sm text-muted-foreground"> + 결재자를 검색하여 추가하고, 결재 순서를 설정하세요. + </p> + </div> + + <ApprovalLineSelector + value={approvalLines} + onChange={handleApprovalLinesChange} + placeholder="결재자를 검색하세요..." + maxSelections={10} + domainFilter={{ type: "exclude", domains: ["partners"] }} + /> + + {/* 결재선 요약 */} + <div className="rounded-md bg-blue-50 p-3 border border-blue-200"> + <p className="text-sm font-medium text-blue-900 mb-1"> + 📋 결재 경로 + </p> + <p className="text-sm text-blue-700"> + {approvalLines + .filter((line) => line.seq !== "0") + .sort((a, b) => parseInt(a.seq) - parseInt(b.seq)) + .map((line) => line.name) + .join(" → ") || "결재자를 선택해주세요"} + </p> + </div> + </TabsContent> + </Tabs> + </div> + ); + + // Desktop: Dialog + if (isDesktop) { + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col"> + <DialogHeader> + <DialogTitle>결재 미리보기</DialogTitle> + <DialogDescription> + 결재 문서를 확인하고 결재선을 설정한 후 상신하세요. + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-y-auto"> + <FormContent /> + </div> + + <DialogFooter className="gap-2 sm:space-x-0"> + <Button + variant="outline" + onClick={handleCancel} + disabled={isSubmitting} + > + <X className="size-4 mr-2" /> + 취소 + </Button> + <Button + onClick={handleSubmit} + disabled={isSubmitting || isLoadingTemplate} + > + {isSubmitting ? ( + <> + <Loader2 className="size-4 mr-2 animate-spin" /> + 상신 중... + </> + ) : ( + <> + <Send className="size-4 mr-2" /> + 결재 상신 + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); + } + + // Mobile: Drawer + return ( + <Drawer open={open} onOpenChange={onOpenChange}> + <DrawerContent className="max-h-[90vh]"> + <DrawerHeader> + <DrawerTitle>결재 미리보기</DrawerTitle> + <DrawerDescription> + 결재 문서를 확인하고 결재선을 설정한 후 상신하세요. + </DrawerDescription> + </DrawerHeader> + + <div className="px-4 overflow-y-auto"> + <FormContent /> + </div> + + <DrawerFooter className="gap-2"> + <Button + variant="outline" + onClick={handleCancel} + disabled={isSubmitting} + > + <X className="size-4 mr-2" /> + 취소 + </Button> + <Button + onClick={handleSubmit} + disabled={isSubmitting || isLoadingTemplate} + > + {isSubmitting ? ( + <> + <Loader2 className="size-4 mr-2 animate-spin" /> + 상신 중... + </> + ) : ( + <> + <Send className="size-4 mr-2" /> + 결재 상신 + </> + )} + </Button> + </DrawerFooter> + </DrawerContent> + </Drawer> + ); +} + diff --git a/lib/approval/client.ts b/lib/approval/client.ts new file mode 100644 index 00000000..4f62ddfc --- /dev/null +++ b/lib/approval/client.ts @@ -0,0 +1,23 @@ +/** + * 결재 시스템 - 클라이언트 컴포넌트 Export + * + * ⚠️ 중요: 이 파일은 클라이언트 컴포넌트만 export합니다. + * + * 서버 전용 함수(Saga, 템플릿 유틸 등)와 분리하여 + * 클라이언트 번들에 서버 전용 코드가 포함되는 것을 방지합니다. + * + * 사용법: + * ```typescript + * // ✅ 올바른 방법 + * import { ApprovalPreviewDialog } from '@/lib/approval/client'; + * + * // ❌ 잘못된 방법 (서버 코드가 클라이언트 번들에 포함됨) + * import { ApprovalPreviewDialog } from '@/lib/approval'; + * ``` + */ + +export { + ApprovalPreviewDialog, + type ApprovalPreviewDialogProps, +} from './approval-preview-dialog'; + diff --git a/lib/approval/index.ts b/lib/approval/index.ts index 943ada81..a2750805 100644 --- a/lib/approval/index.ts +++ b/lib/approval/index.ts @@ -55,3 +55,6 @@ export { revalidateAllApprovalCaches, revalidateApprovalDetail, } from './cache-utils'; + +// ⚠️ 주의: 클라이언트 컴포넌트는 '@/lib/approval/client'에서 import 하세요 +// export { ApprovalPreviewDialog } from './approval-preview-dialog'; // 제거됨
\ No newline at end of file diff --git a/lib/approval/template-utils.ts b/lib/approval/template-utils.ts index a39f8ac4..0607f289 100644 --- a/lib/approval/template-utils.ts +++ b/lib/approval/template-utils.ts @@ -108,13 +108,13 @@ export async function htmlTableConverter( columns: Array<{ key: string; label: string }> ): Promise<string> { if (!data || data.length === 0) { - return '<p class="text-gray-500">데이터가 없습니다.</p>'; + return '<p style="color: #6b7280;">데이터가 없습니다.</p>'; } const headerRow = columns .map( (col) => - `<th class="border border-gray-300 px-4 py-2 bg-gray-100 font-semibold text-left">${col.label}</th>` + `<th style="border: 1px solid #d1d5db; padding: 8px 16px; background-color: #f3f4f6; font-weight: 600; text-align: left;">${col.label}</th>` ) .join(''); @@ -125,7 +125,7 @@ export async function htmlTableConverter( const value = row[col.key]; const displayValue = value !== undefined && value !== null ? String(value) : '-'; - return `<td class="border border-gray-300 px-4 py-2">${displayValue}</td>`; + return `<td style="border: 1px solid #d1d5db; padding: 8px 16px;">${displayValue}</td>`; }) .join(''); return `<tr>${cells}</tr>`; @@ -133,7 +133,7 @@ export async function htmlTableConverter( .join(''); return ` - <table class="w-full border-collapse border border-gray-300 my-4"> + <table style="width: 100%; border-collapse: collapse; border: 1px solid #d1d5db; margin: 16px 0;"> <thead> <tr>${headerRow}</tr> </thead> @@ -162,19 +162,19 @@ export async function htmlListConverter( ordered: boolean = false ): Promise<string> { if (!items || items.length === 0) { - return '<p class="text-gray-500">항목이 없습니다.</p>'; + return '<p style="color: #6b7280;">항목이 없습니다.</p>'; } const listItems = items - .map((item) => `<li class="mb-1">${item}</li>`) + .map((item) => `<li style="margin-bottom: 4px;">${item}</li>`) .join(''); const tag = ordered ? 'ol' : 'ul'; - const listClass = ordered - ? 'list-decimal list-inside my-4' - : 'list-disc list-inside my-4'; + const listStyle = ordered + ? 'list-style-type: decimal; list-style-position: inside; margin: 16px 0; padding-left: 20px;' + : 'list-style-type: disc; list-style-position: inside; margin: 16px 0; padding-left: 20px;'; - return `<${tag} class="${listClass}">${listItems}</${tag}>`; + return `<${tag} style="${listStyle}">${listItems}</${tag}>`; } /** @@ -196,20 +196,20 @@ export async function htmlDescriptionList( items: Array<{ label: string; value: string }> ): Promise<string> { if (!items || items.length === 0) { - return '<p class="text-gray-500">정보가 없습니다.</p>'; + return '<p style="color: #6b7280;">정보가 없습니다.</p>'; } const listItems = items .map( (item) => ` - <div class="flex border-b border-gray-200 py-2"> - <dt class="w-1/3 font-semibold text-gray-700">${item.label}</dt> - <dd class="w-2/3 text-gray-900">${item.value}</dd> + <div style="display: flex; border-bottom: 1px solid #e5e7eb; padding: 8px 0;"> + <dt style="width: 33.333%; font-weight: 600; color: #374151;">${item.label}</dt> + <dd style="width: 66.667%; color: #111827;">${item.value}</dd> </div> ` ) .join(''); - return `<dl class="my-4">${listItems}</dl>`; + return `<dl style="margin: 16px 0;">${listItems}</dl>`; } diff --git a/lib/approval/templates/벤더 가입 승인 요청.html b/lib/approval/templates/벤더 가입 승인 요청.html new file mode 100644 index 00000000..f3e216aa --- /dev/null +++ b/lib/approval/templates/벤더 가입 승인 요청.html @@ -0,0 +1,233 @@ +<!-- 벤더 가입 승인 요청 템플릿 --> +<!-- 이 템플릿은 approvalTemplates 테이블에 저장됩니다 --> +<!-- 템플릿명: 벤더 가입 승인 요청 --> + +<div + style=" + max-width: 900px; + margin: 0 auto; + font-family: 'Segoe UI', 'Malgun Gothic', sans-serif; + color: #333; + line-height: 1.6; + " +> + <!-- 헤더 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 2px solid #000; + " + > + <thead> + <tr> + <th + colspan="3" + style=" + background-color: #fff; + color: #000; + padding: 20px; + text-align: center; + font-size: 24px; + font-weight: 700; + " + > + 벤더 가입 승인 요청 + </th> + </tr> + </thead> + </table> + + <!-- 요청 정보 테이블 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 1px solid #666; + " + > + <thead> + <tr> + <th + colspan="2" + style=" + background-color: #333; + color: #fff; + padding: 12px; + text-align: left; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #666; + " + > + 요청 정보 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + width: 150px; + border: 1px solid #ccc; + " + > + 요청일 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{요청일}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + 요청자 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{요청자}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + 승인 대상 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + {{업체수}}개 업체 + </td> + </tr> + </tbody> + </table> + + <!-- 승인 대상 업체 목록 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 1px solid #666; + " + > + <thead> + <tr> + <th + style=" + background-color: #333; + color: #fff; + padding: 12px; + text-align: left; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #666; + " + > + 승인 대상 업체 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style=" + background-color: #fff; + color: #333; + padding: 15px; + border: 1px solid #ccc; + line-height: 1.8; + " + > + {{업체목록}} + </td> + </tr> + <tr> + <td style="background-color: #fff; padding: 0; border: 1px solid #ccc"> + {{업체목록테이블}} + </td> + </tr> + </tbody> + </table> + + <!-- 업체 상세 정보 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 1px solid #666; + " + > + <thead> + <tr> + <th + style=" + background-color: #333; + color: #fff; + padding: 12px; + text-align: left; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #666; + " + > + 업체 상세 정보 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style=" + background-color: #fff; + color: #333; + padding: 20px; + border: 1px solid #ccc; + " + > + {{업체상세정보}} + </td> + </tr> + </tbody> + </table> +</div> diff --git a/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html b/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html new file mode 100644 index 00000000..e2a7cf6d --- /dev/null +++ b/lib/approval/templates/실사의뢰 및 실사재의뢰 요청.html @@ -0,0 +1,316 @@ +<div + style=" + max-width: 900px; + margin: 0 auto; + font-family: 'Segoe UI', 'Malgun Gothic', sans-serif; + color: #333; + line-height: 1.6; + " +> + <!-- 헤더 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 2px solid #000; + " + > + <thead> + <tr> + <th + colspan="3" + style=" + background-color: #fff; + color: #000; + padding: 20px; + text-align: center; + font-size: 24px; + font-weight: 700; + " + > + VENDOR 실사의뢰 (재의뢰) + </th> + </tr> + </thead> + </table> + + <!-- 실사요청 개요 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 1px solid #666; + " + > + <thead> + <tr> + <th + colspan="2" + style=" + background-color: #333; + color: #fff; + padding: 12px; + text-align: left; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #666; + " + > + ■ 실사요청 개요 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + width: 150px; + border: 1px solid #ccc; + " + > + 협력사명 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{협력사명}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + 실사요청일 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{실사요청일}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + 실사예정일 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{실사예정일}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + 실사장소 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{실사장소}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + QM담당자 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{QM담당자}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + 실사사유목적 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + {{실사사유목적}} + </td> + </tr> + <tr> + <td + style=" + background-color: #f5f5f5; + color: #333; + padding: 12px; + font-weight: 600; + border: 1px solid #ccc; + " + > + ISO 9000S 보유여부 + </td> + <td + style=" + background-color: #fff; + color: #000; + padding: 12px; + border: 1px solid #ccc; + " + > + 비워둠 + </td> + </tr> + </tbody> + </table> + + <!-- 담당자 연락처 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 1px solid #666; + " + > + <thead> + <tr> + <th + style=" + background-color: #333; + color: #fff; + padding: 12px; + text-align: left; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #666; + " + > + ■ 담당자 연락처 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style=" + background-color: #fff; + color: #333; + padding: 15px; + border: 1px solid #ccc; + line-height: 1.8; + " + > + {{담당자연락처}} + </td> + </tr> + </tbody> + </table> + + <!-- 실사품목 --> + <table + style=" + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + border: 1px solid #666; + " + > + <thead> + <tr> + <th + style=" + background-color: #333; + color: #fff; + padding: 12px; + text-align: left; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #666; + " + > + ■ 실사품목 + </th> + </tr> + </thead> + <tbody> + <tr> + <td + style=" + background-color: #fff; + color: #333; + padding: 15px; + border: 1px solid #ccc; + line-height: 1.8; + " + > + 해당 정보 없음... + </td> + </tr> + </tbody> + </table> +</div> |
