mirror of
https://github.com/vbalien/voca.git
synced 2026-01-26 20:36:21 +09:00
refactoring
This commit is contained in:
52
src/crawl.ts
Normal file
52
src/crawl.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { path } from "./deps.ts";
|
||||
import { JSONType, Voca } from "./types.ts";
|
||||
|
||||
const __dirname = path.dirname(path.fromFileUrl(import.meta.url));
|
||||
|
||||
async function getVocaList(levels: number[], day: number): Promise<Voca[]> {
|
||||
const re =
|
||||
/<p class="word">(?:\d+?)\. (?<word>.+?)<\/p>.*?<span class="af_answer">(?<answer>.+?)<\/span>/gis;
|
||||
const res = await fetch(
|
||||
`https://www.hackers.co.kr/?c=s_toeic/new_voca_toeic_testpaper/toeic_study/new_paper&mode=new_view&level=${
|
||||
levels.join(",")
|
||||
}&level_type=&lang_text=2&question=1000&day3=${day}&day4=${day}&day_auto=N&index=1`,
|
||||
);
|
||||
|
||||
const body = await res.text();
|
||||
const matches = [...body.matchAll(re)].map((m) => ({
|
||||
...(m.groups as unknown as Voca),
|
||||
}));
|
||||
return matches;
|
||||
}
|
||||
|
||||
const levels = [6, 7, 8, 9];
|
||||
const days = [...Array(30)].map((_, i) => i + 1);
|
||||
const result: JSONType = { data: [] };
|
||||
|
||||
for (const level of levels) {
|
||||
for (const day of days) {
|
||||
console.log(`GET: ${level}-${day}`);
|
||||
const voca_list = await getVocaList([level], day);
|
||||
voca_list.sort((a, b) => {
|
||||
const wordA = a.word.toUpperCase();
|
||||
const wordB = b.word.toUpperCase();
|
||||
if (wordA < wordB) {
|
||||
return -1;
|
||||
}
|
||||
if (wordA > wordB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
result.data.push({
|
||||
level,
|
||||
day,
|
||||
voca_list,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Deno.writeTextFileSync(
|
||||
path.join(__dirname, "../doc/", "voca.json"),
|
||||
JSON.stringify(result),
|
||||
);
|
||||
2
src/deps.ts
Normal file
2
src/deps.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as path from "https://deno.land/std@0.127.0/path/mod.ts";
|
||||
export * from "./website/deps.ts";
|
||||
14
src/types.ts
Normal file
14
src/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type Voca = {
|
||||
word: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
export type VocaChunk = {
|
||||
level: number;
|
||||
day: number;
|
||||
voca_list: Voca[];
|
||||
};
|
||||
|
||||
export type JSONType = {
|
||||
data: VocaChunk[];
|
||||
};
|
||||
10
src/website/app.tsx
Normal file
10
src/website/app.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { React } from "./deps.ts";
|
||||
import DownloadForm from "./components/download-form.tsx";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div>
|
||||
<DownloadForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
src/website/components/download-form.tsx
Normal file
99
src/website/components/download-form.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { React } from "../deps.ts";
|
||||
import { downloadVocaQuiz } from "../pdf.ts";
|
||||
|
||||
export default function DownloadForm() {
|
||||
const [levels, setLevels] = React.useState<number[]>([6, 7]);
|
||||
const [day, setDay] = React.useState<number>(1);
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((ev) => {
|
||||
downloadVocaQuiz(levels.sort(), day);
|
||||
ev.preventDefault();
|
||||
}, [levels, day]);
|
||||
|
||||
const handleDayChange: React.FormEventHandler<HTMLSelectElement> = (ev) => {
|
||||
const select = ev.target as HTMLSelectElement;
|
||||
setDay(Number.parseInt(select.value));
|
||||
};
|
||||
|
||||
const handleLevelChange: React.FormEventHandler<HTMLInputElement> = (ev) => {
|
||||
const checkbox = ev.target as HTMLInputElement;
|
||||
if (checkbox.checked) {
|
||||
setLevels((prev) => [...prev, Number.parseInt(checkbox.value)]);
|
||||
} else {
|
||||
setLevels((prev) =>
|
||||
prev.filter((val) => val !== Number.parseInt(checkbox.value))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<fieldset>
|
||||
<legend>시험 난이도</legend>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="10"
|
||||
value="10"
|
||||
checked={levels.find((val) => val === 10) !== undefined}
|
||||
onChange={handleLevelChange}
|
||||
/>
|
||||
<label htmlFor="10">전체 난이도</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="6"
|
||||
value="6"
|
||||
checked={levels.find((val) => val === 6) !== undefined}
|
||||
onChange={handleLevelChange}
|
||||
/>
|
||||
<label htmlFor="6">입문반(550+), 기본반(650+)</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="7"
|
||||
value="7"
|
||||
checked={levels.find((val) => val === 7) !== undefined}
|
||||
onChange={handleLevelChange}
|
||||
/>
|
||||
<label htmlFor="7">중급반(750+)</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="8"
|
||||
value="8"
|
||||
checked={levels.find((val) => val === 8) !== undefined}
|
||||
onChange={handleLevelChange}
|
||||
/>
|
||||
<label htmlFor="8">정규반(850+)</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="9"
|
||||
value="9"
|
||||
checked={levels.find((val) => val === 9) !== undefined}
|
||||
onChange={handleLevelChange}
|
||||
/>
|
||||
<label htmlFor="9">실전반(900+)</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<label htmlFor="day">범위</label>
|
||||
<select id="day" value={day} onChange={handleDayChange}>
|
||||
{[...Array(30)].map((_, i) => <option value={i + 1}>{i + 1}
|
||||
</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit">PDF 다운</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
5
src/website/deps.ts
Normal file
5
src/website/deps.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { PDFDocument, PDFPage, rgb } from "https://esm.sh/pdf-lib@^1.11.1";
|
||||
export { default as fontkit } from "https://esm.sh/@pdf-lib/fontkit@^1.0.0";
|
||||
export { default as React } from "https://esm.sh/react@17";
|
||||
export { default as ReactDOM } from "https://esm.sh/react-dom@17";
|
||||
export { default as shuffle } from "https://esm.sh/lodash@4.2/shuffle?no-check";
|
||||
7
src/website/mod.ts
Normal file
7
src/website/mod.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ReactDOM } from "./deps.ts";
|
||||
import app from "./app.tsx";
|
||||
|
||||
export function render() {
|
||||
const rootEl = document.getElementById("root");
|
||||
ReactDOM.render(app(), rootEl);
|
||||
}
|
||||
110
src/website/pdf.ts
Normal file
110
src/website/pdf.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { fontkit, PDFDocument, PDFPage, rgb, shuffle } from "./deps.ts";
|
||||
import { JSONType, Voca } from "../types.ts";
|
||||
|
||||
function downloadBuffer(buffer: Uint8Array, fileName: string) {
|
||||
const a: HTMLAnchorElement = document.createElement("a");
|
||||
const blob = new Blob([buffer], { type: "application/octet-stream" });
|
||||
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.style.display = "none";
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
export async function makePdf(voca_list: Voca[]) {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
const fontBytes = await fetch("./NanumGothic.ttf").then((res) =>
|
||||
res.arrayBuffer()
|
||||
);
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
const customFont = await pdfDoc.embedFont(fontBytes);
|
||||
|
||||
const fontSize = 12;
|
||||
const margin = 20;
|
||||
const perColumn = 25;
|
||||
const perPage = 50;
|
||||
|
||||
let page!: PDFPage;
|
||||
let col = -1;
|
||||
let yCursor = 1;
|
||||
|
||||
// 문제 생성
|
||||
for (let i = 0; i < voca_list.length; ++i) {
|
||||
const data = voca_list[i];
|
||||
if (i % perPage === 0) {
|
||||
page = pdfDoc.addPage();
|
||||
yCursor = 1;
|
||||
col = -1;
|
||||
}
|
||||
if (i % perColumn === 0) {
|
||||
col++;
|
||||
yCursor = 1;
|
||||
}
|
||||
page.drawText(`${i + 1}. ${data.word}`, {
|
||||
x: margin + (page.getWidth() / 2) * col,
|
||||
y: page.getHeight() - (fontSize + margin) * yCursor,
|
||||
size: fontSize,
|
||||
font: customFont,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
page.drawText("_________________", {
|
||||
x: margin + (page.getWidth() / 2) * col + 140,
|
||||
y: page.getHeight() - (fontSize + margin) * yCursor - 3,
|
||||
size: fontSize,
|
||||
font: customFont,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
yCursor++;
|
||||
}
|
||||
|
||||
// 답 생성
|
||||
for (let i = 0; i < voca_list.length; ++i) {
|
||||
const data = voca_list[i];
|
||||
if (i % perPage === 0) {
|
||||
page = pdfDoc.addPage();
|
||||
yCursor = 1;
|
||||
col = -1;
|
||||
}
|
||||
if (i % perColumn === 0) {
|
||||
col++;
|
||||
yCursor = 1;
|
||||
}
|
||||
page.drawText(`${i + 1}. ${data.word}`, {
|
||||
x: margin + (page.getWidth() / 2) * col,
|
||||
y: page.getHeight() - (fontSize + margin) * yCursor,
|
||||
size: fontSize,
|
||||
font: customFont,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
page.drawText("_________________", {
|
||||
x: margin + (page.getWidth() / 2) * col + 140,
|
||||
y: page.getHeight() - (fontSize + margin) * yCursor - 3,
|
||||
size: fontSize,
|
||||
font: customFont,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
page.drawText(data.answer, {
|
||||
x: margin + (page.getWidth() / 2) * col + 140,
|
||||
y: page.getHeight() - (fontSize + margin) * yCursor,
|
||||
size: 8,
|
||||
font: customFont,
|
||||
color: rgb(1, 0, 0),
|
||||
});
|
||||
yCursor++;
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return pdfBytes;
|
||||
}
|
||||
|
||||
export async function downloadVocaQuiz(levels: number[], day: number) {
|
||||
const json: JSONType = await (await fetch("/voca.json")).json();
|
||||
const voca_list = json.data.filter((chunk) =>
|
||||
levels.includes(chunk.level) && chunk.day === day
|
||||
).flatMap((chunk) => chunk.voca_list);
|
||||
const bufferPdf = await makePdf(shuffle(voca_list));
|
||||
downloadBuffer(bufferPdf, "voca.pdf");
|
||||
}
|
||||
Reference in New Issue
Block a user