/**
 * 리포트 관리 컨트롤러
 */

import { Request, Response, NextFunction } from "express";
import reportService from "../services/reportService";
import {
  CreateReportRequest,
  UpdateReportRequest,
  SaveLayoutRequest,
  CreateTemplateRequest,
} from "../types/report";
import path from "path";
import fs from "fs";
import {
  Document,
  Packer,
  Paragraph,
  TextRun,
  ImageRun,
  Table,
  TableRow,
  TableCell,
  WidthType,
  AlignmentType,
  VerticalAlign,
  BorderStyle,
  PageOrientation,
  convertMillimetersToTwip,
  Header,
  Footer,
  HeadingLevel,
  TableLayoutType,
} from "docx";
import { WatermarkConfig } from "../types/report";
import bwipjs from "bwip-js";

export class ReportController {
  /**
   * 리포트 목록 조회
   * GET /api/admin/reports
   */
  async getReports(req: Request, res: Response, next: NextFunction) {
    try {
      const {
        page = "1",
        limit = "20",
        searchText = "",
        reportType = "",
        useYn = "Y",
        sortBy = "created_at",
        sortOrder = "DESC",
      } = req.query;

      const result = await reportService.getReports({
        page: parseInt(page as string, 10),
        limit: parseInt(limit as string, 10),
        searchText: searchText as string,
        reportType: reportType as string,
        useYn: useYn as string,
        sortBy: sortBy as string,
        sortOrder: sortOrder as "ASC" | "DESC",
      });

      return res.json({
        success: true,
        data: result,
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 리포트 상세 조회
   * GET /api/admin/reports/:reportId
   */
  async getReportById(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;

      const report = await reportService.getReportById(reportId);

      if (!report) {
        return res.status(404).json({
          success: false,
          message: "리포트를 찾을 수 없습니다.",
        });
      }

      return res.json({
        success: true,
        data: report,
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 리포트 생성
   * POST /api/admin/reports
   */
  async createReport(req: Request, res: Response, next: NextFunction) {
    try {
      const data: CreateReportRequest = req.body;
      const userId = (req as any).user?.userId || "SYSTEM";

      // 필수 필드 검증
      if (!data.reportNameKor || !data.reportType) {
        return res.status(400).json({
          success: false,
          message: "리포트명과 리포트 타입은 필수입니다.",
        });
      }

      const reportId = await reportService.createReport(data, userId);

      return res.status(201).json({
        success: true,
        data: {
          reportId,
        },
        message: "리포트가 생성되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 리포트 수정
   * PUT /api/admin/reports/:reportId
   */
  async updateReport(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;
      const data: UpdateReportRequest = req.body;
      const userId = (req as any).user?.userId || "SYSTEM";

      const success = await reportService.updateReport(reportId, data, userId);

      if (!success) {
        return res.status(400).json({
          success: false,
          message: "수정할 내용이 없습니다.",
        });
      }

      return res.json({
        success: true,
        message: "리포트가 수정되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 리포트 삭제
   * DELETE /api/admin/reports/:reportId
   */
  async deleteReport(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;

      const success = await reportService.deleteReport(reportId);

      if (!success) {
        return res.status(404).json({
          success: false,
          message: "리포트를 찾을 수 없습니다.",
        });
      }

      return res.json({
        success: true,
        message: "리포트가 삭제되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 리포트 복사
   * POST /api/admin/reports/:reportId/copy
   */
  async copyReport(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;
      const userId = (req as any).user?.userId || "SYSTEM";

      const newReportId = await reportService.copyReport(reportId, userId);

      if (!newReportId) {
        return res.status(404).json({
          success: false,
          message: "리포트를 찾을 수 없습니다.",
        });
      }

      return res.status(201).json({
        success: true,
        data: {
          reportId: newReportId,
        },
        message: "리포트가 복사되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 레이아웃 조회
   * GET /api/admin/reports/:reportId/layout
   */
  async getLayout(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;

      const layout = await reportService.getLayout(reportId);

      if (!layout) {
        return res.status(404).json({
          success: false,
          message: "레이아웃을 찾을 수 없습니다.",
        });
      }

      // components 컬럼에서 JSON 파싱
      const parsedComponents = layout.components
        ? JSON.parse(layout.components)
        : null;

      let layoutData;
      // 새 구조 (layoutConfig.pages)인지 확인
      if (
        parsedComponents &&
        parsedComponents.pages &&
        Array.isArray(parsedComponents.pages)
      ) {
        // pages 배열을 직접 포함하여 반환
        layoutData = {
          ...layout,
          pages: parsedComponents.pages,
          components: [], // 호환성을 위해 빈 배열
        };
      } else {
        // 기존 구조: components 배열
        layoutData = {
          ...layout,
          components: parsedComponents || [],
        };
      }

      return res.json({
        success: true,
        data: layoutData,
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 레이아웃 저장
   * PUT /api/admin/reports/:reportId/layout
   */
  async saveLayout(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;
      const data: SaveLayoutRequest = req.body;
      const userId = (req as any).user?.userId || "SYSTEM";

      // 필수 필드 검증 (페이지 기반 구조)
      if (
        !data.layoutConfig ||
        !data.layoutConfig.pages ||
        data.layoutConfig.pages.length === 0
      ) {
        return res.status(400).json({
          success: false,
          message: "레이아웃 설정이 필요합니다.",
        });
      }

      await reportService.saveLayout(reportId, data, userId);

      return res.json({
        success: true,
        message: "레이아웃이 저장되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 템플릿 목록 조회
   * GET /api/admin/reports/templates
   */
  async getTemplates(req: Request, res: Response, next: NextFunction) {
    try {
      const templates = await reportService.getTemplates();

      return res.json({
        success: true,
        data: templates,
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 템플릿 생성
   * POST /api/admin/reports/templates
   */
  async createTemplate(req: Request, res: Response, next: NextFunction) {
    try {
      const data: CreateTemplateRequest = req.body;
      const userId = (req as any).user?.userId || "SYSTEM";

      // 필수 필드 검증
      if (!data.templateNameKor || !data.templateType) {
        return res.status(400).json({
          success: false,
          message: "템플릿명과 템플릿 타입은 필수입니다.",
        });
      }

      const templateId = await reportService.createTemplate(data, userId);

      return res.status(201).json({
        success: true,
        data: {
          templateId,
        },
        message: "템플릿이 생성되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 현재 리포트를 템플릿으로 저장
   * POST /api/admin/reports/:reportId/save-as-template
   */
  async saveAsTemplate(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId } = req.params;
      const { templateNameKor, templateNameEng, description } = req.body;
      const userId = (req as any).user?.userId || "SYSTEM";

      // 필수 필드 검증
      if (!templateNameKor) {
        return res.status(400).json({
          success: false,
          message: "템플릿명은 필수입니다.",
        });
      }

      const templateId = await reportService.saveAsTemplate(
        reportId,
        templateNameKor,
        templateNameEng,
        description,
        userId
      );

      return res.status(201).json({
        success: true,
        data: {
          templateId,
        },
        message: "템플릿이 저장되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
   * POST /api/admin/reports/templates/create-from-layout
   */
  async createTemplateFromLayout(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    try {
      const {
        templateNameKor,
        templateNameEng,
        templateType,
        description,
        layoutConfig,
        defaultQueries = [],
      } = req.body;
      const userId = (req as any).user?.userId || "SYSTEM";

      // 필수 필드 검증
      if (!templateNameKor) {
        return res.status(400).json({
          success: false,
          message: "템플릿명은 필수입니다.",
        });
      }

      if (!layoutConfig) {
        return res.status(400).json({
          success: false,
          message: "레이아웃 설정은 필수입니다.",
        });
      }

      const templateId = await reportService.createTemplateFromLayout(
        templateNameKor,
        templateNameEng,
        templateType || "GENERAL",
        description,
        layoutConfig,
        defaultQueries,
        userId
      );

      return res.status(201).json({
        success: true,
        data: {
          templateId,
        },
        message: "템플릿이 생성되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 템플릿 삭제
   * DELETE /api/admin/reports/templates/:templateId
   */
  async deleteTemplate(req: Request, res: Response, next: NextFunction) {
    try {
      const { templateId } = req.params;

      const success = await reportService.deleteTemplate(templateId);

      if (!success) {
        return res.status(404).json({
          success: false,
          message: "템플릿을 찾을 수 없거나 시스템 템플릿입니다.",
        });
      }

      return res.json({
        success: true,
        message: "템플릿이 삭제되었습니다.",
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 쿼리 실행
   * POST /api/admin/reports/:reportId/queries/:queryId/execute
   */
  async executeQuery(req: Request, res: Response, next: NextFunction) {
    try {
      const { reportId, queryId } = req.params;
      const { parameters = {}, sqlQuery, externalConnectionId } = req.body;

      const result = await reportService.executeQuery(
        reportId,
        queryId,
        parameters,
        sqlQuery,
        externalConnectionId
      );

      return res.json({
        success: true,
        data: result,
      });
    } catch (error: any) {
      return res.status(400).json({
        success: false,
        message: error.message || "쿼리 실행에 실패했습니다.",
      });
    }
  }

  /**
   * 외부 DB 연결 목록 조회 (활성화된 것만)
   * GET /api/admin/reports/external-connections
   */
  async getExternalConnections(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    try {
      const { ExternalDbConnectionService } = await import(
        "../services/externalDbConnectionService"
      );

      const result = await ExternalDbConnectionService.getConnections({
        is_active: "Y",
        company_code: req.body.companyCode || "",
      });

      return res.json(result);
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 이미지 파일 업로드
   * POST /api/admin/reports/upload-image
   */
  async uploadImage(req: Request, res: Response, next: NextFunction) {
    try {
      if (!req.file) {
        return res.status(400).json({
          success: false,
          message: "이미지 파일이 필요합니다.",
        });
      }

      const companyCode = req.body.companyCode || "SYSTEM";
      const file = req.file;

      // 파일 저장 경로 생성
      const uploadDir = path.join(
        process.cwd(),
        "uploads",
        `company_${companyCode}`,
        "reports"
      );

      // 디렉토리가 없으면 생성
      if (!fs.existsSync(uploadDir)) {
        fs.mkdirSync(uploadDir, { recursive: true });
      }

      // 고유한 파일명 생성 (타임스탬프 + 원본 파일명)
      const timestamp = Date.now();
      const safeFileName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, "_");
      const fileName = `${timestamp}_${safeFileName}`;
      const filePath = path.join(uploadDir, fileName);

      // 파일 저장
      fs.writeFileSync(filePath, file.buffer);

      // 웹에서 접근 가능한 URL 반환
      const fileUrl = `/uploads/company_${companyCode}/reports/${fileName}`;

      return res.json({
        success: true,
        data: {
          fileName,
          fileUrl,
          originalName: file.originalname,
          size: file.size,
          mimeType: file.mimetype,
        },
      });
    } catch (error) {
      return next(error);
    }
  }

  /**
   * 컴포넌트 데이터를 WORD(DOCX)로 변환
   * POST /api/admin/reports/export-word
   */
  async exportToWord(req: Request, res: Response, next: NextFunction) {
    try {
      const { layoutConfig, queryResults, fileName = "리포트" } = req.body;

      if (!layoutConfig || !layoutConfig.pages) {
        return res.status(400).json({
          success: false,
          message: "레이아웃 데이터가 필요합니다.",
        });
      }

      // mm를 twip으로 변환
      const mmToTwip = (mm: number) => convertMillimetersToTwip(mm);
      
      // 프론트엔드와 동일한 MM_TO_PX 상수 (캔버스에서 mm를 px로 변환할 때 사용하는 값)
      const MM_TO_PX = 4;
      // 1mm = 56.692913386 twip (docx 라이브러리 기준)
      // px를 twip으로 변환: px -> mm -> twip
      const pxToTwip = (px: number) => Math.round((px / MM_TO_PX) * 56.692913386);

      // 쿼리 결과 맵
      const queryResultsMap: Record<
        string,
        { fields: string[]; rows: Record<string, unknown>[] }
      > = queryResults || {};

      // 컴포넌트 값 가져오기
      const getComponentValue = (component: any): string => {
        if (component.queryId && component.fieldName) {
          const queryResult = queryResultsMap[component.queryId];
          if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
            const value = queryResult.rows[0][component.fieldName];
            if (value !== null && value !== undefined) {
              return String(value);
            }
          }
          return `{${component.fieldName}}`;
        }
        return component.defaultValue || "";
      };

      // px → half-point 변환 (1px = 0.75pt, Word는 half-pt 단위 사용)
      // px * 0.75 * 2 = px * 1.5
      const pxToHalfPt = (px: number) => Math.round(px * 1.5);

      // 셀 내용 생성 헬퍼 함수 (가로 배치용)
      const createCellContent = (
        component: any,
        displayValue: string,
        pxToHalfPtFn: (px: number) => number,
        pxToTwipFn: (px: number) => number,
        queryResultsMapRef: Record<
          string,
          { fields: string[]; rows: Record<string, unknown>[] }
        >,
        AlignmentTypeRef: typeof AlignmentType,
        VerticalAlignRef: typeof VerticalAlign,
        BorderStyleRef: typeof BorderStyle,
        ParagraphRef: typeof Paragraph,
        TextRunRef: typeof TextRun,
        ImageRunRef: typeof ImageRun,
        TableRef: typeof Table,
        TableRowRef: typeof TableRow,
        TableCellRef: typeof TableCell,
        pageIndex: number = 0,
        totalPages: number = 1
      ): (Paragraph | Table)[] => {
        const result: (Paragraph | Table)[] = [];

        // Text/Label
        if (component.type === "text" || component.type === "label") {
          const fontSizeHalfPt = pxToHalfPtFn(component.fontSize || 13);
          const alignment =
            component.textAlign === "center"
              ? AlignmentTypeRef.CENTER
              : component.textAlign === "right"
                ? AlignmentTypeRef.RIGHT
                : AlignmentTypeRef.LEFT;

          // 줄바꿈 처리: \n으로 split하여 각 줄을 TextRun으로 생성
          const lines = displayValue.split("\n");
          const textChildren: TextRun[] = [];
          lines.forEach((line: string, index: number) => {
            if (index > 0) {
              // 줄바꿈 추가 (break: 1은 줄바꿈 1개)
              textChildren.push(new TextRunRef({ break: 1 }));
            }
            textChildren.push(
              new TextRunRef({
                text: line,
                size: fontSizeHalfPt,
                color: (component.fontColor || "#000000").replace("#", ""),
                bold:
                  component.fontWeight === "bold" ||
                  component.fontWeight === "600",
                font: "맑은 고딕",
              })
            );
          });

          result.push(
            new ParagraphRef({
              alignment,
              children: textChildren,
            })
          );
        }

        // Image
        else if (component.type === "image" && component.imageBase64) {
          try {
            const base64Data =
              component.imageBase64.split(",")[1] || component.imageBase64;
            const imageBuffer = Buffer.from(base64Data, "base64");
            result.push(
              new ParagraphRef({
                children: [
                  new ImageRunRef({
                    data: imageBuffer,
                    transformation: {
                      width: Math.round(component.width * 0.75),
                      height: Math.round(component.height * 0.75),
                    },
                    type: "png",
                  }),
                ],
              })
            );
          } catch (e) {
            result.push(new ParagraphRef({ children: [] }));
          }
        }

        // Signature
        else if (component.type === "signature") {
          const sigFontSize = pxToHalfPtFn(component.fontSize || 12);
          const textRuns: TextRun[] = [];
          if (component.showLabel !== false) {
            textRuns.push(
              new TextRunRef({
                text: (component.labelText || "서명:") + " ",
                size: sigFontSize,
                font: "맑은 고딕",
              })
            );
          }
          if (component.imageBase64) {
            try {
              const base64Data =
                component.imageBase64.split(",")[1] || component.imageBase64;
              const imageBuffer = Buffer.from(base64Data, "base64");
              // 서명 이미지 크기: 라벨 옆에 인라인으로 표시될 수 있도록 적절한 크기로 조정
              const sigImageHeight = 30; // 고정 높이 (약 40px)
              const sigImageWidth = Math.round((component.width / component.height) * sigImageHeight) || 80;
              result.push(
                new ParagraphRef({
                  children: [
                    ...textRuns,
                    new ImageRunRef({
                      data: imageBuffer,
                      transformation: {
                        width: sigImageWidth,
                        height: sigImageHeight,
                      },
                      type: "png",
                    }),
                  ],
                })
              );
            } catch (e) {
              textRuns.push(
                new TextRunRef({
                  text: "_".repeat(20),
                  size: sigFontSize,
                  font: "맑은 고딕",
                })
              );
              result.push(new ParagraphRef({ children: textRuns }));
            }
          } else {
            textRuns.push(
              new TextRunRef({
                text: "_".repeat(20),
                size: sigFontSize,
                font: "맑은 고딕",
              })
            );
            result.push(new ParagraphRef({ children: textRuns }));
          }
        }

        // Stamp
        else if (component.type === "stamp") {
          const stampFontSize = pxToHalfPtFn(component.fontSize || 12);
          const textRuns: TextRun[] = [];
          if (component.personName) {
            textRuns.push(
              new TextRunRef({
                text: component.personName + " ",
                size: stampFontSize,
                font: "맑은 고딕",
              })
            );
          }
          if (component.imageBase64) {
            try {
              const base64Data =
                component.imageBase64.split(",")[1] || component.imageBase64;
              const imageBuffer = Buffer.from(base64Data, "base64");
              result.push(
                new ParagraphRef({
                  children: [
                    ...textRuns,
                    new ImageRunRef({
                      data: imageBuffer,
                      transformation: {
                        width: Math.round(
                          Math.min(component.width, component.height) * 0.75
                        ),
                        height: Math.round(
                          Math.min(component.width, component.height) * 0.75
                        ),
                      },
                      type: "png",
                    }),
                  ],
                })
              );
            } catch (e) {
              textRuns.push(
                new TextRunRef({
                  text: "(인)",
                  color: "DC2626",
                  size: stampFontSize,
                  font: "맑은 고딕",
                })
              );
              result.push(new ParagraphRef({ children: textRuns }));
            }
          } else {
            textRuns.push(
              new TextRunRef({
                text: "(인)",
                color: "DC2626",
                size: stampFontSize,
                font: "맑은 고딕",
              })
            );
            result.push(new ParagraphRef({ children: textRuns }));
          }
        }

        // PageNumber
        else if (component.type === "pageNumber") {
          const format = component.pageNumberFormat || "number";
          const currentPageNum = pageIndex + 1;
          let pageNumberText = "";
          switch (format) {
            case "number":
              pageNumberText = `${currentPageNum}`;
              break;
            case "numberTotal":
              pageNumberText = `${currentPageNum} / ${totalPages}`;
              break;
            case "koreanNumber":
              pageNumberText = `${currentPageNum} 페이지`;
              break;
            default:
              pageNumberText = `${currentPageNum}`;
          }
          const fontSizeHalfPt = pxToHalfPtFn(component.fontSize || 13);
          const alignment =
            component.textAlign === "center"
              ? AlignmentTypeRef.CENTER
              : component.textAlign === "right"
                ? AlignmentTypeRef.RIGHT
                : AlignmentTypeRef.LEFT;
          result.push(
            new ParagraphRef({
              alignment,
              children: [
                new TextRunRef({
                  text: pageNumberText,
                  size: fontSizeHalfPt,
                  color: (component.fontColor || "#000000").replace("#", ""),
                  font: "맑은 고딕",
                }),
              ],
            })
          );
        }

        // Card 컴포넌트
        else if (component.type === "card") {
          const cardTitle = component.cardTitle || "정보 카드";
          const cardItems = component.cardItems || [];
          const labelWidth = component.labelWidth || 80;
          const showCardTitle = component.showCardTitle !== false;
          const titleFontSize = pxToHalfPtFn(component.titleFontSize || 14);
          const labelFontSize = pxToHalfPtFn(component.labelFontSize || 13);
          const valueFontSize = pxToHalfPtFn(component.valueFontSize || 13);
          const titleColor = (component.titleColor || "#1e40af").replace(
            "#",
            ""
          );
          const labelColor = (component.labelColor || "#374151").replace(
            "#",
            ""
          );
          const valueColor = (component.valueColor || "#000000").replace(
            "#",
            ""
          );
          const borderColor = (component.borderColor || "#e5e7eb").replace(
            "#",
            ""
          );

          // 쿼리 바인딩된 값 가져오기
          const getCardValueFn = (item: {
            label: string;
            value: string;
            fieldName?: string;
          }) => {
            if (
              item.fieldName &&
              component.queryId &&
              queryResultsMapRef[component.queryId]
            ) {
              const qResult = queryResultsMapRef[component.queryId];
              if (qResult.rows && qResult.rows.length > 0) {
                const row = qResult.rows[0];
                return row[item.fieldName] !== undefined
                  ? String(row[item.fieldName])
                  : item.value;
              }
            }
            return item.value;
          };

          // 제목
          if (showCardTitle) {
            result.push(
              new ParagraphRef({
                children: [
                  new TextRunRef({
                    text: cardTitle,
                    size: titleFontSize,
                    color: titleColor,
                    bold: true,
                    font: "맑은 고딕",
                  }),
                ],
              })
            );
            // 구분선
            result.push(
              new ParagraphRef({
                border: {
                  bottom: {
                    color: borderColor,
                    space: 1,
                    style: BorderStyleRef.SINGLE,
                    size: 8,
                  },
                },
                children: [],
              })
            );
          }

          // 항목들
          for (const item of cardItems) {
            const itemValue = getCardValueFn(
              item as { label: string; value: string; fieldName?: string }
            );
            result.push(
              new ParagraphRef({
                children: [
                  new TextRunRef({
                    text: item.label,
                    size: labelFontSize,
                    color: labelColor,
                    bold: true,
                    font: "맑은 고딕",
                  }),
                  new TextRunRef({
                    text: "  ",
                    size: labelFontSize,
                    font: "맑은 고딕",
                  }),
                  new TextRunRef({
                    text: itemValue,
                    size: valueFontSize,
                    color: valueColor,
                    font: "맑은 고딕",
                  }),
                ],
              })
            );
          }
        }

        // 계산 컴포넌트
        else if (component.type === "calculation") {
          const calcItems = component.calcItems || [];
          const resultLabel = component.resultLabel || "합계";
          const calcLabelWidth = component.labelWidth || 120;
          const calcLabelFontSize = pxToHalfPtFn(component.labelFontSize || 13);
          const calcValueFontSize = pxToHalfPtFn(component.valueFontSize || 13);
          const calcResultFontSize = pxToHalfPtFn(
            component.resultFontSize || 16
          );
          const calcLabelColor = (component.labelColor || "#374151").replace(
            "#",
            ""
          );
          const calcValueColor = (component.valueColor || "#000000").replace(
            "#",
            ""
          );
          const calcResultColor = (component.resultColor || "#2563eb").replace(
            "#",
            ""
          );
          const numberFormat = component.numberFormat || "currency";
          const currencySuffix = component.currencySuffix || "원";
          const borderColor = (component.borderColor || "#374151").replace(
            "#",
            ""
          );

          // 숫자 포맷팅 함수
          const formatNumberFn = (num: number): string => {
            if (numberFormat === "none") return String(num);
            if (numberFormat === "comma") return num.toLocaleString();
            if (numberFormat === "currency")
              return num.toLocaleString() + currencySuffix;
            return String(num);
          };

          // 쿼리 바인딩된 값 가져오기
          const getCalcItemValueFn = (item: {
            label: string;
            value: number | string;
            operator: string;
            fieldName?: string;
          }): number => {
            if (
              item.fieldName &&
              component.queryId &&
              queryResultsMapRef[component.queryId]
            ) {
              const qResult = queryResultsMapRef[component.queryId];
              if (qResult.rows && qResult.rows.length > 0) {
                const row = qResult.rows[0];
                const val = row[item.fieldName];
                return typeof val === "number"
                  ? val
                  : parseFloat(String(val)) || 0;
              }
            }
            return typeof item.value === "number"
              ? item.value
              : parseFloat(String(item.value)) || 0;
          };

          // 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
          let calcResult = 0;
          if (calcItems.length > 0) {
            // 첫 번째 항목은 기준값
            calcResult = getCalcItemValueFn(
              calcItems[0] as {
                label: string;
                value: number | string;
                operator: string;
                fieldName?: string;
              }
            );

            // 두 번째 항목부터 연산자 적용
            for (let i = 1; i < calcItems.length; i++) {
              const item = calcItems[i];
              const val = getCalcItemValueFn(
                item as {
                  label: string;
                  value: number | string;
                  operator: string;
                  fieldName?: string;
                }
              );
              switch ((item as { operator: string }).operator) {
                case "+":
                  calcResult += val;
                  break;
                case "-":
                  calcResult -= val;
                  break;
                case "x":
                  calcResult *= val;
                  break;
                case "÷":
                  calcResult = val !== 0 ? calcResult / val : calcResult;
                  break;
              }
            }
          }

          // 테이블로 계산 항목 렌더링
          const calcTableRows = [];

          // 각 항목
          for (const item of calcItems) {
            const itemValue = getCalcItemValueFn(
              item as {
                label: string;
                value: number | string;
                operator: string;
                fieldName?: string;
              }
            );
            calcTableRows.push(
              new TableRowRef({
                children: [
                  new TableCellRef({
                    children: [
                      new ParagraphRef({
                        children: [
                          new TextRunRef({
                            text: item.label,
                            size: calcLabelFontSize,
                            color: calcLabelColor,
                            font: "맑은 고딕",
                          }),
                        ],
                      }),
                    ],
                    width: {
                      size: pxToTwipFn(calcLabelWidth),
                      type: WidthType.DXA,
                    },
                    borders: {
                      top: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      bottom: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
                    margins: { top: 50, bottom: 50, left: 100, right: 100 },
                  }),
                  new TableCellRef({
                    children: [
                      new ParagraphRef({
                        alignment: AlignmentTypeRef.RIGHT,
                        children: [
                          new TextRunRef({
                            text: formatNumberFn(itemValue),
                            size: calcValueFontSize,
                            color: calcValueColor,
                            font: "맑은 고딕",
                          }),
                        ],
                      }),
                    ],
                    borders: {
                      top: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      bottom: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyleRef.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
                    margins: { top: 50, bottom: 50, left: 100, right: 100 },
                  }),
                ],
              })
            );
          }

          // 구분선 행
          calcTableRows.push(
            new TableRowRef({
              children: [
                new TableCellRef({
                  columnSpan: 2,
                  children: [new ParagraphRef({ children: [] })],
                  borders: {
                    top: {
                      style: BorderStyleRef.SINGLE,
                      size: 8,
                      color: borderColor,
                    },
                    bottom: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    left: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    right: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                  },
                }),
              ],
            })
          );

          // 결과 행
          calcTableRows.push(
            new TableRowRef({
              children: [
                new TableCellRef({
                  children: [
                    new ParagraphRef({
                      children: [
                        new TextRunRef({
                          text: resultLabel,
                          size: calcResultFontSize,
                          color: calcLabelColor,
                          bold: true,
                          font: "맑은 고딕",
                        }),
                      ],
                    }),
                  ],
                  width: {
                    size: pxToTwipFn(calcLabelWidth),
                    type: WidthType.DXA,
                  },
                  borders: {
                    top: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    bottom: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    left: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    right: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                  },
                  margins: { top: 50, bottom: 50, left: 100, right: 100 },
                }),
                new TableCellRef({
                  children: [
                    new ParagraphRef({
                      alignment: AlignmentTypeRef.RIGHT,
                      children: [
                        new TextRunRef({
                          text: formatNumberFn(calcResult),
                          size: calcResultFontSize,
                          color: calcResultColor,
                          bold: true,
                          font: "맑은 고딕",
                        }),
                      ],
                    }),
                  ],
                  borders: {
                    top: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    bottom: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    left: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    right: {
                      style: BorderStyleRef.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                  },
                  margins: { top: 50, bottom: 50, left: 100, right: 100 },
                }),
              ],
            })
          );

          result.push(
            new TableRef({
              rows: calcTableRows,
              width: { size: pxToTwipFn(component.width), type: WidthType.DXA },
              borders: {
                top: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
                bottom: {
                  style: BorderStyleRef.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyleRef.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyleRef.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            })
          );
        }

        // Barcode 컴포넌트 (바코드 이미지가 미리 생성되어 전달된 경우)
        else if (component.type === "barcode" && component.barcodeImageBase64) {
          try {
            const base64Data =
              component.barcodeImageBase64.split(",")[1] || component.barcodeImageBase64;
            const imageBuffer = Buffer.from(base64Data, "base64");
            result.push(
              new ParagraphRef({
                children: [
                  new ImageRunRef({
                    data: imageBuffer,
                    transformation: {
                      width: Math.round(component.width * 0.75),
                      height: Math.round(component.height * 0.75),
                    },
                    type: "png",
                  }),
                ],
              })
            );
          } catch (e) {
            // 바코드 이미지 생성 실패 시 텍스트로 대체
            const barcodeValue = component.barcodeValue || "BARCODE";
            result.push(
              new ParagraphRef({
                children: [
                  new TextRunRef({
                    text: `[${barcodeValue}]`,
                    size: pxToHalfPtFn(12),
                    font: "맑은 고딕",
                  }),
                ],
              })
            );
          }
        }

        // Checkbox 컴포넌트
        else if (component.type === "checkbox") {
          // 체크 상태 결정 (쿼리 바인딩 또는 고정값)
          let isChecked = component.checkboxChecked === true;
          if (component.checkboxFieldName && component.queryId && queryResultsMapRef[component.queryId]) {
            const qResult = queryResultsMapRef[component.queryId];
            if (qResult.rows && qResult.rows.length > 0) {
              const row = qResult.rows[0];
              const val = row[component.checkboxFieldName];
              // truthy/falsy 값 판정
              if (val === true || val === "true" || val === "Y" || val === 1 || val === "1") {
                isChecked = true;
              } else {
                isChecked = false;
              }
            }
          }
          
          const checkboxSymbol = isChecked ? "☑" : "☐";
          const checkboxLabel = component.checkboxLabel || "";
          const labelPosition = component.checkboxLabelPosition || "right";
          const displayText = labelPosition === "left" 
            ? `${checkboxLabel} ${checkboxSymbol}` 
            : `${checkboxSymbol} ${checkboxLabel}`;
          
          result.push(
            new ParagraphRef({
              children: [
                new TextRunRef({
                  text: displayText.trim(),
                  size: pxToHalfPtFn(component.fontSize || 14),
                  font: "맑은 고딕",
                  color: (component.fontColor || "#374151").replace("#", ""),
                }),
              ],
            })
          );
        }

        // Divider - 테이블 셀로 감싸서 정확한 너비 적용
        else if (
          component.type === "divider" &&
          component.orientation === "horizontal"
        ) {
          result.push(
            new ParagraphRef({
              border: {
                bottom: {
                  color: (component.lineColor || "#000000").replace("#", ""),
                  space: 1,
                  style: BorderStyleRef.SINGLE,
                  size: (component.lineWidth || 1) * 8,
                },
              },
              children: [],
            })
          );
        }

        // 기타 (빈 paragraph)
        else {
          result.push(new ParagraphRef({ children: [] }));
        }

        return result;
      };

      // 바코드 이미지 생성 헬퍼 함수
      const generateBarcodeImage = async (
        component: any,
        queryResultsMapRef: Record<string, { fields: string[]; rows: Record<string, unknown>[] }>
      ): Promise<string | null> => {
        try {
          const barcodeType = component.barcodeType || "CODE128";
          const barcodeColor = (component.barcodeColor || "#000000").replace("#", "");
          // transparent는 bwip-js에서 지원하지 않으므로 흰색으로 변환
          let barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", "");
          if (barcodeBackground === "transparent" || barcodeBackground === "") {
            barcodeBackground = "ffffff";
          }
          
          // 바코드 값 결정 (쿼리 바인딩 또는 고정값)
          let barcodeValue = component.barcodeValue || "SAMPLE123";
          
          // QR코드 다중 필드 모드
          if (
            barcodeType === "QR" &&
            component.qrUseMultiField &&
            component.qrDataFields &&
            component.qrDataFields.length > 0 &&
            component.queryId &&
            queryResultsMapRef[component.queryId]
          ) {
            const qResult = queryResultsMapRef[component.queryId];
            if (qResult.rows && qResult.rows.length > 0) {
              // 모든 행 포함 모드
              if (component.qrIncludeAllRows) {
                const allRowsData: Record<string, string>[] = [];
                qResult.rows.forEach((row) => {
                  const rowData: Record<string, string> = {};
                  component.qrDataFields!.forEach((field: { fieldName: string; label: string }) => {
                    if (field.fieldName && field.label) {
                      const val = row[field.fieldName];
                      rowData[field.label] = val !== null && val !== undefined ? String(val) : "";
                    }
                  });
                  allRowsData.push(rowData);
                });
                barcodeValue = JSON.stringify(allRowsData);
              } else {
                // 단일 행 (첫 번째 행만)
                const row = qResult.rows[0];
                const jsonData: Record<string, string> = {};
                component.qrDataFields.forEach((field: { fieldName: string; label: string }) => {
                  if (field.fieldName && field.label) {
                    const val = row[field.fieldName];
                    jsonData[field.label] = val !== null && val !== undefined ? String(val) : "";
                  }
                });
                barcodeValue = JSON.stringify(jsonData);
              }
            }
          }
          // 단일 필드 바인딩
          else if (component.barcodeFieldName && component.queryId && queryResultsMapRef[component.queryId]) {
            const qResult = queryResultsMapRef[component.queryId];
            if (qResult.rows && qResult.rows.length > 0) {
              // QR코드 + 모든 행 포함
              if (barcodeType === "QR" && component.qrIncludeAllRows) {
                const allValues = qResult.rows
                  .map((row) => {
                    const val = row[component.barcodeFieldName!];
                    return val !== null && val !== undefined ? String(val) : "";
                  })
                  .filter((v) => v !== "");
                barcodeValue = JSON.stringify(allValues);
              } else {
                // 단일 행 (첫 번째 행만)
                const row = qResult.rows[0];
                const val = row[component.barcodeFieldName];
                if (val !== null && val !== undefined) {
                  barcodeValue = String(val);
                }
              }
            }
          }

          // bwip-js 바코드 타입 매핑
          const bcidMap: Record<string, string> = {
            "CODE128": "code128",
            "CODE39": "code39",
            "EAN13": "ean13",
            "EAN8": "ean8",
            "UPC": "upca",
            "QR": "qrcode",
          };

          const bcid = bcidMap[barcodeType] || "code128";
          const isQR = barcodeType === "QR";

          // 바코드 옵션 설정
          const options: any = {
            bcid: bcid,
            text: barcodeValue,
            scale: 3,
            includetext: !isQR && component.showBarcodeText !== false,
            textxalign: "center",
            barcolor: barcodeColor,
            backgroundcolor: barcodeBackground,
          };

          // QR 코드 옵션
          if (isQR) {
            options.eclevel = component.qrErrorCorrectionLevel || "M";
          }

          // 바코드 이미지 생성
          const png = await bwipjs.toBuffer(options);
          const base64 = png.toString("base64");
          return `data:image/png;base64,${base64}`;
        } catch (error) {
          console.error("바코드 생성 오류:", error);
          return null;
        }
      };

      // 모든 페이지의 바코드 컴포넌트에 대해 이미지 생성
      for (const page of layoutConfig.pages) {
        if (page.components) {
          for (const component of page.components) {
            if (component.type === "barcode") {
              const barcodeImage = await generateBarcodeImage(component, queryResultsMap);
              if (barcodeImage) {
                component.barcodeImageBase64 = barcodeImage;
              }
            }
          }
        }
      }

      // 섹션 생성 (페이지별)
      const sortedPages = layoutConfig.pages.sort(
        (a: any, b: any) => a.page_order - b.page_order
      );
      const totalPagesCount = sortedPages.length;

      const sections = sortedPages.map((page: any, pageIndex: number) => {
        const pageWidthTwip = mmToTwip(page.width);
        const pageHeightTwip = mmToTwip(page.height);
        const marginTopMm = page.margins?.top || 10;
        const marginBottomMm = page.margins?.bottom || 10;
        const marginLeftMm = page.margins?.left || 10;
        const marginRightMm = page.margins?.right || 10;

        const marginTop = mmToTwip(marginTopMm);
        const marginBottom = mmToTwip(marginBottomMm);
        const marginLeft = mmToTwip(marginLeftMm);
        const marginRight = mmToTwip(marginRightMm);

        // 마진을 px로 변환 (1mm ≈ 3.78px at 96 DPI)
        const marginLeftPx = marginLeftMm * 3.78;
        const marginTopPx = marginTopMm * 3.78;

        // 컴포넌트를 Y좌표순으로 정렬
        const sortedComponents = [...(page.components || [])].sort(
          (a: any, b: any) => a.y - b.y
        );

        // 같은 Y좌표 범위(±30px)의 컴포넌트들을 그룹화
        const Y_GROUP_THRESHOLD = 30; // px
        const componentGroups: any[][] = [];
        let currentGroup: any[] = [];
        let groupBaseY = -Infinity;

        for (const comp of sortedComponents) {
          const compY = comp.y - marginTopPx;
          if (currentGroup.length === 0) {
            currentGroup.push(comp);
            groupBaseY = compY;
          } else if (Math.abs(compY - groupBaseY) <= Y_GROUP_THRESHOLD) {
            currentGroup.push(comp);
          } else {
            componentGroups.push(currentGroup);
            currentGroup = [comp];
            groupBaseY = compY;
          }
        }
        if (currentGroup.length > 0) {
          componentGroups.push(currentGroup);
        }

        // 컴포넌트를 Paragraph/Table로 변환
        const children: (Paragraph | Table)[] = [];

        // Y좌표를 spacing으로 변환하기 위한 추적 변수
        let lastBottomY = 0;

        // 각 그룹 처리
        for (const group of componentGroups) {
          // 그룹 내 컴포넌트들을 X좌표 순으로 정렬
          const sortedGroup = [...group].sort((a: any, b: any) => a.x - b.x);

          // 그룹의 Y 좌표 (첫 번째 컴포넌트 기준)
          const groupY = Math.max(0, sortedGroup[0].y - marginTopPx);
          const groupHeight = Math.max(
            ...sortedGroup.map((c: any) => c.height)
          );

          // spacing 계산
          const gapFromPrevious = Math.max(0, groupY - lastBottomY);
          const spacingBefore = pxToTwip(gapFromPrevious);

          // 그룹에 컴포넌트가 여러 개면 하나의 테이블 행으로 배치
          if (sortedGroup.length > 1) {
            // spacing을 위한 빈 paragraph
            if (spacingBefore > 0) {
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  children: [],
                })
              );
            }

            // 각 컴포넌트를 셀로 변환
            const cells: TableCell[] = [];
            let prevEndX = 0;

            for (const component of sortedGroup) {
              const adjustedX = Math.max(0, component.x - marginLeftPx);
              const displayValue = getComponentValue(component);

              // 이전 셀과의 간격을 위한 빈 셀 추가
              if (adjustedX > prevEndX + 5) {
                const gapWidth = adjustedX - prevEndX;
                cells.push(
                  new TableCell({
                    children: [new Paragraph({ children: [] })],
                    width: { size: pxToTwip(gapWidth), type: WidthType.DXA },
                    borders: {
                      top: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      bottom: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
                  })
                );
              }

              // 컴포넌트 셀 생성
              const cellContent = createCellContent(
                component,
                displayValue,
                pxToHalfPt,
                pxToTwip,
                queryResultsMap,
                AlignmentType,
                VerticalAlign,
                BorderStyle,
                Paragraph,
                TextRun,
                ImageRun,
                Table,
                TableRow,
                TableCell,
                pageIndex,
                totalPagesCount
              );
              cells.push(
                new TableCell({
                  children: cellContent,
                  width: {
                    size: pxToTwip(component.width),
                    type: WidthType.DXA,
                  },
                  borders: {
                    top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                    bottom: {
                      style: BorderStyle.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                    left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                    right: {
                      style: BorderStyle.NONE,
                      size: 0,
                      color: "FFFFFF",
                    },
                  },
                  verticalAlign: VerticalAlign.TOP,
                })
              );
              prevEndX = adjustedX + component.width;
            }

            // 테이블 행 생성
            const rowTable = new Table({
              rows: [new TableRow({ children: cells })],
              width: { size: 100, type: WidthType.PERCENTAGE },
              layout: TableLayoutType.FIXED, // 셀 너비 고정
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            });
            children.push(rowTable);
            lastBottomY = groupY + groupHeight;
            continue;
          }

          // 단일 컴포넌트 처리 (기존 로직)
          const component = sortedGroup[0];
          const displayValue = getComponentValue(component);
          const adjustedX = Math.max(0, component.x - marginLeftPx);
          const adjustedY = groupY;

          // X좌표를 indent로 변환 (마진 제외한 순수 들여쓰기)
          const indentLeft = pxToTwip(adjustedX);

          // Text/Label 컴포넌트 - 테이블 셀로 감싸서 width 내 줄바꿈 적용
          if (component.type === "text" || component.type === "label") {
            const fontSizeHalfPt = pxToHalfPt(component.fontSize || 13);
            const alignment =
              component.textAlign === "center"
                ? AlignmentType.CENTER
                : component.textAlign === "right"
                  ? AlignmentType.RIGHT
                  : AlignmentType.LEFT;

            // 줄바꿈 처리: \n으로 split하여 각 줄을 TextRun으로 생성
            const lines = displayValue.split("\n");
            const textChildren: TextRun[] = [];
            lines.forEach((line: string, index: number) => {
              if (index > 0) {
                textChildren.push(new TextRun({ break: 1 }));
              }
              textChildren.push(
                new TextRun({
                  text: line,
                  size: fontSizeHalfPt,
                  color: (component.fontColor || "#000000").replace("#", ""),
                  bold:
                    component.fontWeight === "bold" ||
                    component.fontWeight === "600",
                  font: "맑은 고딕",
                })
              );
            });

            // 테이블 셀로 감싸서 width 제한 → 자동 줄바꿈
            const textCell = new TableCell({
              children: [
                new Paragraph({
                  alignment,
                  children: textChildren,
                }),
              ],
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
              },
              verticalAlign: VerticalAlign.TOP,
            });

            const textTable = new Table({
              rows: [new TableRow({ children: [textCell] })],
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              layout: TableLayoutType.FIXED, // 셀 너비 고정
              indent: { size: indentLeft, type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            });

            // spacing을 위한 빈 paragraph
            if (spacingBefore > 0) {
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  children: [],
                })
              );
            }
            children.push(textTable);
            lastBottomY = adjustedY + component.height;
          }

          // Image 컴포넌트
          else if (component.type === "image" && component.imageBase64) {
            try {
              const base64Data =
                component.imageBase64.split(",")[1] || component.imageBase64;
              const imageBuffer = Buffer.from(base64Data, "base64");

              const paragraph = new Paragraph({
                spacing: { before: spacingBefore, after: 0 },
                indent: { left: indentLeft },
                children: [
                  new ImageRun({
                    data: imageBuffer,
                    transformation: {
                      width: Math.round(component.width * 0.75),
                      height: Math.round(component.height * 0.75),
                    },
                    type: "png",
                  }),
                ],
              });
              children.push(paragraph);
              lastBottomY = adjustedY + component.height;
            } catch (imgError) {
              console.error("이미지 처리 오류:", imgError);
            }
          }

          // Divider 컴포넌트 - 테이블 셀로 감싸서 정확한 위치와 너비 적용
          else if (component.type === "divider") {
            if (component.orientation === "horizontal") {
              // spacing을 위한 빈 paragraph
              if (spacingBefore > 0) {
                children.push(
                  new Paragraph({
                    spacing: { before: spacingBefore, after: 0 },
                    children: [],
                  })
                );
              }

              // 테이블 셀로 감싸서 너비 제한
              const dividerCell = new TableCell({
                children: [
                  new Paragraph({
                    border: {
                      bottom: {
                        color: (component.lineColor || "#000000").replace(
                          "#",
                          ""
                        ),
                        space: 1,
                        style: BorderStyle.SINGLE,
                        size: (component.lineWidth || 1) * 8,
                      },
                    },
                    children: [],
                  }),
                ],
                width: { size: pxToTwip(component.width), type: WidthType.DXA },
                borders: {
                  top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                },
              });

              const dividerTable = new Table({
                rows: [new TableRow({ children: [dividerCell] })],
                width: { size: pxToTwip(component.width), type: WidthType.DXA },
                indent: { size: indentLeft, type: WidthType.DXA },
                borders: {
                  top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                  insideHorizontal: {
                    style: BorderStyle.NONE,
                    size: 0,
                    color: "FFFFFF",
                  },
                  insideVertical: {
                    style: BorderStyle.NONE,
                    size: 0,
                    color: "FFFFFF",
                  },
                },
              });
              children.push(dividerTable);
              lastBottomY = adjustedY + component.height;
            }
          }

          // Signature 컴포넌트
          else if (component.type === "signature") {
            const labelText = component.labelText || "서명:";
            const showLabel = component.showLabel !== false;
            const sigFontSize = pxToHalfPt(component.fontSize || 12);
            const textRuns: TextRun[] = [];

            if (showLabel) {
              textRuns.push(
                new TextRun({
                  text: labelText + " ",
                  size: sigFontSize,
                  font: "맑은 고딕",
                })
              );
            }

            if (component.imageBase64) {
              try {
                const base64Data =
                  component.imageBase64.split(",")[1] || component.imageBase64;
                const imageBuffer = Buffer.from(base64Data, "base64");

                // 서명 이미지 크기: 라벨 옆에 인라인으로 표시될 수 있도록 적절한 크기로 조정
                const sigImageHeight = 30; // 고정 높이
                const sigImageWidth = Math.round((component.width / component.height) * sigImageHeight) || 80;

                const paragraph = new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  indent: { left: indentLeft },
                  children: [
                    ...textRuns,
                    new ImageRun({
                      data: imageBuffer,
                      transformation: {
                        width: sigImageWidth,
                        height: sigImageHeight,
                      },
                      type: "png",
                    }),
                  ],
                });
                children.push(paragraph);
              } catch (imgError) {
                console.error("서명 이미지 오류:", imgError);
                textRuns.push(
                  new TextRun({
                    text: "_".repeat(20),
                    size: sigFontSize,
                    font: "맑은 고딕",
                  })
                );
                children.push(
                  new Paragraph({
                    spacing: { before: spacingBefore, after: 0 },
                    indent: { left: indentLeft },
                    children: textRuns,
                  })
                );
              }
            } else {
              textRuns.push(
                new TextRun({
                  text: "_".repeat(20),
                  size: sigFontSize,
                  font: "맑은 고딕",
                })
              );
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  indent: { left: indentLeft },
                  children: textRuns,
                })
              );
            }
            lastBottomY = adjustedY + component.height;
          }

          // Stamp 컴포넌트
          else if (component.type === "stamp") {
            const personName = component.personName || "";
            const stampFontSize = pxToHalfPt(component.fontSize || 12);
            const textRuns: TextRun[] = [];

            if (personName) {
              textRuns.push(
                new TextRun({
                  text: personName + " ",
                  size: stampFontSize,
                  font: "맑은 고딕",
                })
              );
            }

            if (component.imageBase64) {
              try {
                const base64Data =
                  component.imageBase64.split(",")[1] || component.imageBase64;
                const imageBuffer = Buffer.from(base64Data, "base64");

                const paragraph = new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  indent: { left: indentLeft },
                  children: [
                    ...textRuns,
                    new ImageRun({
                      data: imageBuffer,
                      transformation: {
                        width: Math.round(
                          Math.min(component.width, component.height) * 0.75
                        ),
                        height: Math.round(
                          Math.min(component.width, component.height) * 0.75
                        ),
                      },
                      type: "png",
                    }),
                  ],
                });
                children.push(paragraph);
              } catch (imgError) {
                console.error("도장 이미지 오류:", imgError);
                textRuns.push(
                  new TextRun({
                    text: "(인)",
                    color: "DC2626",
                    size: stampFontSize,
                    font: "맑은 고딕",
                  })
                );
                children.push(
                  new Paragraph({
                    spacing: { before: spacingBefore, after: 0 },
                    indent: { left: indentLeft },
                    children: textRuns,
                  })
                );
              }
            } else {
              textRuns.push(
                new TextRun({
                  text: "(인)",
                  color: "DC2626",
                  size: stampFontSize,
                  font: "맑은 고딕",
                })
              );
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  indent: { left: indentLeft },
                  children: textRuns,
                })
              );
            }
            lastBottomY = adjustedY + component.height;
          }

          // PageNumber 컴포넌트 - 테이블 셀로 감싸서 정확한 위치 적용
          else if (component.type === "pageNumber") {
            const format = component.pageNumberFormat || "number";
            const currentPageNum = pageIndex + 1;
            let pageNumberText = "";
            switch (format) {
              case "number":
                pageNumberText = `${currentPageNum}`;
                break;
              case "numberTotal":
                pageNumberText = `${currentPageNum} / ${totalPagesCount}`;
                break;
              case "koreanNumber":
                pageNumberText = `${currentPageNum} 페이지`;
                break;
              default:
                pageNumberText = `${currentPageNum}`;
            }
            const pageNumFontSize = pxToHalfPt(component.fontSize || 13);
            const alignment =
              component.textAlign === "center"
                ? AlignmentType.CENTER
                : component.textAlign === "right"
                  ? AlignmentType.RIGHT
                  : AlignmentType.LEFT;

            // 테이블 셀로 감싸서 width와 indent 정확히 적용
            const pageNumCell = new TableCell({
              children: [
                new Paragraph({
                  alignment,
                  children: [
                    new TextRun({
                      text: pageNumberText,
                      size: pageNumFontSize,
                      color: (component.fontColor || "#000000").replace(
                        "#",
                        ""
                      ),
                      font: "맑은 고딕",
                    }),
                  ],
                }),
              ],
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
              },
              verticalAlign: VerticalAlign.TOP,
            });

            const pageNumTable = new Table({
              rows: [new TableRow({ children: [pageNumCell] })],
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              indent: { size: indentLeft, type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            });

            // spacing을 위한 빈 paragraph
            if (spacingBefore > 0) {
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  children: [],
                })
              );
            }
            children.push(pageNumTable);
            lastBottomY = adjustedY + component.height;
          }

          // Card 컴포넌트 - 테이블로 감싸서 정확한 위치 적용
          else if (component.type === "card") {
            const cardTitle = component.cardTitle || "정보 카드";
            const cardItems = component.cardItems || [];
            const labelWidthPx = component.labelWidth || 80;
            const showCardTitle = component.showCardTitle !== false;
            const titleFontSize = pxToHalfPt(component.titleFontSize || 14);
            const labelFontSizeCard = pxToHalfPt(component.labelFontSize || 13);
            const valueFontSizeCard = pxToHalfPt(component.valueFontSize || 13);
            const titleColorCard = (component.titleColor || "#1e40af").replace(
              "#",
              ""
            );
            const labelColorCard = (component.labelColor || "#374151").replace(
              "#",
              ""
            );
            const valueColorCard = (component.valueColor || "#000000").replace(
              "#",
              ""
            );
            const borderColorCard = (
              component.borderColor || "#e5e7eb"
            ).replace("#", "");

            // 쿼리 바인딩된 값 가져오기
            const getCardValueLocal = (item: {
              label: string;
              value: string;
              fieldName?: string;
            }) => {
              if (
                item.fieldName &&
                component.queryId &&
                queryResultsMap[component.queryId]
              ) {
                const qResult = queryResultsMap[component.queryId];
                if (qResult.rows && qResult.rows.length > 0) {
                  const row = qResult.rows[0];
                  return row[item.fieldName] !== undefined
                    ? String(row[item.fieldName])
                    : item.value;
                }
              }
              return item.value;
            };

            const cardParagraphs: Paragraph[] = [];

            // 제목
            if (showCardTitle) {
              cardParagraphs.push(
                new Paragraph({
                  children: [
                    new TextRun({
                      text: cardTitle,
                      size: titleFontSize,
                      color: titleColorCard,
                      bold: true,
                      font: "맑은 고딕",
                    }),
                  ],
                })
              );
              // 구분선
              cardParagraphs.push(
                new Paragraph({
                  border: {
                    bottom: {
                      color: borderColorCard,
                      space: 1,
                      style: BorderStyle.SINGLE,
                      size: 8,
                    },
                  },
                  children: [],
                })
              );
            }

            // 항목들을 테이블로 구성 (라벨 + 값)
            const itemRows = cardItems.map(
              (item: { label: string; value: string; fieldName?: string }) => {
                const itemValue = getCardValueLocal(item);
                return new TableRow({
                  children: [
                    new TableCell({
                      width: {
                        size: pxToTwip(labelWidthPx),
                        type: WidthType.DXA,
                      },
                      children: [
                        new Paragraph({
                          children: [
                            new TextRun({
                              text: item.label,
                              size: labelFontSizeCard,
                              color: labelColorCard,
                              bold: true,
                              font: "맑은 고딕",
                            }),
                          ],
                        }),
                      ],
                      borders: {
                        top: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        bottom: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        left: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        right: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                      },
                    }),
                    new TableCell({
                      width: {
                        size: pxToTwip(component.width - labelWidthPx - 16),
                        type: WidthType.DXA,
                      },
                      children: [
                        new Paragraph({
                          children: [
                            new TextRun({
                              text: itemValue,
                              size: valueFontSizeCard,
                              color: valueColorCard,
                              font: "맑은 고딕",
                            }),
                          ],
                        }),
                      ],
                      borders: {
                        top: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        bottom: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        left: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        right: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                      },
                    }),
                  ],
                });
              }
            );

            const itemsTable = new Table({
              rows: itemRows,
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            });

            // 전체를 하나의 테이블 셀로 감싸기
            const cardCell = new TableCell({
              children: [...cardParagraphs, itemsTable],
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              borders:
                component.showCardBorder !== false
                  ? {
                      top: {
                        style: BorderStyle.SINGLE,
                        size: 4,
                        color: borderColorCard,
                      },
                      bottom: {
                        style: BorderStyle.SINGLE,
                        size: 4,
                        color: borderColorCard,
                      },
                      left: {
                        style: BorderStyle.SINGLE,
                        size: 4,
                        color: borderColorCard,
                      },
                      right: {
                        style: BorderStyle.SINGLE,
                        size: 4,
                        color: borderColorCard,
                      },
                    }
                  : {
                      top: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      bottom: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
              verticalAlign: VerticalAlign.TOP,
            });

            const cardTable = new Table({
              rows: [new TableRow({ children: [cardCell] })],
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              indent: { size: indentLeft, type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            });

            // spacing을 위한 빈 paragraph
            if (spacingBefore > 0) {
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  children: [],
                })
              );
            }
            children.push(cardTable);
            lastBottomY = adjustedY + component.height;
          }

          // 계산 컴포넌트 - 테이블로 감싸서 정확한 위치 적용
          else if (component.type === "calculation") {
            const calcItems = component.calcItems || [];
            const resultLabel = component.resultLabel || "합계";
            const calcLabelWidth = component.labelWidth || 120;
            const calcLabelFontSize = pxToHalfPt(component.labelFontSize || 13);
            const calcValueFontSize = pxToHalfPt(component.valueFontSize || 13);
            const calcResultFontSize = pxToHalfPt(
              component.resultFontSize || 16
            );
            const calcLabelColor = (component.labelColor || "#374151").replace(
              "#",
              ""
            );
            const calcValueColor = (component.valueColor || "#000000").replace(
              "#",
              ""
            );
            const calcResultColor = (
              component.resultColor || "#2563eb"
            ).replace("#", "");
            const numberFormat = component.numberFormat || "currency";
            const currencySuffix = component.currencySuffix || "원";
            const borderColor = (component.borderColor || "#374151").replace(
              "#",
              ""
            );

            // 숫자 포맷팅 함수
            const formatNumberFn = (num: number): string => {
              if (numberFormat === "none") return String(num);
              if (numberFormat === "comma") return num.toLocaleString();
              if (numberFormat === "currency")
                return num.toLocaleString() + currencySuffix;
              return String(num);
            };

            // 쿼리 바인딩된 값 가져오기
            const getCalcItemValueFn = (item: {
              label: string;
              value: number | string;
              operator: string;
              fieldName?: string;
            }): number => {
              if (
                item.fieldName &&
                component.queryId &&
                queryResultsMap[component.queryId]
              ) {
                const qResult = queryResultsMap[component.queryId];
                if (qResult.rows && qResult.rows.length > 0) {
                  const row = qResult.rows[0];
                  const val = row[item.fieldName];
                  return typeof val === "number"
                    ? val
                    : parseFloat(String(val)) || 0;
                }
              }
              return typeof item.value === "number"
                ? item.value
                : parseFloat(String(item.value)) || 0;
            };

            // 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
            let calcResult = 0;
            if (calcItems.length > 0) {
              // 첫 번째 항목은 기준값
              calcResult = getCalcItemValueFn(
                calcItems[0] as {
                  label: string;
                  value: number | string;
                  operator: string;
                  fieldName?: string;
                }
              );

              // 두 번째 항목부터 연산자 적용
              for (let i = 1; i < calcItems.length; i++) {
                const calcItem = calcItems[i];
                const val = getCalcItemValueFn(
                  calcItem as {
                    label: string;
                    value: number | string;
                    operator: string;
                    fieldName?: string;
                  }
                );
                switch ((calcItem as { operator: string }).operator) {
                  case "+":
                    calcResult += val;
                    break;
                  case "-":
                    calcResult -= val;
                    break;
                  case "x":
                    calcResult *= val;
                    break;
                  case "÷":
                    calcResult = val !== 0 ? calcResult / val : calcResult;
                    break;
                }
              }
            }

            // 테이블 행 생성
            const calcTableRows: TableRow[] = [];

            // 각 항목 행
            for (const calcItem of calcItems) {
              const itemValue = getCalcItemValueFn(
                calcItem as {
                  label: string;
                  value: number | string;
                  operator: string;
                  fieldName?: string;
                }
              );
              calcTableRows.push(
                new TableRow({
                  children: [
                    new TableCell({
                      children: [
                        new Paragraph({
                          children: [
                            new TextRun({
                              text: calcItem.label,
                              size: calcLabelFontSize,
                              color: calcLabelColor,
                              font: "맑은 고딕",
                            }),
                          ],
                        }),
                      ],
                      width: {
                        size: pxToTwip(calcLabelWidth),
                        type: WidthType.DXA,
                      },
                      borders: {
                        top: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        bottom: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        left: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        right: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                      },
                      margins: { top: 50, bottom: 50, left: 100, right: 100 },
                    }),
                    new TableCell({
                      children: [
                        new Paragraph({
                          alignment: AlignmentType.RIGHT,
                          children: [
                            new TextRun({
                              text: formatNumberFn(itemValue),
                              size: calcValueFontSize,
                              color: calcValueColor,
                              font: "맑은 고딕",
                            }),
                          ],
                        }),
                      ],
                      borders: {
                        top: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        bottom: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        left: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                        right: {
                          style: BorderStyle.NONE,
                          size: 0,
                          color: "FFFFFF",
                        },
                      },
                      margins: { top: 50, bottom: 50, left: 100, right: 100 },
                    }),
                  ],
                })
              );
            }

            // 구분선 행
            calcTableRows.push(
              new TableRow({
                children: [
                  new TableCell({
                    columnSpan: 2,
                    children: [new Paragraph({ children: [] })],
                    borders: {
                      top: {
                        style: BorderStyle.SINGLE,
                        size: 8,
                        color: borderColor,
                      },
                      bottom: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
                  }),
                ],
              })
            );

            // 결과 행
            calcTableRows.push(
              new TableRow({
                children: [
                  new TableCell({
                    children: [
                      new Paragraph({
                        children: [
                          new TextRun({
                            text: resultLabel,
                            size: calcResultFontSize,
                            color: calcLabelColor,
                            bold: true,
                            font: "맑은 고딕",
                          }),
                        ],
                      }),
                    ],
                    width: {
                      size: pxToTwip(calcLabelWidth),
                      type: WidthType.DXA,
                    },
                    borders: {
                      top: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      bottom: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
                    margins: { top: 50, bottom: 50, left: 100, right: 100 },
                  }),
                  new TableCell({
                    children: [
                      new Paragraph({
                        alignment: AlignmentType.RIGHT,
                        children: [
                          new TextRun({
                            text: formatNumberFn(calcResult),
                            size: calcResultFontSize,
                            color: calcResultColor,
                            bold: true,
                            font: "맑은 고딕",
                          }),
                        ],
                      }),
                    ],
                    borders: {
                      top: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      bottom: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      left: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                      right: {
                        style: BorderStyle.NONE,
                        size: 0,
                        color: "FFFFFF",
                      },
                    },
                    margins: { top: 50, bottom: 50, left: 100, right: 100 },
                  }),
                ],
              })
            );

            const calcTable = new Table({
              rows: calcTableRows,
              width: { size: pxToTwip(component.width), type: WidthType.DXA },
              indent: { size: indentLeft, type: WidthType.DXA },
              borders: {
                top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
                insideHorizontal: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
                insideVertical: {
                  style: BorderStyle.NONE,
                  size: 0,
                  color: "FFFFFF",
                },
              },
            });

            // spacing을 위한 빈 paragraph
            if (spacingBefore > 0) {
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  children: [],
                })
              );
            }
            children.push(calcTable);
            lastBottomY = adjustedY + component.height;
          }

          // Barcode 컴포넌트
          else if (component.type === "barcode") {
            if (component.barcodeImageBase64) {
              try {
                const base64Data =
                  component.barcodeImageBase64.split(",")[1] || component.barcodeImageBase64;
                const imageBuffer = Buffer.from(base64Data, "base64");

                // spacing을 위한 빈 paragraph
                if (spacingBefore > 0) {
                  children.push(
                    new Paragraph({
                      spacing: { before: spacingBefore, after: 0 },
                      children: [],
                    })
                  );
                }

                children.push(
                  new Paragraph({
                    indent: { left: indentLeft },
                    children: [
                      new ImageRun({
                        data: imageBuffer,
                        transformation: {
                          width: Math.round(component.width * 0.75),
                          height: Math.round(component.height * 0.75),
                        },
                        type: "png",
                      }),
                    ],
                  })
                );
              } catch (imgError) {
                console.error("바코드 이미지 오류:", imgError);
                // 바코드 이미지 생성 실패 시 텍스트로 대체
                const barcodeValue = component.barcodeValue || "BARCODE";
                children.push(
                  new Paragraph({
                    spacing: { before: spacingBefore, after: 0 },
                    indent: { left: indentLeft },
                    children: [
                      new TextRun({
                        text: `[${barcodeValue}]`,
                        size: pxToHalfPt(12),
                        font: "맑은 고딕",
                      }),
                    ],
                  })
                );
              }
            } else {
              // 바코드 이미지가 없는 경우 텍스트로 대체
              const barcodeValue = component.barcodeValue || "BARCODE";
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  indent: { left: indentLeft },
                  children: [
                    new TextRun({
                      text: `[${barcodeValue}]`,
                      size: pxToHalfPt(12),
                      font: "맑은 고딕",
                    }),
                  ],
                })
              );
            }
            lastBottomY = adjustedY + component.height;
          }

          // Checkbox 컴포넌트
          else if (component.type === "checkbox") {
            // 체크 상태 결정 (쿼리 바인딩 또는 고정값)
            let isChecked = component.checkboxChecked === true;
            if (component.checkboxFieldName && component.queryId && queryResultsMap[component.queryId]) {
              const qResult = queryResultsMap[component.queryId];
              if (qResult.rows && qResult.rows.length > 0) {
                const row = qResult.rows[0];
                const val = row[component.checkboxFieldName];
                // truthy/falsy 값 판정
                if (val === true || val === "true" || val === "Y" || val === 1 || val === "1") {
                  isChecked = true;
                } else {
                  isChecked = false;
                }
              }
            }
            
            const checkboxSymbol = isChecked ? "☑" : "☐";
            const checkboxLabel = component.checkboxLabel || "";
            const labelPosition = component.checkboxLabelPosition || "right";
            const displayText = labelPosition === "left" 
              ? `${checkboxLabel} ${checkboxSymbol}` 
              : `${checkboxSymbol} ${checkboxLabel}`;
            
            // spacing을 위한 빈 paragraph
            if (spacingBefore > 0) {
              children.push(
                new Paragraph({
                  spacing: { before: spacingBefore, after: 0 },
                  children: [],
                })
              );
            }
            
            children.push(
              new Paragraph({
                indent: { left: indentLeft },
                children: [
                  new TextRun({
                    text: displayText.trim(),
                    size: pxToHalfPt(component.fontSize || 14),
                    font: "맑은 고딕",
                    color: (component.fontColor || "#374151").replace("#", ""),
                  }),
                ],
              })
            );
            
            lastBottomY = adjustedY + component.height;
          }

          // Table 컴포넌트
          else if (component.type === "table" && component.queryId) {
            const queryResult = queryResultsMap[component.queryId];
            if (
              queryResult &&
              queryResult.rows &&
              queryResult.rows.length > 0
            ) {
              // 테이블 앞에 spacing과 indent를 위한 빈 paragraph 추가
              if (spacingBefore > 0 || indentLeft > 0) {
                children.push(
                  new Paragraph({
                    spacing: { before: spacingBefore, after: 0 },
                    indent: { left: indentLeft },
                    children: [],
                  })
                );
              }

              const columns =
                component.tableColumns && component.tableColumns.length > 0
                  ? component.tableColumns
                  : queryResult.fields.map((field: string) => ({
                      field,
                      header: field,
                      align: "left",
                      width: undefined,
                    }));

              // 테이블 폰트 사이즈 (기본 12px)
              const tableFontSize = pxToHalfPt(component.fontSize || 12);

              // 헤더 행
              const headerCells = columns.map(
                (col: { header: string; align?: string }) =>
                  new TableCell({
                    children: [
                      new Paragraph({
                        alignment:
                          col.align === "center"
                            ? AlignmentType.CENTER
                            : col.align === "right"
                              ? AlignmentType.RIGHT
                              : AlignmentType.LEFT,
                        children: [
                          new TextRun({
                            text: col.header,
                            bold: true,
                            size: tableFontSize,
                            font: "맑은 고딕",
                          }),
                        ],
                      }),
                    ],
                    shading: {
                      fill: (
                        component.headerBackgroundColor || "#f3f4f6"
                      ).replace("#", ""),
                    },
                    verticalAlign: VerticalAlign.CENTER,
                  })
              );
              const headerRow = new TableRow({ children: headerCells });

              // 데이터 행
              const dataRows = queryResult.rows.map(
                (row: Record<string, unknown>) =>
                  new TableRow({
                    children: columns.map(
                      (col: { field: string; align?: string }) =>
                        new TableCell({
                          children: [
                            new Paragraph({
                              alignment:
                                col.align === "center"
                                  ? AlignmentType.CENTER
                                  : col.align === "right"
                                    ? AlignmentType.RIGHT
                                    : AlignmentType.LEFT,
                              children: [
                                new TextRun({
                                  text: String(row[col.field] ?? ""),
                                  size: tableFontSize,
                                  font: "맑은 고딕",
                                }),
                              ],
                            }),
                          ],
                          verticalAlign: VerticalAlign.CENTER,
                        })
                    ),
                  })
              );

              const table = new Table({
                width: { size: pxToTwip(component.width), type: WidthType.DXA },
                indent: { size: indentLeft, type: WidthType.DXA },
                rows: [headerRow, ...dataRows],
              });
              children.push(table);
              lastBottomY = adjustedY + component.height;
            }
          }
        }

        // 빈 페이지 방지
        if (children.length === 0) {
          children.push(new Paragraph({ children: [] }));
        }

        // 워터마크 헤더 생성 (전체 페이지 공유 워터마크)
        const watermark: WatermarkConfig | undefined = layoutConfig.watermark;
        let headers: { default?: Header } | undefined;

        if (watermark?.enabled && watermark.type === "text" && watermark.text) {
          // 워터마크 색상을 hex로 변환 (alpha 적용)
          const opacity = watermark.opacity ?? 0.3;
          const fontColor = watermark.fontColor || "#CCCCCC";
          // hex 색상에서 # 제거
          const cleanColor = fontColor.replace("#", "");

          headers = {
            default: new Header({
              children: [
                new Paragraph({
                  alignment: AlignmentType.CENTER,
                  children: [
                    new TextRun({
                      text: watermark.text,
                      size: (watermark.fontSize || 48) * 2, // Word는 half-point 사용
                      color: cleanColor,
                      bold: true,
                    }),
                  ],
                }),
              ],
            }),
          };
        }

        return {
          properties: {
            page: {
              size: {
                width: pageWidthTwip,
                height: pageHeightTwip,
                orientation:
                  page.width > page.height
                    ? PageOrientation.LANDSCAPE
                    : PageOrientation.PORTRAIT,
              },
              margin: {
                top: marginTop,
                bottom: marginBottom,
                left: marginLeft,
                right: marginRight,
              },
            },
          },
          headers,
          children,
        };
      });

      // Document 생성
      const doc = new Document({
        sections,
      });

      // Buffer로 변환
      const docxBuffer = await Packer.toBuffer(doc);

      // 파일명 인코딩 (한글 지원)
      const timestamp = new Date().toISOString().slice(0, 10);
      const safeFileName = encodeURIComponent(`${fileName}_${timestamp}.docx`);

      // DOCX 파일로 응답
      res.setHeader(
        "Content-Type",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
      );
      res.setHeader(
        "Content-Disposition",
        `attachment; filename*=UTF-8''${safeFileName}`
      );
      res.setHeader("Content-Length", docxBuffer.length);

      return res.send(docxBuffer);
    } catch (error: any) {
      console.error("WORD 변환 오류:", error);
      return res.status(500).json({
        success: false,
        message: error.message || "WORD 변환에 실패했습니다.",
      });
    }
  }
}

export default new ReportController();
