From 2f68a5bf84d70ab6f132892713ffd80c4fe387f1 Mon Sep 17 00:00:00 2001 From: minseonju <10sc1108@naver.com> Date: Mon, 19 May 2025 15:39:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20QrCode=20Generator=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 115 ++++++++++++++++++ pnpm-workspace.yaml | 5 + src/pages/tools/string/qr-generator/index.tsx | 52 ++++++++ src/pages/tools/string/qr-generator/meta.ts | 14 +++ .../string/qr-generator/qr-generator.tsx | 0 .../tools/string/qr-generator/service.ts | 17 +++ 7 files changed, 205 insertions(+) create mode 100644 pnpm-workspace.yaml create mode 100644 src/pages/tools/string/qr-generator/index.tsx create mode 100644 src/pages/tools/string/qr-generator/meta.ts create mode 100644 src/pages/tools/string/qr-generator/qr-generator.tsx create mode 100644 src/pages/tools/string/qr-generator/service.ts diff --git a/package.json b/package.json index 6b1cb59..2f2d3c2 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef72550..9ea18e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@types/ffmpeg': specifier: ^1.0.7 version: 1.0.7 + '@types/js-quantities': + specifier: ^1.6.6 + version: 1.6.6 '@types/lodash': specifier: ^4.17.5 version: 4.17.16 @@ -56,12 +59,18 @@ importers: color: specifier: ^4.2.3 version: 4.2.3 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 formik: specifier: ^2.4.6 version: 2.4.6(react@18.3.1) jimp: specifier: ^0.22.12 version: 0.22.12 + js-quantities: + specifier: ^1.8.0 + version: 1.8.0 lint-staged: specifier: ^15.4.3 version: 15.5.0 @@ -74,6 +83,9 @@ importers: morsee: specifier: ^1.0.9 version: 1.0.10 + nerdamer-prime: + specifier: ^1.2.4 + version: 1.2.4 notistack: specifier: ^3.0.1 version: 3.0.2(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -86,6 +98,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) @@ -101,9 +116,15 @@ 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) + tesseract.js: + specifier: ^6.0.0 + version: 6.0.1 type-fest: specifier: ^4.35.0 version: 4.38.0 @@ -1182,6 +1203,9 @@ packages: '@types/hoist-non-react-statics@3.3.6': resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/js-quantities@1.6.6': + resolution: {integrity: sha512-k2Q8/Avj4Oz50flfTnfVGnUCkt7OYQ3U+lfQVELE/x5mdbwChZ7fM0wpUpVWzbuSL8kIYt9ZsFQ0RFNBv8T3Qw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1684,6 +1708,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2243,6 +2270,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2423,6 +2453,9 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -2467,6 +2500,9 @@ packages: jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-quantities@1.8.0: + resolution: {integrity: sha512-swDw9RJpXACAWR16vAKoSojAsP6NI7cZjjnjKqhOyZSdybRUdmPr071foD3fejUKSU2JMHz99hflWkRWvfLTpQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2724,6 +2760,10 @@ packages: ndarray@1.0.19: resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} + nerdamer-prime@1.2.4: + resolution: {integrity: sha512-oaGnI7GUTj3T2k2PkeGf/694uLY9pYwFSOkn/5aTuz1RXdJeD6hN0+2csKagB65H6R8J1Zb7UlPq7zEzQ2dumw==} + engines: {node: '>=0.10.0'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2822,6 +2862,10 @@ packages: onnxruntime-web@1.21.0-dev.20250206-d981b153d3: resolution: {integrity: sha512-esDVQdRic6J44VBMFLumYvcGfioMh80ceLmzF1yheJyuLKq/Th8VT2aj42XWQst+2bcWnAhw4IKmRQaqzU8ugg==} + opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3060,6 +3104,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==} @@ -3109,6 +3161,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'} @@ -3426,6 +3483,12 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tesseract.js-core@6.0.0: + resolution: {integrity: sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==} + + tesseract.js@6.0.1: + resolution: {integrity: sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -3661,6 +3724,9 @@ packages: engines: {node: '>=12.0.0'} hasBin: true + wasm-feature-detect@1.8.0: + resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -3781,6 +3847,9 @@ packages: yup@1.6.1: resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==} + zlibjs@0.3.1: + resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==} + zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} @@ -4749,6 +4818,8 @@ snapshots: '@types/react': 18.3.3 hoist-non-react-statics: 3.3.2 + '@types/js-quantities@1.6.6': {} + '@types/json-schema@7.0.15': {} '@types/lodash@4.17.16': {} @@ -5346,6 +5417,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dayjs@1.11.13: {} + debug@4.3.4: dependencies: ms: 2.1.2 @@ -6054,6 +6127,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb-keyval@6.2.2: {} + ieee754@1.2.1: {} ignore@5.3.1: {} @@ -6206,6 +6281,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-url@1.2.4: {} + is-weakmap@2.0.2: {} is-weakref@1.0.2: @@ -6266,6 +6343,8 @@ snapshots: jpeg-js@0.4.4: {} + js-quantities@1.8.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.0: {} @@ -6500,6 +6579,8 @@ snapshots: iota-array: 1.0.0 is-buffer: 1.1.6 + nerdamer-prime@1.2.4: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -6601,6 +6682,8 @@ snapshots: platform: 1.3.6 protobufjs: 7.4.0 + opencollective-postinstall@2.0.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6822,6 +6905,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): @@ -6869,6 +6958,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 @@ -7265,6 +7360,22 @@ snapshots: transitivePeerDependencies: - ts-node + tesseract.js-core@6.0.0: {} + + tesseract.js@6.0.1: + dependencies: + bmp-js: 0.1.0 + idb-keyval: 6.2.2 + is-url: 1.2.4 + node-fetch: 2.7.0 + opencollective-postinstall: 2.0.3 + regenerator-runtime: 0.13.11 + tesseract.js-core: 6.0.0 + wasm-feature-detect: 1.8.0 + zlibjs: 0.3.1 + transitivePeerDependencies: + - encoding + text-extensions@2.4.0: {} text-table@0.2.0: {} @@ -7489,6 +7600,8 @@ snapshots: transitivePeerDependencies: - debug + wasm-feature-detect@1.8.0: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -7624,4 +7737,6 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 + zlibjs@0.3.1: {} + zod@3.24.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..df140b0 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +onlyBuiltDependencies: + - '@swc/core' + - esbuild + - protobufjs + - tesseract.js diff --git a/src/pages/tools/string/qr-generator/index.tsx b/src/pages/tools/string/qr-generator/index.tsx new file mode 100644 index 0000000..5c35f49 --- /dev/null +++ b/src/pages/tools/string/qr-generator/index.tsx @@ -0,0 +1,52 @@ +'use client'; + +import React, { useState } from 'react'; +import ToolTextInput from '@components/input/ToolTextInput'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolContent from '@components/ToolContent'; +import { generateQRCodeSVG } from './service'; + +const initialValues = {}; + +export default function QRGeneratorTool({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const computeExternal = () => { + setResult(generateQRCodeSVG(input)); + }; + + return ( + []} // QR은 옵션 없이 단순 입력 → 변환 + compute={computeExternal} + input={input} + setInput={setInput} + inputComponent={ + + } + resultComponent={ + + } + toolInfo={{ + title: 'QR 코드 생성기란?', + description: longDescription + }} + exampleCards={[]} // 예시 생략 가능 + /> + ); +} diff --git a/src/pages/tools/string/qr-generator/meta.ts b/src/pages/tools/string/qr-generator/meta.ts new file mode 100644 index 0000000..9e5d147 --- /dev/null +++ b/src/pages/tools/string/qr-generator/meta.ts @@ -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')) +}); diff --git a/src/pages/tools/string/qr-generator/qr-generator.tsx b/src/pages/tools/string/qr-generator/qr-generator.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/tools/string/qr-generator/service.ts b/src/pages/tools/string/qr-generator/service.ts new file mode 100644 index 0000000..849916a --- /dev/null +++ b/src/pages/tools/string/qr-generator/service.ts @@ -0,0 +1,17 @@ +import QRCode from 'qrcode'; + +/** + * 문자열을 QR 코드 SVG 문자열로 변환 + */ +export function generateQRCodeSVG(text: string): string { + let svg = ''; + QRCode.toString( + text || 'https://example.com', + { type: 'svg' }, + (err, out) => { + if (err) console.error(err); + else svg = out; + } + ); + return svg; +}