체크박스 컴포넌트 추가
This commit is contained in:
parent
ea01309158
commit
8d34b73a45
|
|
@ -1364,6 +1364,45 @@ export class ReportController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 - 테이블 셀로 감싸서 정확한 너비 적용
|
// Divider - 테이블 셀로 감싸서 정확한 너비 적용
|
||||||
else if (
|
else if (
|
||||||
component.type === "divider" &&
|
component.type === "divider" &&
|
||||||
|
|
@ -2809,6 +2848,58 @@ export class ReportController {
|
||||||
lastBottomY = adjustedY + component.height;
|
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 컴포넌트
|
// Table 컴포넌트
|
||||||
else if (component.type === "table" && component.queryId) {
|
else if (component.type === "table" && component.queryId) {
|
||||||
const queryResult = queryResultsMap[component.queryId];
|
const queryResult = queryResultsMap[component.queryId];
|
||||||
|
|
|
||||||
|
|
@ -260,4 +260,12 @@ export interface ComponentConfig {
|
||||||
barcodeBackground?: string;
|
barcodeBackground?: string;
|
||||||
barcodeMargin?: number;
|
barcodeMargin?: number;
|
||||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H";
|
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H";
|
||||||
|
// 체크박스 컴포넌트 전용
|
||||||
|
checkboxChecked?: boolean; // 체크 상태 (고정값)
|
||||||
|
checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값)
|
||||||
|
checkboxLabel?: string; // 체크박스 옆 레이블 텍스트
|
||||||
|
checkboxSize?: number; // 체크박스 크기 (px)
|
||||||
|
checkboxColor?: string; // 체크 색상
|
||||||
|
checkboxBorderColor?: string; // 테두리 색상
|
||||||
|
checkboxLabelPosition?: "left" | "right"; // 레이블 위치
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1002,6 +1002,89 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
// 체크박스 컴포넌트 렌더링
|
||||||
|
const checkboxSize = component.checkboxSize || 18;
|
||||||
|
const checkboxColor = component.checkboxColor || "#2563eb";
|
||||||
|
const checkboxBorderColor = component.checkboxBorderColor || "#6b7280";
|
||||||
|
const checkboxLabelPosition = component.checkboxLabelPosition || "right";
|
||||||
|
const checkboxLabel = component.checkboxLabel || "";
|
||||||
|
|
||||||
|
// 체크 상태 결정 (쿼리 바인딩 또는 고정값)
|
||||||
|
const getCheckboxValue = (): boolean => {
|
||||||
|
if (component.checkboxFieldName && component.queryId) {
|
||||||
|
const queryResult = getQueryResult(component.queryId);
|
||||||
|
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||||
|
const row = queryResult.rows[0];
|
||||||
|
const val = row[component.checkboxFieldName];
|
||||||
|
// truthy/falsy 값 판정
|
||||||
|
if (val === true || val === "true" || val === "Y" || val === 1 || val === "1") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return component.checkboxChecked === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChecked = getCheckboxValue();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col">
|
||||||
|
<div className="mb-1 flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<span>체크박스</span>
|
||||||
|
{component.checkboxFieldName && component.queryId && (
|
||||||
|
<span className="text-blue-600">● 연결됨</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex flex-1 items-center gap-2 ${
|
||||||
|
checkboxLabelPosition === "left" ? "flex-row-reverse justify-end" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* 체크박스 */}
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center rounded-sm border-2 transition-colors"
|
||||||
|
style={{
|
||||||
|
width: `${checkboxSize}px`,
|
||||||
|
height: `${checkboxSize}px`,
|
||||||
|
borderColor: isChecked ? checkboxColor : checkboxBorderColor,
|
||||||
|
backgroundColor: isChecked ? checkboxColor : "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isChecked && (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{
|
||||||
|
width: `${checkboxSize * 0.7}px`,
|
||||||
|
height: `${checkboxSize * 0.7}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* 레이블 */}
|
||||||
|
{checkboxLabel && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: `${component.fontSize || 14}px`,
|
||||||
|
color: component.fontColor || "#374151",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{checkboxLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <div>알 수 없는 컴포넌트</div>;
|
return <div>알 수 없는 컴포넌트</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useDrag } from "react-dnd";
|
import { useDrag } from "react-dnd";
|
||||||
import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator, Barcode } from "lucide-react";
|
import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator, Barcode, CheckSquare } from "lucide-react";
|
||||||
|
|
||||||
interface ComponentItem {
|
interface ComponentItem {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -20,6 +20,7 @@ const COMPONENTS: ComponentItem[] = [
|
||||||
{ type: "card", label: "정보카드", icon: <CreditCard className="h-4 w-4" /> },
|
{ type: "card", label: "정보카드", icon: <CreditCard className="h-4 w-4" /> },
|
||||||
{ type: "calculation", label: "계산", icon: <Calculator className="h-4 w-4" /> },
|
{ type: "calculation", label: "계산", icon: <Calculator className="h-4 w-4" /> },
|
||||||
{ type: "barcode", label: "바코드/QR", icon: <Barcode className="h-4 w-4" /> },
|
{ type: "barcode", label: "바코드/QR", icon: <Barcode className="h-4 w-4" /> },
|
||||||
|
{ type: "checkbox", label: "체크박스", icon: <CheckSquare className="h-4 w-4" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
function DraggableComponentItem({ type, label, icon }: ComponentItem) {
|
function DraggableComponentItem({ type, label, icon }: ComponentItem) {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@ export function ReportDesignerCanvas() {
|
||||||
} else if (item.componentType === "barcode") {
|
} else if (item.componentType === "barcode") {
|
||||||
width = 200;
|
width = 200;
|
||||||
height = 80;
|
height = 80;
|
||||||
|
} else if (item.componentType === "checkbox") {
|
||||||
|
width = 150;
|
||||||
|
height = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 여백을 px로 변환 (1mm ≈ 3.7795px)
|
// 여백을 px로 변환 (1mm ≈ 3.7795px)
|
||||||
|
|
@ -218,6 +221,15 @@ export function ReportDesignerCanvas() {
|
||||||
barcodeMargin: 10,
|
barcodeMargin: 10,
|
||||||
qrErrorCorrectionLevel: "M" as const,
|
qrErrorCorrectionLevel: "M" as const,
|
||||||
}),
|
}),
|
||||||
|
// 체크박스 컴포넌트 전용
|
||||||
|
...(item.componentType === "checkbox" && {
|
||||||
|
checkboxChecked: false,
|
||||||
|
checkboxLabel: "항목",
|
||||||
|
checkboxSize: 18,
|
||||||
|
checkboxColor: "#2563eb",
|
||||||
|
checkboxBorderColor: "#6b7280",
|
||||||
|
checkboxLabelPosition: "right" as const,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
addComponent(newComponent);
|
addComponent(newComponent);
|
||||||
|
|
|
||||||
|
|
@ -1834,11 +1834,170 @@ export function ReportDesignerRightPanel() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 데이터 바인딩 (텍스트/라벨/테이블/바코드 컴포넌트) */}
|
{/* 체크박스 컴포넌트 전용 설정 */}
|
||||||
|
{selectedComponent.type === "checkbox" && (
|
||||||
|
<Card className="mt-4 border-purple-200 bg-purple-50">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm text-purple-900">체크박스 설정</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{/* 체크 상태 (쿼리 연결 없을 때) */}
|
||||||
|
{!selectedComponent.queryId && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="checkboxChecked"
|
||||||
|
checked={selectedComponent.checkboxChecked === true}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxChecked: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-4 w-4 rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="checkboxChecked" className="text-xs">
|
||||||
|
체크됨
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 쿼리 연결 시 필드 선택 */}
|
||||||
|
{selectedComponent.queryId && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">체크 상태 바인딩 필드</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedComponent.checkboxFieldName || "none"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxFieldName: value === "none" ? "" : value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8">
|
||||||
|
<SelectValue placeholder="필드 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">선택 안함</SelectItem>
|
||||||
|
{(() => {
|
||||||
|
const query = queries.find((q) => q.id === selectedComponent.queryId);
|
||||||
|
const result = query ? getQueryResult(query.id) : null;
|
||||||
|
if (result && result.fields) {
|
||||||
|
return result.fields.map((field: string) => (
|
||||||
|
<SelectItem key={field} value={field}>
|
||||||
|
{field}
|
||||||
|
</SelectItem>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1 text-[10px] text-gray-500">
|
||||||
|
true, "Y", 1 등 truthy 값이면 체크됨
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 레이블 텍스트 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">레이블 텍스트</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={selectedComponent.checkboxLabel || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxLabel: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="체크박스 옆 텍스트"
|
||||||
|
className="h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 레이블 위치 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">레이블 위치</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedComponent.checkboxLabelPosition || "right"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxLabelPosition: value as "left" | "right",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">왼쪽</SelectItem>
|
||||||
|
<SelectItem value="right">오른쪽</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 체크박스 크기 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">체크박스 크기 (px)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={selectedComponent.checkboxSize || 18}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxSize: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
min={12}
|
||||||
|
max={40}
|
||||||
|
className="h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 색상 설정 */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">체크 색상</Label>
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={selectedComponent.checkboxColor || "#2563eb"}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxColor: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-8 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">테두리 색상</Label>
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={selectedComponent.checkboxBorderColor || "#6b7280"}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateComponent(selectedComponent.id, {
|
||||||
|
checkboxBorderColor: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-8 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 쿼리 연결 안내 */}
|
||||||
|
{!selectedComponent.queryId && (
|
||||||
|
<div className="rounded border border-purple-200 bg-purple-100 p-2 text-xs text-purple-800">
|
||||||
|
쿼리를 연결하면 데이터베이스 값으로 체크 상태를 결정할 수 있습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 데이터 바인딩 (텍스트/라벨/테이블/바코드/체크박스 컴포넌트) */}
|
||||||
{(selectedComponent.type === "text" ||
|
{(selectedComponent.type === "text" ||
|
||||||
selectedComponent.type === "label" ||
|
selectedComponent.type === "label" ||
|
||||||
selectedComponent.type === "table" ||
|
selectedComponent.type === "table" ||
|
||||||
selectedComponent.type === "barcode") && (
|
selectedComponent.type === "barcode" ||
|
||||||
|
selectedComponent.type === "checkbox") && (
|
||||||
<Card className="mt-4 border-blue-200 bg-blue-50">
|
<Card className="mt-4 border-blue-200 bg-blue-50">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,14 @@ export interface ComponentConfig {
|
||||||
barcodeBackground?: string; // 배경 색상
|
barcodeBackground?: string; // 배경 색상
|
||||||
barcodeMargin?: number; // 여백
|
barcodeMargin?: number; // 여백
|
||||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; // QR 오류 보정 수준
|
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; // QR 오류 보정 수준
|
||||||
|
// 체크박스 컴포넌트 전용
|
||||||
|
checkboxChecked?: boolean; // 체크 상태 (고정값)
|
||||||
|
checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값)
|
||||||
|
checkboxLabel?: string; // 체크박스 옆 레이블 텍스트
|
||||||
|
checkboxSize?: number; // 체크박스 크기 (px)
|
||||||
|
checkboxColor?: string; // 체크 색상
|
||||||
|
checkboxBorderColor?: string; // 테두리 색상
|
||||||
|
checkboxLabelPosition?: "left" | "right"; // 레이블 위치
|
||||||
}
|
}
|
||||||
|
|
||||||
// 리포트 상세
|
// 리포트 상세
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue