From cbf92ffa10be78432a117d3b10955937ccf5911c Mon Sep 17 00:00:00 2001 From: seung6lee Date: Sun, 14 Jun 2026 08:44:10 +0900 Subject: [PATCH] initial commit --- README.md | 3 ++ index.html | 13 +++++ main.js | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 90 ++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 README.md create mode 100644 index.html create mode 100644 main.js create mode 100644 style.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cee6a3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +This is a korean typing practice website. + +The main function of this program is that it records the number of times each word has been entered wrong. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..37e9b0b --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + Typing practive + + + + + + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..7cdd9d3 --- /dev/null +++ b/main.js @@ -0,0 +1,150 @@ +class TypingPractice extends HTMLElement { + constructor() { + super(); + this.dict = {} + this.dict_display = null + } + + async connectedCallback() { + await this.render(); + } + + async render() { + const raw = await fetch('https://korean-advice-open-api.vercel.app/api/advice') + const res = await raw.json() + const sentence = res["message"] + const sentence_jamo = decompose(sentence) + let progress = 0 + let recording_mistake = false + + const dict = this.dict + + if (!this.dict_display) { + this.dict_display = document.createElement('div') + this.dict_display.className = 'dict-display' + document.body.append(this.dict_display) + } + + const updateDict = () => { + const sorted = Object.entries(dict).sort((a, b) => b[1] - a[1]) + this.dict_display.innerHTML = sorted.length + ? sorted.map(([ch, cnt]) => `${ch}${cnt}`).join('') + : '' + } + + const sentence_display = document.createElement('div') + sentence_display.className = 'sentence' + ;[...sentence].forEach(ch => { + const span = document.createElement('span') + span.textContent = ch + span.className = 'pending' + sentence_display.append(span) + }) + + const updateColors = (has_error) => { + ;[...sentence].forEach((_, idx) => { + const span = sentence_display.children[idx] + if (idx < progress) { + span.className = 'correct' + } else if (idx === progress && has_error) { + span.className = 'wrong' + } else { + span.className = 'pending' + } + }) + } + + const input_box = document.createElement('input') + input_box.setAttribute('autofocus', '') + input_box.addEventListener('input', (e) => { + let cur = e.target.value + let cur_jamo = decompose(cur) + + if (cur.length < progress) { + e.target.value = sentence.slice(0, progress) + updateColors(false) + } else { + let i = 0 + let found_mistake = false + while (i < Math.min(sentence_jamo.length, cur_jamo.length)) { + if (sentence_jamo[i] !== cur_jamo[i]) { + found_mistake = true + if (!recording_mistake) { + recording_mistake = true + dict[sentence_jamo[i]] = (dict[sentence_jamo[i]] || 0) + 1 + updateDict() + } + break + } + i += 1 + } + if (!found_mistake) recording_mistake = false + + const sentence_sliced = sentence.slice(progress, cur.length) + const cur_sliced = cur.slice(progress) + + let j = 0 + while (j < Math.min(sentence_sliced.length, cur_sliced.length)) { + if (sentence_sliced[j] !== cur_sliced[j]) { + break + } + j += 1 + } + progress += j + + updateColors(found_mistake) + + if (progress === sentence.length) { + this.innerHTML = '' + this.render().then(() => { + this.querySelector('input')?.focus() + }) + } + } + }) + + this.append(sentence_display) + this.append(input_box) + } +} + +customElements.define("typing-practice", TypingPractice) + + + +const JONG_MAP = { + 'ㄱ': 'ㄱ', 'ㄲ': 'ㄲ', 'ㄳ': 'ㄱㅅ', 'ㄴ': 'ㄴ', + 'ㄵ': 'ㄴㅈ', 'ㄶ': 'ㄴㅎ', 'ㄷ': 'ㄷ', 'ㄹ': 'ㄹ', + 'ㄺ': 'ㄹㄱ', 'ㄻ': 'ㄹㅁ', 'ㄼ': 'ㄹㅂ', 'ㄽ': 'ㄹㅅ', + 'ㄾ': 'ㄹㅌ', 'ㄿ': 'ㄹㅍ', 'ㅀ': 'ㄹㅎ', 'ㅁ': 'ㅁ', + 'ㅂ': 'ㅂ', 'ㅄ': 'ㅂㅅ', 'ㅅ': 'ㅅ', 'ㅆ': 'ㅆ', + 'ㅇ': 'ㅇ', 'ㅈ': 'ㅈ', 'ㅊ': 'ㅊ', 'ㅋ': 'ㅋ', + 'ㅌ': 'ㅌ', 'ㅍ': 'ㅍ', 'ㅎ': 'ㅎ' +}; + +const CHO = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ']; +const JUNG = ['ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ','ㅗ','ㅘ','ㅙ','ㅚ','ㅛ','ㅜ','ㅝ','ㅞ','ㅟ','ㅠ','ㅡ','ㅢ','ㅣ']; +const JONG = ['','ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅁ','ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ']; + +const COMPOUND_JUNG = { + 'ㅘ': 'ㅗㅏ', 'ㅙ': 'ㅗㅐ', 'ㅚ': 'ㅗㅣ', + 'ㅝ': 'ㅜㅓ', 'ㅞ': 'ㅜㅔ', 'ㅟ': 'ㅜㅣ', + 'ㅢ': 'ㅡㅣ' +} + +function decompose(str) { + return [...str].map(ch => { + const code = ch.charCodeAt(0); + + if (code >= 0xAC00 && code <= 0xD7A3) { + const offset = code - 0xAC00; + const cho = CHO[Math.floor(offset / (21 * 28))]; + const jung = JUNG[Math.floor((offset % (21 * 28)) / 28)]; + const jong = JONG[offset % 28]; + const jung_expanded = COMPOUND_JUNG[jung] || jung + return cho + jung_expanded + (jong ? JONG_MAP[jong] : ''); + } + + return ch; + }).join(''); +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..bfeb925 --- /dev/null +++ b/style.css @@ -0,0 +1,90 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #f5f5f5; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + gap: 24px; + font-family: 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif; +} + +typing-practice { + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 12px; + padding: 48px 56px; + width: 640px; + display: flex; + flex-direction: column; + gap: 32px; +} + +.sentence { + font-size: 28px; + letter-spacing: 0.02em; + line-height: 1.6; +} + +.sentence span.correct { + color: #111111; +} + +.sentence span.wrong { + color: #e53935; +} + +.sentence span.pending { + color: #c0c0c0; +} + +input { + font-size: 20px; + font-family: inherit; + border: none; + border-bottom: 2px solid #e0e0e0; + outline: none; + padding: 8px 0; + width: 100%; + background: transparent; + color: #111111; + transition: border-color 0.15s; +} + +input:focus { + border-bottom-color: #111111; +} + +.dict-display { + display: flex; + flex-wrap: wrap; + gap: 8px; + width: 640px; + min-height: 36px; +} + +.dict-item { + display: flex; + align-items: center; + gap: 4px; + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 4px 10px; + font-size: 14px; +} + +.dict-char { + color: #e53935; + font-size: 16px; +} + +.dict-count { + color: #888888; +}