체크박스 컴포넌트 추가
This commit is contained in:
@@ -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 - 테이블 셀로 감싸서 정확한 너비 적용
|
||||
else if (
|
||||
component.type === "divider" &&
|
||||
@@ -2809,6 +2848,58 @@ export class ReportController {
|
||||
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];
|
||||
|
||||
@@ -260,4 +260,12 @@ export interface ComponentConfig {
|
||||
barcodeBackground?: string;
|
||||
barcodeMargin?: number;
|
||||
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>
|
||||
);
|
||||
|
||||
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:
|
||||
return <div>알 수 없는 컴포넌트</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 {
|
||||
type: string;
|
||||
@@ -20,6 +20,7 @@ const COMPONENTS: ComponentItem[] = [
|
||||
{ type: "card", label: "정보카드", icon: <CreditCard 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: "checkbox", label: "체크박스", icon: <CheckSquare className="h-4 w-4" /> },
|
||||
];
|
||||
|
||||
function DraggableComponentItem({ type, label, icon }: ComponentItem) {
|
||||
|
||||
@@ -71,6 +71,9 @@ export function ReportDesignerCanvas() {
|
||||
} else if (item.componentType === "barcode") {
|
||||
width = 200;
|
||||
height = 80;
|
||||
} else if (item.componentType === "checkbox") {
|
||||
width = 150;
|
||||
height = 30;
|
||||
}
|
||||
|
||||
// 여백을 px로 변환 (1mm ≈ 3.7795px)
|
||||
@@ -218,6 +221,15 @@ export function ReportDesignerCanvas() {
|
||||
barcodeMargin: 10,
|
||||
qrErrorCorrectionLevel: "M" as const,
|
||||
}),
|
||||
// 체크박스 컴포넌트 전용
|
||||
...(item.componentType === "checkbox" && {
|
||||
checkboxChecked: false,
|
||||
checkboxLabel: "항목",
|
||||
checkboxSize: 18,
|
||||
checkboxColor: "#2563eb",
|
||||
checkboxBorderColor: "#6b7280",
|
||||
checkboxLabelPosition: "right" as const,
|
||||
}),
|
||||
};
|
||||
|
||||
addComponent(newComponent);
|
||||
|
||||
@@ -1834,11 +1834,170 @@ export function ReportDesignerRightPanel() {
|
||||
</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 === "label" ||
|
||||
selectedComponent.type === "table" ||
|
||||
selectedComponent.type === "barcode") && (
|
||||
selectedComponent.type === "barcode" ||
|
||||
selectedComponent.type === "checkbox") && (
|
||||
<Card className="mt-4 border-blue-200 bg-blue-50">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -198,6 +198,14 @@ export interface ComponentConfig {
|
||||
barcodeBackground?: string; // 배경 색상
|
||||
barcodeMargin?: number; // 여백
|
||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H"; // QR 오류 보정 수준
|
||||
// 체크박스 컴포넌트 전용
|
||||
checkboxChecked?: boolean; // 체크 상태 (고정값)
|
||||
checkboxFieldName?: string; // 쿼리 필드 바인딩 (truthy/falsy 값)
|
||||
checkboxLabel?: string; // 체크박스 옆 레이블 텍스트
|
||||
checkboxSize?: number; // 체크박스 크기 (px)
|
||||
checkboxColor?: string; // 체크 색상
|
||||
checkboxBorderColor?: string; // 테두리 색상
|
||||
checkboxLabelPosition?: "left" | "right"; // 레이블 위치
|
||||
}
|
||||
|
||||
// 리포트 상세
|
||||
|
||||
Reference in New Issue
Block a user