mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-16 04:48:32 +05:30
Merge pull request #8 from minseonju/feat/qr-code-generator
Feat/qr code generator
This commit is contained in:
commit
5949982bae
11 changed files with 199 additions and 10 deletions
|
|
@ -44,6 +44,7 @@
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"jimp": "^0.22.12",
|
"jimp": "^0.22.12",
|
||||||
"js-quantities": "^1.8.0",
|
"js-quantities": "^1.8.0",
|
||||||
"lint-staged": "^15.4.3",
|
"lint-staged": "^15.4.3",
|
||||||
|
|
@ -55,11 +56,13 @@
|
||||||
"omggif": "^1.0.10",
|
"omggif": "^1.0.10",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"playwright": "^1.45.0",
|
"playwright": "^1.45.0",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"rc-slider": "^11.1.8",
|
"rc-slider": "^11.1.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-image-crop": "^11.0.7",
|
"react-image-crop": "^11.0.7",
|
||||||
|
"react-qr-code": "^2.0.15",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"tesseract.js": "^6.0.0",
|
"tesseract.js": "^6.0.0",
|
||||||
"type-fest": "^4.35.0",
|
"type-fest": "^4.35.0",
|
||||||
|
|
|
||||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
|
|
@ -65,6 +65,9 @@ importers:
|
||||||
formik:
|
formik:
|
||||||
specifier: ^2.4.6
|
specifier: ^2.4.6
|
||||||
version: 2.4.6(react@18.3.1)
|
version: 2.4.6(react@18.3.1)
|
||||||
|
html-to-image:
|
||||||
|
specifier: ^1.11.13
|
||||||
|
version: 1.11.13
|
||||||
jimp:
|
jimp:
|
||||||
specifier: ^0.22.12
|
specifier: ^0.22.12
|
||||||
version: 0.22.12
|
version: 0.22.12
|
||||||
|
|
@ -98,6 +101,9 @@ importers:
|
||||||
playwright:
|
playwright:
|
||||||
specifier: ^1.45.0
|
specifier: ^1.45.0
|
||||||
version: 1.51.1
|
version: 1.51.1
|
||||||
|
qrcode.react:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0(react@18.3.1)
|
||||||
rc-slider:
|
rc-slider:
|
||||||
specifier: ^11.1.8
|
specifier: ^11.1.8
|
||||||
version: 11.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 11.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
@ -113,6 +119,9 @@ importers:
|
||||||
react-image-crop:
|
react-image-crop:
|
||||||
specifier: ^11.0.7
|
specifier: ^11.0.7
|
||||||
version: 11.0.7(react@18.3.1)
|
version: 11.0.7(react@18.3.1)
|
||||||
|
react-qr-code:
|
||||||
|
specifier: ^2.0.15
|
||||||
|
version: 2.0.15(react@18.3.1)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.23.1
|
specifier: ^6.23.1
|
||||||
version: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
@ -2247,6 +2256,9 @@ packages:
|
||||||
hoist-non-react-statics@3.3.2:
|
hoist-non-react-statics@3.3.2:
|
||||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
|
|
||||||
|
html-to-image@1.11.13:
|
||||||
|
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||||
|
|
||||||
human-signals@2.1.0:
|
human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
engines: {node: '>=10.17.0'}
|
engines: {node: '>=10.17.0'}
|
||||||
|
|
@ -3098,6 +3110,14 @@ packages:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
qr.js@0.0.0:
|
||||||
|
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||||
|
|
||||||
|
qrcode.react@4.2.0:
|
||||||
|
resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
|
@ -3147,6 +3167,11 @@ packages:
|
||||||
react-is@19.1.0:
|
react-is@19.1.0:
|
||||||
resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==}
|
resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==}
|
||||||
|
|
||||||
|
react-qr-code@2.0.15:
|
||||||
|
resolution: {integrity: sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
|
||||||
react-router-dom@6.30.0:
|
react-router-dom@6.30.0:
|
||||||
resolution: {integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==}
|
resolution: {integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
@ -6098,6 +6123,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
|
|
||||||
|
html-to-image@1.11.13: {}
|
||||||
|
|
||||||
human-signals@2.1.0: {}
|
human-signals@2.1.0: {}
|
||||||
|
|
||||||
human-signals@5.0.0: {}
|
human-signals@5.0.0: {}
|
||||||
|
|
@ -6886,6 +6913,12 @@ snapshots:
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
|
qr.js@0.0.0: {}
|
||||||
|
|
||||||
|
qrcode.react@4.2.0(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
rc-slider@11.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
rc-slider@11.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
|
@ -6933,6 +6966,12 @@ snapshots:
|
||||||
|
|
||||||
react-is@19.1.0: {}
|
react-is@19.1.0: {}
|
||||||
|
|
||||||
|
react-qr-code@2.0.15(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
prop-types: 15.8.1
|
||||||
|
qr.js: 0.0.0
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@remix-run/router': 1.23.0
|
'@remix-run/router': 1.23.0
|
||||||
|
|
|
||||||
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@swc/core'
|
||||||
|
- esbuild
|
||||||
|
- protobufjs
|
||||||
|
- tesseract.js
|
||||||
|
|
@ -23,7 +23,7 @@ const FormikListenerComponent = <T,>({
|
||||||
const { values } = useFormikContext<T>();
|
const { values } = useFormikContext<T>();
|
||||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
compute(values, input);
|
compute(values, input);
|
||||||
} catch (exception: unknown) {
|
} catch (exception: unknown) {
|
||||||
|
|
@ -35,7 +35,8 @@ const FormikListenerComponent = <T,>({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onValuesChange?.(values);
|
onValuesChange?.(values);
|
||||||
}, [onValuesChange, values]);
|
}, [onValuesChange, values]);
|
||||||
return null; // This component doesn't render anything
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ToolContentProps<T, I> extends ToolComponentProps {
|
interface ToolContentProps<T, I> extends ToolComponentProps {
|
||||||
|
|
@ -99,7 +100,11 @@ export default function ToolContent<T extends FormikValues, I>({
|
||||||
input={input}
|
input={input}
|
||||||
onValuesChange={onValuesChange}
|
onValuesChange={onValuesChange}
|
||||||
/>
|
/>
|
||||||
<ToolOptions getGroups={getGroups} vertical={verticalGroups} />
|
|
||||||
|
{/* meta 파일의 name 속성이 QR 코드 생성기일 때만 ToolOptions 숨기기 */}
|
||||||
|
{title !== 'QR 코드 생성기' && (
|
||||||
|
<ToolOptions getGroups={getGroups} vertical={verticalGroups} />
|
||||||
|
)}
|
||||||
|
|
||||||
{toolInfo && toolInfo.title && toolInfo.description && (
|
{toolInfo && toolInfo.title && toolInfo.description && (
|
||||||
<ToolInfo
|
<ToolInfo
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,18 @@ export default function InputFooter({
|
||||||
handleCopy,
|
handleCopy,
|
||||||
handleClear
|
handleClear
|
||||||
}: {
|
}: {
|
||||||
handleImport: () => void;
|
handleImport?: () => void;
|
||||||
handleCopy?: () => void;
|
handleCopy?: () => void;
|
||||||
handleClear?: () => void;
|
handleClear?: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Stack mt={1} direction={'row'} spacing={2}>
|
<Stack mt={1} direction={'row'} spacing={2}>
|
||||||
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
{handleImport && (
|
||||||
Import from file
|
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
||||||
</Button>
|
Import from file
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{handleCopy && (
|
{handleCopy && (
|
||||||
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
||||||
Copy to clipboard
|
Copy to clipboard
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ import InputFooter from './InputFooter';
|
||||||
export default function ToolTextInput({
|
export default function ToolTextInput({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
title = 'Input text'
|
title = 'Input text',
|
||||||
|
hideFileImport = false
|
||||||
}: {
|
}: {
|
||||||
title?: string;
|
title?: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
hideFileImport?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -24,6 +26,7 @@ export default function ToolTextInput({
|
||||||
showSnackBar('Failed to copy: ' + err, 'error');
|
showSnackBar('Failed to copy: ' + err, 'error');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
|
@ -41,6 +44,7 @@ export default function ToolTextInput({
|
||||||
const handleImportClick = () => {
|
const handleImportClick = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<InputHeader title={title} />
|
<InputHeader title={title} />
|
||||||
|
|
@ -59,7 +63,12 @@ export default function ToolTextInput({
|
||||||
'data-testid': 'text-input'
|
'data-testid': 'text-input'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
|
||||||
|
<InputFooter
|
||||||
|
handleCopy={handleCopy}
|
||||||
|
handleImport={hideFileImport ? undefined : handleImportClick}
|
||||||
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="*"
|
accept="*"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { tool as stringJoin } from './join/meta';
|
||||||
import { tool as stringReplace } from './text-replacer/meta';
|
import { tool as stringReplace } from './text-replacer/meta';
|
||||||
import { tool as stringRepeat } from './repeat/meta';
|
import { tool as stringRepeat } from './repeat/meta';
|
||||||
import { tool as stringTruncate } from './truncate/meta';
|
import { tool as stringTruncate } from './truncate/meta';
|
||||||
|
import { tool as stringQrGenerator } from './qr-generator/meta';
|
||||||
|
|
||||||
export const stringTools = [
|
export const stringTools = [
|
||||||
stringSplit,
|
stringSplit,
|
||||||
|
|
@ -31,5 +32,6 @@ export const stringTools = [
|
||||||
stringPalindrome,
|
stringPalindrome,
|
||||||
stringQuote,
|
stringQuote,
|
||||||
stringRotate,
|
stringRotate,
|
||||||
stringRot13
|
stringRot13,
|
||||||
|
stringQrGenerator
|
||||||
];
|
];
|
||||||
|
|
|
||||||
106
src/pages/tools/string/qr-generator/index.tsx
Normal file
106
src/pages/tools/string/qr-generator/index.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import ToolTextInput from '@components/input/ToolTextInput';
|
||||||
|
import ToolContent from '@components/ToolContent';
|
||||||
|
import QRCode from 'react-qr-code';
|
||||||
|
import { toPng } from 'html-to-image';
|
||||||
|
import type { ToolComponentProps } from '@tools/defineTool';
|
||||||
|
|
||||||
|
const initialValues = {};
|
||||||
|
|
||||||
|
export default function QRGeneratorTool({
|
||||||
|
title,
|
||||||
|
longDescription
|
||||||
|
}: ToolComponentProps) {
|
||||||
|
const [input, setInput] = useState<string>('');
|
||||||
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!qrRef.current) return;
|
||||||
|
try {
|
||||||
|
const dataUrl = await toPng(qrRef.current);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = 'qr-code.png';
|
||||||
|
a.click();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('QR 다운로드 실패:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolContent
|
||||||
|
title={title}
|
||||||
|
initialValues={initialValues}
|
||||||
|
getGroups={() => []}
|
||||||
|
compute={() => {}}
|
||||||
|
input={input}
|
||||||
|
setInput={setInput}
|
||||||
|
inputComponent={
|
||||||
|
<ToolTextInput
|
||||||
|
title="QR 코드에 들어갈 텍스트"
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
hideFileImport
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
resultComponent={
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div
|
||||||
|
ref={qrRef}
|
||||||
|
className="p-4 bg-white rounded shadow inline-block"
|
||||||
|
>
|
||||||
|
<QRCode value={input || 'https://example.com'} size={256} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
QR 코드 다운로드
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
toolInfo={{
|
||||||
|
title: 'QR 코드 생성기란?',
|
||||||
|
description: longDescription
|
||||||
|
}}
|
||||||
|
exampleCards={[]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-12">
|
||||||
|
<h2 className="text-2xl font-semibold text-center mb-6">
|
||||||
|
QR 코드 예시 미리보기
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'NAVER',
|
||||||
|
text: 'https://naver.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ChungBuk University',
|
||||||
|
text: 'https://lms.chungbuk.ac.kr/login/index.php'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'EMAIL',
|
||||||
|
text: 'sunju@example.com'
|
||||||
|
}
|
||||||
|
].map((example, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-white rounded-xl shadow p-6 flex flex-col items-center justify-center text-center"
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-bold mb-3">{example.title}</h3>
|
||||||
|
<div className="w-full text-sm bg-gray-100 p-3 rounded mb-4 break-words">
|
||||||
|
{example.text}
|
||||||
|
</div>
|
||||||
|
<QRCode value={example.text} size={128} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/pages/tools/string/qr-generator/meta.ts
Normal file
14
src/pages/tools/string/qr-generator/meta.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineTool } from '@tools/defineTool';
|
||||||
|
import { lazy } from 'react';
|
||||||
|
//import { QrCodeIcon } from '@iconify-icons/mdi/qrcode';
|
||||||
|
|
||||||
|
export const tool = defineTool('string', {
|
||||||
|
path: 'qr-generator',
|
||||||
|
name: 'QR 코드 생성기',
|
||||||
|
icon: 'proicons:quote',
|
||||||
|
description: '텍스트를 QR 코드로 변환합니다.',
|
||||||
|
shortDescription: 'QR 코드 생성',
|
||||||
|
longDescription: '입력한 문자열을 QR 코드로 생성하는 도구입니다.',
|
||||||
|
keywords: ['qr', 'code', 'generator', 'string', '텍스트'],
|
||||||
|
component: lazy(() => import('./index'))
|
||||||
|
});
|
||||||
0
src/pages/tools/string/qr-generator/qr-generator.tsx
Normal file
0
src/pages/tools/string/qr-generator/qr-generator.tsx
Normal file
3
src/pages/tools/string/qr-generator/service.ts
Normal file
3
src/pages/tools/string/qr-generator/service.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const isValidQRCodeInput = (text: string): boolean => {
|
||||||
|
return typeof text === 'string' && text.trim().length > 0;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue