mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-13 11:32:39 +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",
|
||||
"dayjs": "^1.11.13",
|
||||
"formik": "^2.4.6",
|
||||
"html-to-image": "^1.11.13",
|
||||
"jimp": "^0.22.12",
|
||||
"js-quantities": "^1.8.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
|
|
@ -55,11 +56,13 @@
|
|||
"omggif": "^1.0.10",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"playwright": "^1.45.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"rc-slider": "^11.1.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"tesseract.js": "^6.0.0",
|
||||
"type-fest": "^4.35.0",
|
||||
|
|
|
|||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
|
|
@ -65,6 +65,9 @@ importers:
|
|||
formik:
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6(react@18.3.1)
|
||||
html-to-image:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
jimp:
|
||||
specifier: ^0.22.12
|
||||
version: 0.22.12
|
||||
|
|
@ -98,6 +101,9 @@ importers:
|
|||
playwright:
|
||||
specifier: ^1.45.0
|
||||
version: 1.51.1
|
||||
qrcode.react:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(react@18.3.1)
|
||||
rc-slider:
|
||||
specifier: ^11.1.8
|
||||
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:
|
||||
specifier: ^11.0.7
|
||||
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:
|
||||
specifier: ^6.23.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:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
|
|
@ -3098,6 +3110,14 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
|
|
@ -3147,6 +3167,11 @@ packages:
|
|||
react-is@19.1.0:
|
||||
resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==}
|
||||
|
||||
react-qr-code@2.0.15:
|
||||
resolution: {integrity: sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
|
||||
react-router-dom@6.30.0:
|
||||
resolution: {integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -6098,6 +6123,8 @@ snapshots:
|
|||
dependencies:
|
||||
react-is: 16.13.1
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
|
@ -6886,6 +6913,12 @@ snapshots:
|
|||
|
||||
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: {}
|
||||
|
||||
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-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):
|
||||
dependencies:
|
||||
'@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 { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
try {
|
||||
compute(values, input);
|
||||
} catch (exception: unknown) {
|
||||
|
|
@ -35,7 +35,8 @@ const FormikListenerComponent = <T,>({
|
|||
useEffect(() => {
|
||||
onValuesChange?.(values);
|
||||
}, [onValuesChange, values]);
|
||||
return null; // This component doesn't render anything
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface ToolContentProps<T, I> extends ToolComponentProps {
|
||||
|
|
@ -99,7 +100,11 @@ export default function ToolContent<T extends FormikValues, I>({
|
|||
input={input}
|
||||
onValuesChange={onValuesChange}
|
||||
/>
|
||||
<ToolOptions getGroups={getGroups} vertical={verticalGroups} />
|
||||
|
||||
{/* meta 파일의 name 속성이 QR 코드 생성기일 때만 ToolOptions 숨기기 */}
|
||||
{title !== 'QR 코드 생성기' && (
|
||||
<ToolOptions getGroups={getGroups} vertical={verticalGroups} />
|
||||
)}
|
||||
|
||||
{toolInfo && toolInfo.title && toolInfo.description && (
|
||||
<ToolInfo
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ export default function InputFooter({
|
|||
handleCopy,
|
||||
handleClear
|
||||
}: {
|
||||
handleImport: () => void;
|
||||
handleImport?: () => void;
|
||||
handleCopy?: () => void;
|
||||
handleClear?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Stack mt={1} direction={'row'} spacing={2}>
|
||||
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
||||
Import from file
|
||||
</Button>
|
||||
{handleImport && (
|
||||
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
||||
Import from file
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{handleCopy && (
|
||||
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
||||
Copy to clipboard
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import InputFooter from './InputFooter';
|
|||
export default function ToolTextInput({
|
||||
value,
|
||||
onChange,
|
||||
title = 'Input text'
|
||||
title = 'Input text',
|
||||
hideFileImport = false
|
||||
}: {
|
||||
title?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
hideFileImport?: boolean;
|
||||
}) {
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -24,6 +26,7 @@ export default function ToolTextInput({
|
|||
showSnackBar('Failed to copy: ' + err, 'error');
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
|
|
@ -41,6 +44,7 @@ export default function ToolTextInput({
|
|||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title} />
|
||||
|
|
@ -59,7 +63,12 @@ export default function ToolTextInput({
|
|||
'data-testid': 'text-input'
|
||||
}}
|
||||
/>
|
||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
||||
|
||||
<InputFooter
|
||||
handleCopy={handleCopy}
|
||||
handleImport={hideFileImport ? undefined : handleImportClick}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept="*"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { tool as stringJoin } from './join/meta';
|
|||
import { tool as stringReplace } from './text-replacer/meta';
|
||||
import { tool as stringRepeat } from './repeat/meta';
|
||||
import { tool as stringTruncate } from './truncate/meta';
|
||||
import { tool as stringQrGenerator } from './qr-generator/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
|
|
@ -31,5 +32,6 @@ export const stringTools = [
|
|||
stringPalindrome,
|
||||
stringQuote,
|
||||
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