mirror of
https://github.com/vbalien/voca.git
synced 2025-12-06 11:26:21 +09:00
add: voca list
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,17 +1,28 @@
|
|||||||
import { React } from "../deps.ts";
|
import { React } from "../deps.ts";
|
||||||
import { downloadVocaQuiz } from "../pdf.ts";
|
import useDownloadFormState from "../hooks/use-download-form-state.ts";
|
||||||
|
import { downloadVoca } from "../pdf.ts";
|
||||||
|
|
||||||
export default function DownloadForm() {
|
export default function DownloadForm() {
|
||||||
const [levels, setLevels] = React.useState<number[]>([6, 7]);
|
const {
|
||||||
const [day, setDay] = React.useState<number>(1);
|
levelState: [levels, setLevels],
|
||||||
|
dayState: [day, setDay],
|
||||||
|
save,
|
||||||
|
} = useDownloadFormState();
|
||||||
const [formEnable, setFormEnable] = React.useState<boolean>(true);
|
const [formEnable, setFormEnable] = React.useState<boolean>(true);
|
||||||
|
|
||||||
const handleSubmit: React.FormEventHandler = React.useCallback((ev) => {
|
const handleDownload: React.FormEventHandler = React.useCallback((ev) => {
|
||||||
localStorage.setItem("voca-setting", JSON.stringify({ levels, day }));
|
const button = ev.target as HTMLButtonElement;
|
||||||
|
save({ levels, day });
|
||||||
setFormEnable(false);
|
setFormEnable(false);
|
||||||
downloadVocaQuiz(levels.sort(), day).then(() => {
|
downloadVoca(
|
||||||
setFormEnable(true);
|
levels.sort(),
|
||||||
});
|
day,
|
||||||
|
button.id === "testDownload" ? "TEST" : "LIST",
|
||||||
|
).then(
|
||||||
|
() => {
|
||||||
|
setFormEnable(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}, [levels, day]);
|
}, [levels, day]);
|
||||||
|
|
||||||
@@ -31,86 +42,91 @@ export default function DownloadForm() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const setting = JSON.parse(localStorage.getItem("voca-setting") ?? "{}");
|
|
||||||
setting.level && setLevels(setting.levels);
|
|
||||||
setting.day && setDay(setting.day);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit}>
|
<fieldset disabled={!formEnable}>
|
||||||
<fieldset disabled={!formEnable}>
|
<legend>시험 난이도</legend>
|
||||||
<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>
|
<div>
|
||||||
<label htmlFor="day">범위</label>
|
<input
|
||||||
<select
|
type="checkbox"
|
||||||
id="day"
|
id="10"
|
||||||
value={day}
|
value="10"
|
||||||
onChange={handleDayChange}
|
checked={levels.find((val) => val === 10) !== undefined}
|
||||||
disabled={!formEnable}
|
onChange={handleLevelChange}
|
||||||
>
|
/>
|
||||||
{[...Array(30)].map((_, i) => <option value={i + 1}>{i + 1}
|
<label htmlFor="10">전체 난이도</label>
|
||||||
</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" disabled={!formEnable}>PDF 다운</button>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="6"
|
||||||
|
value="6"
|
||||||
|
checked={levels.find((val) => val === 6) !== undefined}
|
||||||
|
onChange={handleLevelChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="6">입문반(550+), 기본반(650+)</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<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}
|
||||||
|
disabled={!formEnable}
|
||||||
|
>
|
||||||
|
{[...Array(30)].map((_, i) => <option value={i + 1}>{i + 1}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
id="testDownload"
|
||||||
|
disabled={!formEnable}
|
||||||
|
onClick={handleDownload}
|
||||||
|
>
|
||||||
|
문제 다운로드
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="listDownload"
|
||||||
|
disabled={!formEnable}
|
||||||
|
onClick={handleDownload}
|
||||||
|
>
|
||||||
|
단어장 다운로드
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
PDFDocument,
|
PDFDocument,
|
||||||
|
PDFFont,
|
||||||
PDFName,
|
PDFName,
|
||||||
PDFPage,
|
PDFPage,
|
||||||
PDFRef,
|
PDFRef,
|
||||||
|
|||||||
24
src/website/hooks/use-download-form-state.ts
Normal file
24
src/website/hooks/use-download-form-state.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { React } from "../deps.ts";
|
||||||
|
|
||||||
|
const VOCA_SETTING_KEY = "voca-setting";
|
||||||
|
|
||||||
|
export default function useDownloadFormState() {
|
||||||
|
const [levels, setLevels] = React.useState<number[]>([6, 7]);
|
||||||
|
const [day, setDay] = React.useState<number>(1);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const setting = JSON.parse(localStorage.getItem(VOCA_SETTING_KEY) ?? "{}");
|
||||||
|
setting.level && setLevels(setting.levels);
|
||||||
|
setting.day && setDay(setting.day);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const save = React.useCallback(({ levels, day }) => {
|
||||||
|
localStorage.setItem(VOCA_SETTING_KEY, JSON.stringify({ levels, day }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
levelState: [levels, setLevels] as const,
|
||||||
|
dayState: [day, setDay] as const,
|
||||||
|
save,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
fontkit,
|
fontkit,
|
||||||
PDFDocument,
|
PDFDocument,
|
||||||
|
PDFFont,
|
||||||
PDFName,
|
PDFName,
|
||||||
PDFPage,
|
PDFPage,
|
||||||
PDFRef,
|
PDFRef,
|
||||||
@@ -25,17 +26,97 @@ function downloadBuffer(buffer: Uint8Array, fileName: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makePdf(voca_list: Voca[]) {
|
type DrawWordOption = {
|
||||||
|
page: PDFPage;
|
||||||
|
font: PDFFont;
|
||||||
|
pdfUrlList: PDFRef[];
|
||||||
|
num: number;
|
||||||
|
voca: Voca;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
blind: boolean;
|
||||||
|
};
|
||||||
|
function drawWord(
|
||||||
|
option: DrawWordOption,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
page,
|
||||||
|
font,
|
||||||
|
pdfUrlList,
|
||||||
|
num,
|
||||||
|
voca,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
blind,
|
||||||
|
} = option;
|
||||||
|
const fontSize = 12;
|
||||||
|
const margin = 20;
|
||||||
|
const text = `${num}. ${voca.word}`;
|
||||||
|
const textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||||
|
const textHeight = font.heightAtSize(fontSize);
|
||||||
|
const textX = margin + (page.getWidth() / 2) * x;
|
||||||
|
const textY = page.getHeight() - (fontSize + margin) * y;
|
||||||
|
|
||||||
|
page.drawText(text, {
|
||||||
|
x: textX,
|
||||||
|
y: textY,
|
||||||
|
size: fontSize,
|
||||||
|
font,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
});
|
||||||
|
page.drawText("_________________", {
|
||||||
|
x: margin + (page.getWidth() / 2) * x + 140,
|
||||||
|
y: page.getHeight() - (fontSize + margin) * y - 3,
|
||||||
|
size: fontSize,
|
||||||
|
font,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfUrlDict = page.doc.context.obj({
|
||||||
|
Type: "Annot",
|
||||||
|
Subtype: "Link",
|
||||||
|
Rect: [textX, textY, textX + textWidth, textY + textHeight],
|
||||||
|
A: {
|
||||||
|
Type: "Action",
|
||||||
|
S: "URI",
|
||||||
|
URI: PDFString.of(
|
||||||
|
`https://en.dict.naver.com/#/search?range=all&query=${
|
||||||
|
encodeURIComponent(voca.word)
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pdfUrl = page.doc.context.register(pdfUrlDict);
|
||||||
|
pdfUrlList.push(pdfUrl);
|
||||||
|
|
||||||
|
if (!blind) {
|
||||||
|
page.drawText(voca.answer, {
|
||||||
|
x: margin + (page.getWidth() / 2) * x + 140,
|
||||||
|
y: page.getHeight() - (fontSize + margin) * y,
|
||||||
|
size: 8,
|
||||||
|
font,
|
||||||
|
color: rgb(1, 0, 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function makePdf(
|
||||||
|
voca_list: Voca[],
|
||||||
|
type: "TEST" | "LIST",
|
||||||
|
) {
|
||||||
const pdfDoc = await PDFDocument.create();
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
|
||||||
|
if (type === "LIST") {
|
||||||
|
voca_list.sort(wordCompare);
|
||||||
|
} else {
|
||||||
|
voca_list = shuffle(voca_list);
|
||||||
|
}
|
||||||
|
|
||||||
const fontBytes = await fetch("./NanumGothic.ttf").then((res) =>
|
const fontBytes = await fetch("./NanumGothic.ttf").then((res) =>
|
||||||
res.arrayBuffer()
|
res.arrayBuffer()
|
||||||
);
|
);
|
||||||
pdfDoc.registerFontkit(fontkit);
|
pdfDoc.registerFontkit(fontkit);
|
||||||
const customFont = await pdfDoc.embedFont(fontBytes);
|
const customFont = await pdfDoc.embedFont(fontBytes);
|
||||||
|
|
||||||
const fontSize = 12;
|
|
||||||
const margin = 20;
|
|
||||||
const perColumn = 25;
|
const perColumn = 25;
|
||||||
const perPage = 50;
|
const perPage = 50;
|
||||||
|
|
||||||
@@ -45,61 +126,36 @@ export async function makePdf(voca_list: Voca[]) {
|
|||||||
let pdfUrlList: PDFRef[] = [];
|
let pdfUrlList: PDFRef[] = [];
|
||||||
|
|
||||||
// 문제 생성
|
// 문제 생성
|
||||||
for (let i = 0; i < voca_list.length; ++i) {
|
if (type === "TEST") {
|
||||||
const data = voca_list[i];
|
for (let i = 0; i < voca_list.length; ++i) {
|
||||||
if (i % perPage === 0) {
|
const data = voca_list[i];
|
||||||
if (page) {
|
if (i % perPage === 0) {
|
||||||
page.node.set(PDFName.of("Annots"), pdfDoc.context.obj(pdfUrlList));
|
if (page) {
|
||||||
|
page.node.set(PDFName.of("Annots"), pdfDoc.context.obj(pdfUrlList));
|
||||||
|
}
|
||||||
|
pdfUrlList = [];
|
||||||
|
page = pdfDoc.addPage();
|
||||||
|
|
||||||
|
yCursor = 1;
|
||||||
|
col = -1;
|
||||||
|
}
|
||||||
|
if (i % perColumn === 0) {
|
||||||
|
col++;
|
||||||
|
yCursor = 1;
|
||||||
}
|
}
|
||||||
pdfUrlList = [];
|
|
||||||
page = pdfDoc.addPage();
|
|
||||||
|
|
||||||
yCursor = 1;
|
drawWord({
|
||||||
col = -1;
|
page,
|
||||||
|
font: customFont,
|
||||||
|
pdfUrlList,
|
||||||
|
num: i + 1,
|
||||||
|
voca: data,
|
||||||
|
x: col,
|
||||||
|
y: yCursor,
|
||||||
|
blind: true,
|
||||||
|
});
|
||||||
|
yCursor++;
|
||||||
}
|
}
|
||||||
if (i % perColumn === 0) {
|
|
||||||
col++;
|
|
||||||
yCursor = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = `${i + 1}. ${data.word}`;
|
|
||||||
const textWidth = customFont.widthOfTextAtSize(text, fontSize);
|
|
||||||
const textHeight = customFont.heightAtSize(fontSize);
|
|
||||||
const textX = margin + (page.getWidth() / 2) * col;
|
|
||||||
const textY = page.getHeight() - (fontSize + margin) * yCursor;
|
|
||||||
|
|
||||||
const pdfUrlDict = pdfDoc.context.obj({
|
|
||||||
Type: "Annot",
|
|
||||||
Subtype: "Link",
|
|
||||||
Rect: [textX, textY, textX + textWidth, textY + textHeight],
|
|
||||||
A: {
|
|
||||||
Type: "Action",
|
|
||||||
S: "URI",
|
|
||||||
URI: PDFString.of(
|
|
||||||
`https://en.dict.naver.com/#/search?range=all&query=${
|
|
||||||
encodeURIComponent(data.word)
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const pdfUrl = pdfDoc.context.register(pdfUrlDict);
|
|
||||||
pdfUrlList.push(pdfUrl);
|
|
||||||
|
|
||||||
page.drawText(text, {
|
|
||||||
x: textX,
|
|
||||||
y: textY,
|
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 답 생성
|
// 답 생성
|
||||||
@@ -119,49 +175,15 @@ export async function makePdf(voca_list: Voca[]) {
|
|||||||
yCursor = 1;
|
yCursor = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = `${i + 1}. ${data.word}`;
|
drawWord({
|
||||||
const textWidth = customFont.widthOfTextAtSize(text, fontSize);
|
page,
|
||||||
const textHeight = customFont.heightAtSize(fontSize);
|
|
||||||
const textX = margin + (page.getWidth() / 2) * col;
|
|
||||||
const textY = page.getHeight() - (fontSize + margin) * yCursor;
|
|
||||||
|
|
||||||
const pdfUrlDict = pdfDoc.context.obj({
|
|
||||||
Type: "Annot",
|
|
||||||
Subtype: "Link",
|
|
||||||
Rect: [textX, textY, textX + textWidth, textY + textHeight],
|
|
||||||
A: {
|
|
||||||
Type: "Action",
|
|
||||||
S: "URI",
|
|
||||||
URI: PDFString.of(
|
|
||||||
`https://en.dict.naver.com/#/search?range=all&query=${
|
|
||||||
encodeURIComponent(data.word)
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const pdfUrl = pdfDoc.context.register(pdfUrlDict);
|
|
||||||
pdfUrlList.push(pdfUrl);
|
|
||||||
|
|
||||||
page.drawText(text, {
|
|
||||||
x: textX,
|
|
||||||
y: textY,
|
|
||||||
size: fontSize,
|
|
||||||
font: customFont,
|
font: customFont,
|
||||||
color: rgb(0, 0, 0),
|
pdfUrlList,
|
||||||
});
|
num: i + 1,
|
||||||
page.drawText("_________________", {
|
voca: data,
|
||||||
x: margin + (page.getWidth() / 2) * col + 140,
|
x: col,
|
||||||
y: page.getHeight() - (fontSize + margin) * yCursor - 3,
|
y: yCursor,
|
||||||
size: fontSize,
|
blind: false,
|
||||||
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++;
|
yCursor++;
|
||||||
}
|
}
|
||||||
@@ -170,15 +192,30 @@ export async function makePdf(voca_list: Voca[]) {
|
|||||||
return pdfBytes;
|
return pdfBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadVocaQuiz(levels: number[], day: number) {
|
const wordCompare = (a: Voca, b: Voca) => {
|
||||||
|
const [wordA, wordB] = [a.word.toUpperCase(), b.word.toUpperCase()];
|
||||||
|
if (wordA < wordB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (wordA > wordB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function downloadVoca(
|
||||||
|
levels: number[],
|
||||||
|
day: number,
|
||||||
|
type: "TEST" | "LIST",
|
||||||
|
) {
|
||||||
if (levels.includes(10)) levels = [6, 7, 8, 9];
|
if (levels.includes(10)) levels = [6, 7, 8, 9];
|
||||||
const json: JSONType = await (await fetch("/voca.json")).json();
|
const json: JSONType = await (await fetch("/voca.json")).json();
|
||||||
const voca_list = json.data.filter((chunk) =>
|
const voca_list = json.data.filter((chunk) =>
|
||||||
levels.includes(chunk.level) && chunk.day === day
|
levels.includes(chunk.level) && chunk.day === day
|
||||||
).flatMap((chunk) => chunk.voca_list);
|
).flatMap((chunk) => chunk.voca_list);
|
||||||
const bufferPdf = await makePdf(shuffle(voca_list));
|
const bufferPdf = await makePdf(shuffle(voca_list), type);
|
||||||
await downloadBuffer(
|
await downloadBuffer(
|
||||||
bufferPdf,
|
bufferPdf,
|
||||||
`voca-day${day}-level(${levels.join(",")}).pdf`,
|
`voca-${type.toLowerCase()}-day${day}-level(${levels.join(",")}).pdf`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user