체크박스 컴포넌트 추가

This commit is contained in:
dohyeons
2025-12-19 18:06:25 +09:00
parent ea01309158
commit 8d34b73a45
7 changed files with 365 additions and 3 deletions

View File

@@ -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];

View File

@@ -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"; // 레이블 위치
}

View File

@@ -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>;
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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">

View File

@@ -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"; // 레이블 위치
}
// 리포트 상세