에디터를 결정하기에 앞서 여러가지를 테스트해봤다.

- slate: 데이터가 json 형태라 기존 데이터와 호환성 문제로 fail

- react-draft: 데이터가 객체형태라 viewer로 따로 써야하고 어쩌고 하는데 무엇보다 반응속도라 느려서 fail

- quill: toolbar를 세부항목까지 자유롭게 커스텀이 가능한 점은 좋았으나 표나 이미지 업로드 기능같은걸 내가 구현해야한다는 점에서 내가 게을러서 fail

- jodit: 무료는 로고 존재감도 쎄고 문서가 불친절해서 보류했다가 tiny mce로 결정해서 fail

- tiny mce: 문서가 매우 잘 되어있고 커뮤니티 버전으로도 어지간한 기능은 다 있어서 최종 결정하게 되었다.

 

import dynamic from 'next/dynamic'
import React, { useState, useRef, useMemo, useEffect, useCallback } from 'react'

const Editor = dynamic(() => import('@tinymce/tinymce-react').then((mode) => mode.Editor), {
	ssr: false,
})

// plugin 기능 import 역할
const tinymcePlugins = [
	'link',
	'lists', // 기본 ol, ul
	'image',
	'autoresize',
	'autolink',
	'advlist', // ol, ul 불릿이 좀 더 여러가지가 생김
	'table',
	'preview',
	'searchreplace', // 치환기능
	'visualblocks', // 태그를 실루엣으로 보여주어 태그가 꼬일경우 편집하는데 도움됨
	'code', // 코드삽입기능ㄴㄴ html 태그로 보여주는 기능임
	'fullscreen',
	'media', // 동영상
	'accordion', // 티스토리 기준 접은글 기능
	'autosave', // localstrage에 저장함
]

// toolbar에 해당 기능을 나열함. plugin이 필요한 기능의 경우 위에도 아래에도 둘 다 들어가야함
const tinymceToolbar =
	'undo redo | blocks fontsize |' +
	'bold italic underline strikethrough forecolor backcolor |' +
	'bullist numlist blockquote|' +
	'alignleft aligncenter alignright alignjustify | outdent indent |' +
	'link image table accordion media | removeformat visualblocks searchreplace restoredraft | fullscreen preview code'

// 온갖 설정은 여기에 다 넣는다고 보면 됨
const editorInitOption = {
    language: 'ko_KR', // 한국어팩
    plugins: tinymcePlugins,
    toolbar: tinymceToolbar,
    color_default_background: '#E67E23', // fontBackColor 기본 선택은 되지만 "@tinymce/tinymce-react": "^4.3.0" 기준 pallete에서 선택은 안되어 있다.
	color_default_foreground: window.matchMedia('(prefers-color-scheme: dark)').matches ? '#fff' : '#000', // fontColor 기본 선택
    
    // dark skin 적용
    // toolbar css 변경 방법은 찾고 있다.
    skin: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'oxide-dark' : 'oxide',
    // dark 대신 css 파일 경로를 넣어줄수도 있다.
    // dark 적용후 import 되는 css 파일을 받아서 dark 기본색인 #222f3를 수정후 사용하면 좋다.
    content_css: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default',
    
    autosave_interval: '20s',
    autosave_restore_when_empty: true, // 빈 편집기에 마지막 저장데이터 복원여부 확인
    autosave_retention: '30m', // 30분 넘은 저장데이터는 삭제
    autosave_prefix: 'tinymce-autosave-{path}{query}-{id}-',
    // min_height: 250,
    // max_height: document.querySelector('#__next').offsetHeight - 250,
    menubar: false,
    branding: false,
    statusbar: false,
    
    // automatic_uploads: false, // true가 기본값
    // 이미지를 업로드할 서버 api 주소. 
    // 다음과 같은 형태로 반환해야 정상적으로 동작한다. 
    // { "location": "folder/sub-folder/new-location.png" }
    images_upload_url: '/api/image-uploads',  
    images_upload_base_path: '/api/files', // 업로드한 파일의 중복경로
    images_upload_credentials: true, // 인증정보가 cookie면 true
    file_picker_callback: (callback, value, meta) => {	
        const input = document.createElement('input')
        input.setAttribute('accept', 'image/*')
        input.setAttribute('type', 'file')
        input.onchange = function () {
            const file = this.files[0]
            const reader = new FileReader()
            reader.onload = function () {
                const id = 'blobid' + new Date().getTime()
                const blobCache = tinymce.activeEditor.editorUpload.blobCache
                const base64 = reader.result.split(',')[1]
                const blobInfo = blobCache.create(id, file, base64)
                blobCache.add(blobInfo)
                callback(blobInfo.blobUri(), { alt: file.name })
            }
            reader.readAsDataURL(file)
        }
        input.click()
    },
    
    // 이미지 업로드 중 커스텀 기능을 넣어야한다면 이걸로 처리해야 함
    // 위에서 설정한 항목중 images_upload_url, images_upload_base_path, images_upload_credentials를 쓰지 않고 내부 코드을 사용함
    // convert_urls: false, // 아래에서 resolve가 절대경로를 반환하는 거라면 false 기본값은 true임
    // images_upload_handler: (blobInfo, progress) => 
    //   
    // 	new Promise((resolve, reject) => {
    //		const images_upload_url = '/api/image-uploads' // 이미지 업로드 api 주소
    // 		const xhr = new XMLHttpRequest()
    // 		xhr.withCredentials = true // 인증정보가 cookie면 true
    // 		xhr.open('POST', images_upload_url)

    // 		xhr.upload.onprogress = (e) => {
    // 			progress((e.loaded / e.total) * 100)
    // 		}

    // 		xhr.onload = () => {
    // 			if (xhr.status === 403) {
    // 				reject({ message: 'HTTP Error: ' + xhr.status, remove: true })
    // 				return
    // 			}
    // 			if (xhr.status < 200 || xhr.status >= 300) {
    // 				reject('HTTP Error: ' + xhr.status)
    // 				return
    // 			}

    // 			const json = JSON.parse(xhr.responseText)
    // 			if (!json || typeof json.location != 'string') {
    // 				reject('Invalid JSON: ' + xhr.responseText)
    // 				return
    // 			}
    
    //			/* 커스텀 설정 넣을 곳 */

    // 			resolve(json.location) // 경로
    // 		}

    // 		xhr.onerror = () => {
    // 			reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status)
    // 		}

    // 		const formData = new FormData()
    // 		formData.append('file', blobInfo.blob(), blobInfo.filename())

    // 		xhr.send(formData)
    // 	}),
}

export default function Editor() {
	const editorRef = useRef(null)
    
    // editorRef.current.getContent()
    
	return <Editor
        onInit={(e, editor) => (editorRef.current = editor)}
        apiKey="API키"
        initialValue={에디터에 출력할 기본값이 있다면 여기}
        init={editorInitOption}
    />
}

toolbar css는 일단.... _doucument 파일에다 <NextScript /> 아래에 <link rel="stylesheet" href="" /> 태그를 삽입하는 방식으로 일단 처리하려고 한다. 대신에 @media (prefers-color-scheme: dark) { } 로 감싸주었다.

필요한 라이브러리를 찾았는데, 혹은 정착한 라이브러리라 하더라도 더 좋은게 있는데 놓치고 있진 않는가 확인하는 용도로 http://npmtrends.com 사이트를 이용하곤 한다.

어쩌다 다국어 관련하여 https://npmtrends.com/next-i18next-vs-next-translate 이걸 확인해보니 애정(?)하는 next-translate의 이용자 수가 미미한 것에 자극받아 귀차니즘을 무릎쓰고 이 글을 쓰게 되었다.

 

1. next-i18next

next.js 기반에서 가장 많이 쓰는 라이브러리고, 위의 두번째 링크를 타고 가서 봐도 알 수 있듯이 사용자 수가 급속히 늘고 있다. 나도 실제로 가장 처음으로 도입했었었다. 그렇게 몇 달 쓰다가 매 페이지마다 serverSideTranslations 이런거 넣어주는거에 현타가 와서 next-translate 테스트 후 바로 넘어가게 되었다. 

import { serverSideTranslations } from "next-i18next/serverSideTranslations"
import { useTranslation } from "next-i18next"

function Page () {
  const { t } = useTranslation("button")

  return (
    <div>
      <p>{t("ok")}</p>
    </div>
  );
};

export default Page

// 이거 페이지마다 해줘야 함
export const getStaticProps = async ({ locale }) => ({
  props: {
    ...(await serverSideTranslations(locale, ["common", "button", ... ])), // 해당 페이지에서 쓸 파일명 나열
  },
});

그러다보니 next-i18next에 대해서 많이 잊었다가 이 글을 쓰기 위해 리마인드하기 위해 검색하다 보니 url에 /ko/ /en/ 이런것도 붙었었던 것 같기도 하고 아무튼 뭔가 많이 너저분했었던 기억만 남아있을 정도랄까?

 

2. next-translate

 next.config.js에서 

const nextTranslate = require('next-translate-plugin')
const nextConfig = {
	.
    .
    .
	...nextTranslate(),
}
module.exports = nextConfig

이 설정과 

/locale/에 언어별로 json  파일을 두고,

i18n.json에서는 pages라는 항목에 경로별로 어떤 파일을 적용할껀지 설정해준다. 여기가 바로 핵심적으로 감동했던 부분이었다.

{
	"locales": [
		"ko",
		"en",
        .
        .
        .
	],
	"defaultLocale": "ko",
	"pages": {
		"*": [
			"common",
			"button",
			"navigation",
            .
            .
            .
		]
	}
}
// "*" 은 경로=1.x 버전일때 저렇게 경로를 안 잡으면 components 폴더 안의 파일들이 안되길래 급한 마음에 저렇게 하고 그 후 테스트를 놨었는데 다른 해결책이 있을 수도 있다.
// []에는 /locales/ 하위 언어별 파일명

마지막으로 매 페이지에서는 다음과 같은 방식으로 쓰기만하면 된다.

import useTranslation from 'next-translate/useTranslation'
import getT from 'next-translate/getT'

// Front
function Page () {
	const { t } = useTranslation('') // '' 안에 locale의 특정 파일명을 넣을 수도 있지만 비워두면 다 불러옴
	return 
    	<>
            <button>{t('button:back')}</button>
            <button>{t('button:confirm.ok')}</button>
            <button>{t('button:confirm.cancel')}</button>
    	</>
}
// button => 파일명
// back, confirm.ok, confirm.cancel => json key

export default Page

// Server
export async function getServerSideProps(context) {
     const { locale } = context
    const t = await getT(locale, '') // '' 안에 특정 파일명 가능
    
    console.log(t('button:back');
    
    return {
        props: {}
    }
}

1.xx버전때 설정한 이후로 쭉 쓰고 있는데 초기 설정 당시 useTranslation를 매 페이지에서 하니 부하가 있는 듯해 react의 context에서 한번 한 후 꺼내 쓰는 방식으로 해서 속도를 드라마틱하게 개선했었는데 그 후 관련 테스트를 안해서 요즘은 모르겠지만 아무튼 사용성이 편해서 잘 쓰고 있다.

 

결론: next-translate 쓰세요!

인증이 된 사용자만 파일 다운로드가 가능해야하는 상황이라서 일반적인 a href를 쓸 수 없었다. 이런 경우 이런 식으로 파일 다운로드를 진행해야하는 것 같았다. 

간단해보이는 소스지만, 이 조합을 찾아내기까지 하루이상의 시간이 소요되었기 때문에 다음에는 헤매지 않기 위해 기록해둔다.

 

1. Express

import { join } from 'path';

...

router.get('/subpath/:id', async (req, res, next) => {
    const id = req.params.id;

    const filePath = join(__dirname, `서버에 저장된하위경로`);
    const filename = '파일명.확장자';
    
    // 1안
    res.setHeader('Content-Disposition', `attachment; filename=다른파일명가능`); // 필수
    res.sendFile(filePath + filename);

    // 2안
    res.download(filePath + filename); // 서버에 저장된 파일명으로만 가능

    // 3안
    res.setHeader('Content-Disposition', `attachment; filename=다른파일명가능`); // 필수
    const stream = fs.createReadStream(filePath + filename);
    stream.pipe(res);
});

출처: 불특정 다수

 

2. Front

const authHeader = 'token값'
const url = `/api/subpath/${id}`
let filename = '' // 서버에서 보내는 파일명이 담길 변수

fetch(url, { headers: { 'X-Auth-Token': authHeader } })
    .then((response) => {
        if (!response.ok) {
            throw Error(response.statusText)
        }

        const header = response.headers.get('Content-Disposition')
        const parts = header.split(';')
        filename = parts[1].split('=')[1].replaceAll('"', '')

        return response.blob()
    })
    .then((blob) => {
        if (blob != null) {
            var url = window.URL.createObjectURL(blob)
            var a = document.createElement('a')
            a.href = url
            a.download = filename
            document.body.appendChild(a)
            a.click()
            a.remove()
        }
    })
    .catch((err) => {
        console.log(err)
    })

출처: https://christosmonogios.com/2022/05/10/Use-the-Fetch-API-to-download-files-with-their-original-filename-and-the-proper-way-to-catch-server-errors/

 

front의 경우 이거 말고도 new ReadableStream에 new Response 동원해서 하는 복잡한 소스도 있었는데 위가 더 심플하고 속도도 체감될 정도로 빨라서 이 걸로 낙찰했다.

 

'Javascript' 카테고리의 다른 글

정렬 sort  (0) 2021.04.01
jQuery to VanillaJS  (0) 2021.03.16
키보드를 이용한 Input text 포커스 이동  (0) 2021.03.15
String to Date / Date to String  (0) 2020.12.17
숫자에 콤마와 언콤마를 편하게  (0) 2016.06.27

trigger를 통해서 값이 insert되면 외부 프로그램, 혹은 API를 호출하고 싶었다.

그때마다 검색되는 UDF인 sys_exec() 는 my_global.h가 없어 컴파일이 안되어 사용할 수 없는 상황이었다.

(MySQL 5에서는 있었지만 8 혹은 마이너 버전부터 제외한다는 글을 본 것 같다.)

mysql 내부에서 해결하는 것은 포기하고 돌도 돌다 node.js에 binlog를 이용하여 테이블을 감시하는 것을 찾았다.

https://www.npmjs.com/package/@vlasky/mysql-live-select

원래는 목적으로는 특정 레코드의 컬럼까지 감시가 가능한 것 같지만, 나는 테이블만 감시하는 목적으로 사용하려고 했고, 제공된 예제를 아래와 같이 수정하니 목적을 달성할 수 있게 되었다.

const LiveMysql = require('@vlasky/mysql-live-select');
const LiveMysqlKeySelector = require('@vlasky/mysql-live-select/lib/LiveMysqlKeySelector');

const settings = {
  host: '호스트주소',
  user: '아이디',
  password: '비밀번호',
  database: 'DB명'
};

const liveConnection = new LiveMysql(settings);

liveConnection
  .select('select version()', [], LiveMysqlKeySelector.Index(), [
    {
      table: '테이블명',
      condition: function (row, newRow, rowDeleted) {
        if (row && newRow === null) {
          if (rowDeleted === false) {
            // insert 상황            
          } else {
            // delete 상황
          }
        } else {
          // update 상황
        }
        return true;
      }
    }
  ]);

설치 innologica.github.io/vue2-daterange-picker/#installation :무사히 완료

npm i vue2-daterange-picker --save
or
yarn add vue2-daterange-picker

 

가장 기본 용법인 이건 된다.

// 컴포넌트 등록
import DateRangePicker from 'vue2-daterange-picker'
import 'vue2-daterange-picker/dist/vue2-daterange-picker.css'

Vue.component('DateRangePicker', DateRangePicker)


// 실사용
<template>
  <date-range-picker
    v-model="daterange" 
    :locale-data="{
      direction: 'ltr',
      firstDay: 0,
      format: 'yyyy-mm-dd',
      separator: ' - ',
      applyLabel: '확인',
      cancelLabel: '취소',
      weekLabel: '주',
      customRangeLabel: '기간',

      daysOfWeek: ['일', '월', '화', '수', '목', '금', '토'],
      monthNames: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
     }"
  />
</template>
<script>
import DateRangePicker from 'vue2-daterange-picker'
</script>

근데, 나는 props의 디폴트 값이 마음에 들지 않았다.

그리고 혼자 오해를 했다. 용법 innologica.github.io/vue2-daterange-picker/#usage : Register the component 이걸 하면 default props를 overriding 해줄거라고

내 잘못이 맞긴 한데 내가 파악한 구조상으로는 default props를 설정하려면 DateRangePicker.vue를 새로 만들던가 해야 할 것 같다. 기존 거 고쳐봤자 업데이트하면 날라갈 것 같으니 말이다.

그리고 매뉴얼이 진짜 불친절하다 props이름이랑 밑에 설명이랑 매칭이 바로 바로 안되서 일일히 _넣어서 확인해봐야했다.

시간도 버렸고 안 쓰기로 했지만 또 언젠가 쓰게 될지도 모르니까 오늘 정리한 것은 기록으로 남겨두겠다.

 

node_modules\vue2-daterange-picker\dist\vue2-daterange-picker.css에서 나는 문제가 있어서 수정한 부분은 아래와 같다.

// original
.daterangepicker.openscenter[data-v-7d8c7845]{
    ...
    left:50%;
    ...
}

// modify
.daterangepicker.openscenter[data-v-7d8c7845]{
    ...
    left:100%;
    ...
}

 

매뉴얼 중 props 관련하여 이름순 말고 관련있는 설정끼리 묶어봤다.

/** 날짜 설정과 관련된 옵션 **/
:minDate [String, Date], // 최소
:maxDate [String, Date], // 최대
:localeData Object,  // 로케일 데이터 포함한 객체
:dateRange [Object], // v-model prop에서 사용함 startDate와 endDate 를 포함하는 객체여야며 Date로 파싱할 수 있는 문자열이어야 함
:dateFormat Function, 
// function(classes, date) - 2 개의 매개 변수를받는 특수한 prop 유형 함수:
// "classes" - 컴포넌트의 로직이 정의한 클래스,
// "date" - 현재 처리 된 날짜.
// 렌더링 된 날짜에 적용 할 Vue 클래스 객체를 반환해야 함.


/** input 상태 관련 **/
:disabled Boolean, // 비활성화 상태
:readonly Boolean, // readonly 여부


/** input class명 관련 **/
:controlContainerClass [Object, String], // class명

/** 달력 모양 관련 **/
:ranges [Object, Boolean], // 오늘, 어제, 이번달, 올해, 지날달 범위 선택으로 뜨는것. 숨기려면 false로 설정할 수 있음
:opens String, // :ranges 목록의 위치를 지정하는 것임 "center", "left", "right", "inline"라는데 제대로 동작 안한다

:showWeekNumbers Boolean, // 주수표시
:showDropdowns Boolean, // 달력 위에 월 및 연도 선택에 대한 드롭 다운을 표시
:alwaysShowCalendars Boolean, // false로 설정하고 미리 정의 된 범위 중 하나를 선택하면 달력이 숨겨진다고 함
:singleDatePicker [Boolean, String], // 단일 캘린더만 표시

:timePicker Boolean, // 달력 아래에 시간 (시간 / 분) 선택에 대한 드롭 다운을 표시
:timePickerIncrement Number, // 분 드롭 다운에서 분 단위
:timePicker24Hour Boolean, // 24시간제 여부
:timePickerSeconds Boolean, // 시/분을 제외한 초를 선택

:appendToBody Boolean, // 드롭 다운 요소를 본문 끝에 추가하고 동적으로 크기 / 위치를 지정할지 여부
:calculatePosition Function, // appendToBody true일 경우 설정함


/** 달력 이벤트 관련 **/
:linkedCalendars Boolean, // 달력선택 연동
:autoApply Boolean, // 선택한 범위를 자동으로 적용
:closeOnEsc Boolean, // esc키로 드롭다운을 닫을 지 여부

여기까지만 봐서 정리도 여기까지만 한다.

 

// 숫자 배열

const arr = [5, 2, 6, 11, 8, 20];

// 오름차순
arr.sort(function(a, b) {
    return a - b;
});

// 내림차순
arr.sort(function(a, b) {
    return b - a;
});

 

// 문자 배열

const arr = ['cherry', 'peach', 'strawberry', 'apple'];

// 오름차순
arr.sort();

// 내림차순
arr.sort(function(a, b) {
	return (a > b)? -1 : ((a < b)? 1 : 0);
});

 

// 객체 배열

const arr = [
  { key: 10, value: '휴대폰' },
  { key: 5, value: '이어폰' },
  { key: 21, value: '가방' },
  { key: 8, value: '보조배터리' },
  { key: 16, value: '전화기' },
];

// 객체 내 숫자 기준
arr.sort(function(a, b) {
    return a.key - b.key;
});

// 객체 내 문자열 기준
arr.sort(function(a, b) {
	return (a.value > b.value)? -1 : ((a.value < b.value)? 1 : 0);
});

 

1. 선택자

// 기본
$(".box");

document.querySelector(".box"); // 첫번째만 반환함 
document.querySelectorAll(".box"); // 모두 반환함 nodeList 형태라고 하는데 배열이라고 생각하면 편함


// find
var container = $(".container");
container.find(".box");

var container = document.querySelector(".container");
container.querySelector(".box");

2. 기능실행

$(".box").hide();

// 한개
document.querySelector(".box").style.display = "none";
// 여러개
document.querySelectorAll(".box").forEach(box => { box.style.display = "none" })

3. 탐색

$(".box").next();
$(".box").prev();
$(".box").parent();

var box = document.querySelector(".box");
box.nextElementSibling;
box.previousElementSibling;
box.parentElement;

4. 요소

// 요소 추가
$("<div/>");
$("<span/>");

document.createElement("div");
document.createElement("span");


// 요소에 텍스트 추가
var element = document.createElement("div");
element.textContent = "Text"

var text = document.createTextNode("div");
element.appendChild(text);


// 요소의 속성 수정 
$(".button").text("New text");

document.querySelector(".button").textContent = "New text";

// 요소의 속성값 가져오기
$(".button").text();

document.querySelector(".button").textContent;


// 요소에 하위 요소 추가
$(".container").append($("<div/>"));

var element = document.createElement("div");
document.querySelector(".container").appendChild(element);

4. 이벤트 추가

// 기본
$(".button").click(function(e) { /* handle click event */ });
$(".button").mouseenter(function(e) {  /* handle click event */ });
$(document).keyup(function(e) {  /* handle key up event */  });

document.querySelector(".button").addEventListener("click", (e) => { /* ... */ });
document.querySelector(".button").addEventListener("mouseenter", (e) => { /* ... */ });
document.addEventListener("keyup", (e) => { /* ... */ });


// 동적으로 추가하는 요소에도 이벤트 추가
$(".search-container").on("click", ".search-result", handleClick); // 기존 요소와 추가될 요소까지

var searchElement = document.createElement("div");
document.querySelector(".search-container").appendChild(searchElement);
searchElement.addEventListener("click", handleClick); // 추가한 요소에 이벤트 추가하기


// Trigger
$(document).trigger("myEvent");
$(".box").trigger("myEvent");

document.dispatchEvent(new Event("myEvent"));
document.querySelector(".box").dispatchEvent(new Event("myEvent"));

5. Style (CSS)

// 하나라면
$(".box").css("color", "#000");

document.querySelector(".box").style.color = "#000";


// 여러개라면
$(".box").css({
  "color": "#000",
  "background-color": "red"
});

var box = document.querySelector(".box");
box.style.color = "#000";
box.style.backgroundColor = "red";

// Set all styles at once (and override any existing styles)
box.style.cssText = "color: #000; background-color: red";


// Class명 추가/삭제/토글
$(".box").addClass("focus");
$(".box").removeClass("focus");
$(".box").toggleClass("focus");

// 한개
var box = document.querySelector(".box");
box.classList.add("focus");
box.classList.remove("focus");
box.classList.toggle("focus");

// 여러개
var box = document.querySelector(".box");
box.classList.add("focus", "highlighted");
box.classList.remove("focus", "highlighted");

// 상호 베타적인 토글용 2개의 Class를 번갈아가며 써야 한다면
document.querySelector(".box").classList.replace("focus", "blurred");


// Class명을 가지고 있는지 확인
$(".box").hasClass("focus");

document.querySelector(".box").classList.contains("focus");

6. Document Ready

$(document).ready(function() { 
	// 소스
});

var ready = (callback) => {
  if (document.readyState != "loading") callback();
  else document.addEventListener("DOMContentLoaded", callback);
}

ready(() => { 
	// 소스
});

7. Ajax

$.ajax({
	url: "data.json"
}).done(function(data) {
	// ...
}).fail(function() {
	// Handle error
});


fetch("data.json").then(data => {
	// Handle data
}).catch(error => {
	// Handle error
});

 

출처를 기반으로 다시 정리함

출처: tobiasahlin.com/blog/move-from-jquery-to-vanilla-javascript/

 

Cheat sheet for moving from jQuery to vanilla JavaScript

This reference guide will help you convert jQuery's most common patterns to vanilla JavaScript

tobiasahlin.com

 

'Javascript' 카테고리의 다른 글

fetch로 file download (feat. express)  (0) 2023.05.15
정렬 sort  (0) 2021.04.01
키보드를 이용한 Input text 포커스 이동  (0) 2021.03.15
String to Date / Date to String  (0) 2020.12.17
숫자에 콤마와 언콤마를 편하게  (0) 2016.06.27

방향키와 페이지 업다운 키를 이용하여 포커스 이동을 구현해보았다.

<!doctype html>
<html lang="ko">
<head>
	<meta charset="utf-8">
	<title>키보드를 이용한 포커스 이동</title>
</head>
<body>
	<h1>키보드로 이동하기</h1>
	
	<form name="frm">
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a"><input name="b"><input name="c"><br>
		<input name="a">
		
		<button>전송</button>
	</form>
	
	
</body>
<script>
	const inputs = document.querySelectorAll("input");
	const total = inputs.length;

	const cols = 3; // 컬럼수
	//const rows = Math.ceil(total/cols); // 줄수

	function keyevent(event) {
		const keycode = event.keyCode;
		const idx = Array.from(document.querySelectorAll('input')).indexOf(event.target);

		switch (keycode) {
			case (13) : // Enter
				if(idx===(total-1)){ //enter      
					//alert("폼 전송");
				}else{
					document.querySelectorAll('input')[idx+1].focus();
				}
				break;

			case(33) : // PageUp
				document.querySelectorAll('input')[0].focus();
				break;

			case(34) : // PageDown
				document.querySelectorAll('input')[total-1].focus();
				break;
			case(37) : // Left
				if(idx===0){
					return false;      
				}else{  
					document.querySelectorAll('input')[idx-1].focus();
				}
				break;
			case(38) : // Up
				if(idx < cols){
					return false;      
				}else{
					document.querySelectorAll('input')[idx-cols].focus();
				} 
				break;
			case(39) : // Right
				if(idx===(total-1)){ 
					return false;      
				}else{      
					document.querySelectorAll('input')[idx+1].focus();
				} 
				break;

			case (40) : // Down
				if(idx+cols >= total){
					return false;       
				}else{
					document.querySelectorAll('input')[idx+cols].focus();
				}
				break;

			default : 
				return false;
				break;
		}
	}


	
	function formCheck(e) {
		// 문제가 있으면
		// e.preventDefault();
	}
	
	document.querySelector("form").addEventListener('submit', formCheck);
	for(const item of inputs) {
		item.addEventListener('keydown', keyevent);
	}

</script>
</html>

 

 

'Javascript' 카테고리의 다른 글

정렬 sort  (0) 2021.04.01
jQuery to VanillaJS  (0) 2021.03.16
String to Date / Date to String  (0) 2020.12.17
숫자에 콤마와 언콤마를 편하게  (0) 2016.06.27
정규식으로 괄호 안의 문자 추출과 치환하기  (0) 2015.07.09
String.prototype.toDate = function(format){
	var normalized      = this.replace(/[^a-zA-Z0-9]/g, '-');
	var normalizedFormat= format.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-');
	var formatItems     = normalizedFormat.split('-');
	var dateItems       = normalized.split('-');

	var monthIndex  = formatItems.indexOf("mm");
	var dayIndex    = formatItems.indexOf("dd");
	var yearIndex   = formatItems.indexOf("yyyy");
	var hourIndex     = formatItems.indexOf("hh");
	var minutesIndex  = formatItems.indexOf("ii");
	var secondsIndex  = formatItems.indexOf("ss");

	var today = new Date();

	var year  = yearIndex>-1  ? dateItems[yearIndex]    : today.getFullYear();
	var month = monthIndex>-1 ? dateItems[monthIndex]-1 : today.getMonth()-1;
	var day   = dayIndex>-1   ? dateItems[dayIndex]     : today.getDate();

	var hour    = hourIndex>-1      ? dateItems[hourIndex]    : today.getHours();
	var minute  = minutesIndex>-1   ? dateItems[minutesIndex] : today.getMinutes();
	var second  = secondsIndex>-1   ? dateItems[secondsIndex] : today.getSeconds();

	return new Date(year,month,day,hour,minute,second);
};

Date.prototype.format = function(f) {
    if (!this.valueOf()) return " ";
 
    var weekName = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
    var d = this;
     
    return f.replace(/(yyyy|yy|MM|dd|E|hh|mm|ss|a\/p)/gi, function($1) {
        switch ($1) {
            case "yyyy": return d.getFullYear();
            case "yy": return (d.getFullYear() % 1000).zf(2);
            case "MM": return (d.getMonth() + 1).zf(2);
            case "dd": return d.getDate().zf(2);
            case "E": return weekName[d.getDay()];
            case "HH": return d.getHours().zf(2);
            case "hh": return ((h = d.getHours() % 12) ? h : 12).zf(2);
            case "mm": return d.getMinutes().zf(2);
            case "ss": return d.getSeconds().zf(2);
            case "a/p": return d.getHours() < 12 ? "오전" : "오후";
            default: return $1;
        }
    });
};
 
String.prototype.string = function(len){var s = '', i = 0; while (i++ < len) { s += this; } return s;};
String.prototype.zf = function(len){return "0".string(len - this.length) + this;};
Number.prototype.zf = function(len){return this.toString().zf(len);};

 

사용법

const str = "2020-12-17";
const d = str.toDate("yyyy-MM-dd");
const dstr = d.format("yyyy-MM-dd");

 

출처: https://stove99.tistory.com/46

stackoverflow.com/questions/5619202/converting-a-string-to-a-date-in-javascript

// 숫자 타입에서 쓸 수 있도록 format() 함수 추가

Number.prototype.format = function(){

    if(this==0) return 0;

 

    var reg = /(^[+-]?\d+)(\d{3})/;

    var n = (this + '');

 

    while (reg.test(n)) n = n.replace(reg, '$1' + ',' + '$2');

 

    return n;

};

 

// 문자열 타입에서 쓸 수 있도록 format() 함수 추가

String.prototype.format = function(){

    var num = parseFloat(this);

    if( isNaN(num) ) return "0";

 

    return num.format();

};


// 콤마가 들어간 문자열을 콤마 제거하면서 숫자형 반환

String.prototype.unformat = function(){

var str = this.replace(/,/g,'');

var num = parseFloat(str);

if( isNaN(num) ) {

return "0";

}


return String(num);

};

 

 

// 숫자 타입 test

var num = 123456.012;

console.log(num.format());               // 123,456.012

 

num = 13546745;

console.log(num.format());               // 13,546,745

 

// 문자열 타입 test

console.log("12348".format());           // 12,348

console.log("12348.6456".format());      // 12,348.6456


출처: http://stove99.tistory.com/113


+ Recent posts