├── .eslintignore ├── app ├── controllers │ ├── index.ts │ └── qr.controller.ts ├── server.ts └── src │ └── qrCode.ts ├── .gitignore ├── .eslintrc ├── package.json ├── README.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /app/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './qr.controller'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | *~ 3 | .DS_Store 4 | 5 | # NPM 6 | node_modules 7 | 8 | # Build 9 | dist 10 | -------------------------------------------------------------------------------- /app/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { QrController } from './controllers'; 4 | 5 | const app: express.Application = express(); 6 | const port: number = Number(process.env.PORT) || 3000; 7 | 8 | app.use('/qr', QrController); 9 | 10 | app.listen(port, () => { 11 | console.log(`Listening at http://localhost:${port}/`); 12 | }); 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": "./tsconfig.json", 6 | "tsconfigRootDir": "./" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "extends": ["airbnb-typescript/base"], 12 | "rules": { 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "import/prefer-default-export": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/es6-shim": "^0.31.41", 4 | "@types/express": "^4.17.9", 5 | "@types/puppeteer": "^5.4.0", 6 | "express": "^4.17.1", 7 | "puppeteer": "^5.5.0", 8 | "typescript": "^4.0.5" 9 | }, 10 | "scripts": { 11 | "build": "tsc && node dist/server.js", 12 | "watch": "tsc --watch", 13 | "lint": "eslint . --ext .ts" 14 | }, 15 | "devDependencies": { 16 | "@typescript-eslint/eslint-plugin": "^4.7.0", 17 | "@typescript-eslint/parser": "^4.7.0", 18 | "eslint": "^7.13.0", 19 | "eslint-config-airbnb-typescript": "^12.0.0", 20 | "eslint-plugin-import": "^2.22.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/controllers/qr.controller.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from 'express'; 2 | import { getQrCode } from '../src/qrCode'; 3 | 4 | const router: Router = Router(); 5 | 6 | router.get('/', async (request: Request, response: Response) => { 7 | const qrCodeResult = await getQrCode({ 8 | id: '', 9 | password: '', 10 | }); 11 | if (qrCodeResult.isSuccess) { 12 | const imageBuffer = Buffer.from(qrCodeResult.result, 'base64'); 13 | response.writeHead(200, { 14 | 'Content-Type': 'image/png', 15 | 'Content-Length': imageBuffer.length 16 | }); 17 | response.end(imageBuffer); 18 | } else { 19 | response.json(qrCodeResult); 20 | response.end(); 21 | } 22 | }); 23 | 24 | export const QrController: Router = router; 25 | -------------------------------------------------------------------------------- /app/src/qrCode.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | export interface INaverCredential { 4 | id: string; 5 | password: string; 6 | } 7 | 8 | export interface IQrResult { 9 | isSuccess: boolean; 10 | result: string; 11 | } 12 | 13 | const MOBILE_VERIFICATION_REQUIRED = '네이버 휴대전화 인증'; 14 | const POLICY_AGREEMENT_REQUIRED = '집합시설 출입을 위한 QR 체크인'; 15 | 16 | export async function getQrCode( 17 | credential: INaverCredential, 18 | ): Promise { 19 | const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); 20 | const page = await browser.newPage(); 21 | 22 | const { id, password } = credential; 23 | 24 | await page.goto('https://nid.naver.com/nidlogin.login?mode=form'); 25 | 26 | await page.evaluate((id: string, password: string) => { 27 | (document.querySelector('#id')).value = id; 28 | (document.querySelector('#pw')).value = password; 29 | }, id, password); 30 | 31 | await page.click('.btn_login'); 32 | await page.waitForNavigation(); 33 | 34 | if (page.url().indexOf('nidlogin.login') !== -1) { 35 | await browser.close(); 36 | return { 37 | isSuccess: false, 38 | result: 'Login has failed.', 39 | }; 40 | } 41 | 42 | await page.goto('https://nid.naver.com/login/privacyQR?term=on'); 43 | 44 | const actionRequiredTextElement = await page.$("#content > .top_copy > .title"); 45 | if (actionRequiredTextElement) { 46 | const actionRequiredText = await page.evaluate(element => element.textContent, actionRequiredTextElement); 47 | if (actionRequiredText === POLICY_AGREEMENT_REQUIRED) { 48 | await page.evaluate(() => { 49 | (document.getElementById('check1')).click(); 50 | (document.getElementById('done')).click(); 51 | }); 52 | await page.waitForNavigation(); 53 | } else if (actionRequiredText === MOBILE_VERIFICATION_REQUIRED) { 54 | await browser.close(); 55 | return { 56 | isSuccess: false, 57 | result: 'Mobile Verification Is Required. Check Via Browser.', 58 | }; 59 | } 60 | } 61 | 62 | await page.waitForSelector('#qrImage'); 63 | const qrImageElement = await page.$('#qrImage'); 64 | 65 | const qrImage = qrImageElement ? await qrImageElement.screenshot({ encoding: "base64" }) : null; 66 | 67 | await browser.close(); 68 | 69 | return { 70 | isSuccess: !!qrImage, 71 | result: !!qrImage ? qrImage : 'Unknown Error', 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # covid19-qrcode 4 | 5 | Get a temporary QR-code, which to use to entrance public facilities using the NAVER QR-CheckIn service. 6 | 집합시설 출입을 위한 대한민국 전자출입명부 QR 체크인에 사용되는 QR 이미지를 빠르게 추출합니다. 7 | 8 | [![Last Commit](https://img.shields.io/github/last-commit/stevejkang/covid19-qrcode.svg)](https://github.com/stevejkang/covid19-qrcode/commits) 9 | 10 |
11 | 📌 IMPORTANT NOTICE 12 | 13 | ``` 14 | 이 프로젝트는 네이버에서 제공하는 "집합시설 출입을 위한 QR 체크인 기능"을 이용하였습니다. 15 | 네이버와 무관한 프로젝트로 언제든 지원이 중단될 수 있습니다. 16 | 개인적으로 집합시설 이용시 오랜 조작 등으로 생기는 불편함을 느껴 이를 간소화하고자 만들게 된 프로젝트입니다. 17 | 다양한 방향으로의 악용을 막고자 코드상으로 API 호출할 수 있는 형태로 제작했음에도, 18 | 누구나 사용할 수 있는 주소(서비스)를 제공하는 것이 아닌 오픈소스로만 공유하며, 19 | 사용함에 있어 모든 책임은 전적으로 사용하는 개인에 있습니다. 20 | 21 | * 이 프로젝트는, 최초에 네이버 QR 체크인 단계 중 하나인 "개인정보 수집 및 제공에 동의" 과정을 22 | headless로 생략하고 동의한 것으로 간주합니다. 23 | 24 | * 이 프로젝트는 2FA, 혹은 기타 로그인을 검증하기 위한 수단을 사용하거나 필요로하는 경우 25 | 예상치 못한 오류가 발생할 수 있습니다. 26 | ``` 27 |
28 | 29 | --- 30 | 31 | ## How To Use? 32 | 33 | 1. `app/controllers/qr.controllers.ts`에서 `YOUR_ID`와 `YOUR_PASSWORD`를 본인의 설정에 맞춰 업데이트합니다. 34 | ```ts 35 | // ... 36 | router.get('/', (request: Request, response: Response) => { 37 | getQrCode({ 38 | id: 'YOUR_ID', 39 | password: 'YOUR_PASSWORD' 40 | }).then((qrCodeResult) => { 41 | // ... 42 | ``` 43 | 44 | 2. `npm run build`로 실행합니다. 이때 해당 서버로 외부에서 접속이 가능해야하며, 포트는 `3000`이 기본 값입니다. 상황에 따라 [pm2](https://www.npmjs.com/package/pm2), [forever](https://www.npmjs.com/package/forever)를 사용하여 실행할 수 있습니다. 45 | ```bash 46 | $ npm i 47 | $ npm run build 48 | ``` 49 | 50 | 3. `:3000/qr`을 iOS Shotcuts 앱에 등록합니다. (템플릿으로 [바로 시작하기](https://www.icloud.com/shortcuts/dd2a9958597b4a6095dce4ac4e0247f7)) 51 | 52 |
53 | 54 | iOS 14부터 지원하는 위젯으로 만들어 사용하면 아래처럼 홈 화면에서 바로 QR 코드를 얻을 수 있습니다. 55 | 56 | ![Giphy](https://media.giphy.com/media/Pij6wCRRInlq0dMOGQ/giphy.gif) 57 | 58 | ## Troubleshooting 59 | 60 | - `...chrome-linux/chrome: error while loading shared libraries: xxxxxxx.so: cannot open shared object file: No such file or directory` on AWS EC2 ubuntu 61 | 62 | ```bash 63 | $ sudo apt-get install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm-dev libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget 64 | ``` 65 | Reference [here](https://techoverflow.net/2018/06/05/how-to-fix-puppetteer-error-while-loading-shared-libraries-libx11-xcb-so-1-cannot-open-shared-object-file-no-such-file-or-directory/) 66 | 67 | ## License 68 | 69 | MIT 70 | 71 | ## Author 72 | 73 | stevejkang 74 | 75 | > [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fstevejkang%2Fcovid19-qrcode)](https://github.com/stevejkang/covid19-qrcode) 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | --------------------------------------------------------------------------------