프로그래머스 데브코스 5기 TIL 75 - 한글 글자 완성 체크 + 오타검출, input이벤트 순서

김영현·2024년 3월 4일
0

TIL

목록 보기
86/129

글자 완성 판별

내가 원하는 로직은 이렇다.

  1. 예시 문장이 존재함 ex) '벨로그 사용중'
  2. 유저가 예시 문장을 따라침. 이때 오타를 검출함. 중요한 건 한 글자가 완성되고나서 오타를 검출해야함

=> 한 글자가 완성되었단걸 어떻게 판별할까?

  • 커서 위치로 판별
  • 다음 글자를 입력하면 판별
  • 조합이 완료된 글자(유니코드 등...)으로 판별

일단 하나씩 해보자

조합이 완료된 글자로 판별

'슭'이라는 글자를 만드려면 'ㅅ', 'ㅡ', 'ㄹ', 'ㄱ' 총 4개의 글자를 합성해야함.

function isCompleteSyllable(char: string) {
  const charCode = char.charCodeAt(0);

  // 한글 음절 유니코드 범위: 44032 ~ 55203
  return charCode >= 44032 && charCode <= 55203;
}

나쁘지 않은 방법이지만 문제가있음. '띄어쓰기' 라는 글자를 입력할때
'띄'까지는 되지만, '어'를 치려고 'ㅇ'을 입력하면, '띙'이 되어 잠깐동안 오타로 나옴.
=> 글자 하나가 아예 완성되고나서 처리하는 게 나아보임

다음 글자를 입력하면, 바로 이전글자를 판별

원본 글자와 input글자의 인덱스를 비교함.

function comparePrevChar(sampleString: string, currentString: string) {
  return sampleString.at(-1) === currentString.at(-1);
}

길이를 저장해두었다가 다음글자인지 판별하거나 맨 마지막 -1번째 글자가 조합이 완료된 글자인지 판별하면 된다.

이 방법도 문제가 있음. 글자를 완성하기 전 까지 판별 불가.
한글은 조합글자라서 '산'이라는 글자를 입력할때 만약 'ㅅ'대신 'ㄱ'을 입력했다면, 바로 검출이 가능함.

이를 어떻게 판단한담?

초성, 중성, 종성으로 나눈다.

'산'에서 'ㅅ'이 초성, 'ㅏ'가 중성, 'ㄴ'이 종성이다.
글자를 이렇게 잘라서 구분하면, 'ㅅ'대신 'ㄱ'을 입력했을때 검출이 가능하지 않을까?

방법을 찾아본다...


한글을 분리해보자

다시 작성하기 귀찮아서...댓글 달았던 걸로 대체!
아무튼 로직은 이렇다.

인덱스를 구해서 각 초,중,종성배열에서 찾으면 됨.

export const chosungList = [
  'ㄱ',
  'ㄲ',
  'ㄴ',
  'ㄷ',
  'ㄸ',
  'ㄹ',
  'ㅁ',
  'ㅂ',
  'ㅃ',
  'ㅅ',
  'ㅆ',
  'ㅇ',
  'ㅈ',
  'ㅉ',
  'ㅊ',
  'ㅋ',
  'ㅌ',
  'ㅍ',
  'ㅎ',
];

export const jungsungList = [
  ['ㅏ'],
  ['ㅐ'],
  ['ㅑ'],
  ['ㅒ'],
  ['ㅓ'],
  ['ㅔ'],
  ['ㅕ'],
  ['ㅖ'],
  ['ㅗ'],
  ['ㅗ', 'ㅏ'],
  ['ㅗ', 'ㅐ'],
  ['ㅗ', 'ㅣ'],
  ['ㅛ'],
  ['ㅜ'],
  ['ㅜ', 'ㅓ'],
  ['ㅜ', 'ㅔ'],
  ['ㅜ', 'ㅣ'],
  ['ㅠ'],
  ['ㅡ'],
  ['ㅡ', 'ㅣ'],
  ['ㅣ'],
];

export const jongsungList = [
  [''],
  ['ㄱ'],
  ['ㄲ'],
  ['ㄱ', 'ㅅ'],
  ['ㄴ'],
  ['ㄴ', 'ㅈ'],
  ['ㄴ', 'ㅎ'],
  ['ㄷ'],
  ['ㄹ'],
  ['ㄹ', 'ㄱ'],
  ['ㄹ', 'ㅁ'],
  ['ㄹ', 'ㅂ'],
  ['ㄹ', 'ㅅ'],
  ['ㄹ', 'ㅌ'],
  ['ㄹ', 'ㅍ'],
  ['ㄹ', 'ㅎ'],
  ['ㅁ'],
  ['ㅂ'],
  ['ㅂ', 'ㅅ'],
  ['ㅅ'],
  ['ㅆ'],
  ['ㅇ'],
  ['ㅈ'],
  ['ㅊ'],
  ['ㅋ'],
  ['ㅌ'],
  ['ㅍ'],
  ['ㅎ'],
];

...

//초성, 중성, 종성을 분리하는 로직
import {
  chosungList,
  jongsungList,
  jungsungList,
} from '../constants/koreanCharacters';

export const decomposeKrChar = (char: string) => {
  //모든 글자를 배열로 처리하기위해 공백도 배열화 한다.
  if (char === ' ') {
    return [' '];
  }
  
  const BASE_CODE = 44032; // '가'의 유니코드
  const JUNGSUNG_COUNT = 21; // 중성 개수
  const JONGSUNG_COUNT = 28; // 종성 개수

  const getCodePoint = (char: string) => char.charCodeAt(0) - BASE_CODE;

  const code = getCodePoint(char);
  const chosungIndex = Math.floor(code / (JUNGSUNG_COUNT * JONGSUNG_COUNT));
  const jungsungIndex = Math.floor(
    (code % (JUNGSUNG_COUNT * JONGSUNG_COUNT)) / JONGSUNG_COUNT
  );
  const jongsungIndex = code % JONGSUNG_COUNT;

  const chosung = chosungList[chosungIndex];
  const jungsung = jungsungList[jungsungIndex];
  const jongsung = jongsungList[jongsungIndex];

  //char에 초성만 들어왔을때는 chosungIndex로 초성을 찾을 수 없다. 따라서 undefined일테니, char를 반환한다.
  return [chosung ?? char, jungsung, jongsung];
};

//오타를 검출하는 로직

import { decomposeKrChar } from './decomposeKrChar';
import isKorean from './isKorean';

//오타를 반환하는 함수라서 ===가 아닌 !==를 반환해야 합니다.
const getTypo = (
  sampleChar: string,
  userInputChar: string,
  isPrevChar = false
) => {
  //한글이 아닐때는 바로 비교하여도 손색없다. (일단 특문, 영어만 상정했다.)
  if (!isKorean(sampleChar)) {
    return sampleChar !== userInputChar;
  }

  const [sampleChosung, sampleJungsung, sampleJongsung] =
    decomposeKrChar(sampleChar);
  const [userChosung, userJungsung, userJongsung] =
    decomposeKrChar(userInputChar);

  const isUserJungsung = !!userJungsung?.length;
  const isSampleJungsung = !!sampleJungsung?.length;
  const isUserJongsung = !!userJongsung?.length && userJongsung[0] !== '';
  const isSampleJongsung = !!sampleJongsung?.length && sampleJongsung[0] !== '';

  //이전 글자 검사할땐 초,중,종성 모두 검사
  if (isPrevChar) {
    //유저 입력글자에 중성이 없으면, 오타(이전글자 검사에 공백은 들어오지 않는다.)
    if (!isUserJungsung) {
      return true;
    }
    //샘플에 종성이 있는데, 입력글자에 없었다면 오타
    if (isSampleJongsung && !isUserJongsung) {
      return true;
    }
    return (
      [...sampleJongsung].join('') !== [...userJongsung].join('') ||
      [...sampleJungsung].join('') !== [...userJungsung].join('') ||
      sampleChosung !== userChosung
    );
  }

  //현재 글자 검사할땐, 먼저 중성검사. 중성을 입력한 길이까지 검사한다.
  if (isUserJungsung && isSampleJungsung) {
    return (
      [...sampleJungsung].slice(0, userJungsung.length).join('') !==
        [...userJungsung].join('') || sampleChosung !== userChosung
    );
  }

  //그다음 초성검사. 공백이거나, 초성만 입력시 초성끼리만 비교한다.
  if (sampleChosung === ' ' || !isUserJungsung) {
    return sampleChosung !== userChosung;
  }

  //나머지. 예시글자에 종성이 없지만, 유저가 종성을 입력한 경우 오타가 아님.
  return false;
};

export default getTypo;

로직이 좀 길어졌는데...리팩토링이 필요하긴 하다 ㅎㅎ


input 이벤트 순서

  • Key down
  • Key press
  • Change

IME가 이벤트를 가로채다

input에 넘기는 이벤트를 IME(한글 입력기)가 가로채서 e.key이벤트가 process라는 단어만 뱉음.

profile
모르는 것을 모른다고 하기

0개의 댓글