Merge pull request #8 from minseonju/feat/qr-code-generator

Feat/qr code generator
This commit is contained in:
minseonju 2025-05-21 22:35:59 +09:00 committed by GitHub
commit 5949982bae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 199 additions and 10 deletions

View file

@ -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
View file

@ -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
View file

@ -0,0 +1,5 @@
onlyBuiltDependencies:
- '@swc/core'
- esbuild
- protobufjs
- tesseract.js

View file

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

View file

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

View file

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

View file

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

View 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>
</>
);
}

View 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'))
});

View file

@ -0,0 +1,3 @@
export const isValidQRCodeInput = (text: string): boolean => {
return typeof text === 'string' && text.trim().length > 0;
};