your system software has rejected this edit

이 오류 메시지가 뜨는 경우 번거로워도 불필요한 프로그램 설치없이 설정하려면 다음과 같은 방법으로 해야 한다.

1. 개발자 옵션을 활성화 한다.
설정 > 휴대전화 정보 > 소프트웨어 정보 > 빌드번호 항목을 연속하여 터치(클릭)하여 개발자 옵션을 활성화 한다.

2. usb 디버깅 모드를 활성화환다.
설정 > 개발자 옵션 > USB 디버깅을 허용으로 설정한다.

3. PC에 Google에서 제공하는 platform-tools를 다운로드 받아 압축을 해제한다. 한글 경로가 없도록 c:나 d: 등의 root에 압축을 해제하는 편이 편리한다.
다운로드: https://developer.android.com/studio/releases/platform-tools?hl=ko

4. 파워쉘이나 cmd를 실행하여 해당 경로로 이동하거나 해당 경로에서 오픈한다. (탐색기에서 shift + 우클릭하여 실행하거나 상단 경로에 cmd 입력 후 엔터 등)

5. 휴대폰을 PC와 연결한다. 충전케이블 말고 반드시 데이터케이블을 이용해야 한다.

6. cmd창에서

adb devices

 

7. 휴대폰 화면에 뜨는 USB 디버깅 확인 메시지에서 허용한다.

8. SetEdit에서 했던 설정을 여기서 입력한다.

adb shell settings put system csc_pref_camera_forced_shuttersound_key 0

 

9. 기본카메라에서 무음으로 사진이 잘 찍히는지 확인한다.

 

원인

이 스킨에는 단축키 적용이 되어 있었다.

w: /admin/entry/post/
e: /admin/skin/edit/
r: /admin/plugin/refererUrlLog/
h: /

그리고 해당키는 input과 textarea 태그를 제외한 곳에서 입력한 경우 발동되도록 액션이 걸려있다.

문제는 이 스킨의 댓글창은 div태그에 ContentEditable속성을 적용하여 만들어졌던 것이었다.

 

해결

스킨 html 편집을 하고 상단의 스크립트에서 getKey 함수의 if문의 조건을 다음과 같은 방식으로 변경한다.

<script>
    //추가 단축키
    var key = new Array();
    key['w'] = "/admin/entry/post/";
    key['e'] = "/admin/skin/edit/";
    key['r'] = "/admin/plugin/refererUrlLog/";
    key['h'] = "/";

    function getKey(keyStroke) {
      if (((event.srcElement.tagName === 'INPUT') || (event.srcElement.tagName === 'TEXTAREA') || (event.srcElement.tagName === 'DIV' && event.srcElement.isContentEditable))===false) {
        isNetscape = (document.layers);
        eventChooser = (isNetscape) ? keyStroke.which : event.keyCode;
        which = String.fromCharCode(eventChooser).toLowerCase();
        for (var i in key)
          if (which == i) window.location = key[i];
      }
    }
    document.onkeypress = getKey;
  </script>

 

'오류노트' 카테고리의 다른 글

IE 엑셀 다운로드 파일 바로열기 에러  (0) 2015.08.17

라든가 error msg=“Unable to gather” 등등

influxdb2에서 발생하는 에러인데

 

원인

나의 경우에는 보안인증서 적용후에 발생했는데 privkey.pem파일을 실행할 수 있는 권한이 없어서 발생한 문제였다.

 

해결

letsencrypt를 기준으로 해당 경로와 파일에 influxdb권한그룹과 계정이 권한을 가질 수 있도록 명령어를 작성하여 해결하였다.

centos기준

기존 파일에 읽기 권한을 부여함

setfacl -m d:user:influxdb:r /etc/letsencrypt/archive/[도메인]/privekey.pem

 

아래의 명령어로 미래에 생성될 파일에도 읽기 권한을 부여함

setfacl -m d:user:influxdb:r /etc/letsencrypt/archive/[도메인]

 

influx cli를 통해서 백업가능하다.

백업 공식문서 https://docs.influxdata.com/influxdb/v2/admin/backup-restore/backup/

influx backup [저장경로] -t [관리자초기토큰]

근데 골때리는게 저 관리자초기토큰이다.

아래의 명령어로 일반적인 관리자토큰으로 기존 토큰 목록을 조회할 수 있는데 influxdb UI와 달리 실제 token 값도 조회가 가능하다.

influx auth list -t [관리자토큰]

여기서 [read:/authorizations write:/authorizations read:/buckets write:/buckets read:/dashboards write:/dashboards read:/orgs write:/orgs read:/sources write:/sources read:/tasks write:/tasks read:/telegrafs write:/telegrafs read:/users write:/users read:/variables write:/variables read:/scrapers write:/scrapers read:/secrets write:/secrets read:/labels write:/labels read:/views write:/views read:/documents write:/documents read:/notificationRules write:/notificationRules read:/notificationEndpoints write:/notificationEndpoints read:/checks write:/checks read:/dbrp write:/dbrp read:/notebooks write:/notebooks read:/annotations write:/annotations read:/remotes write:/remotes read:/replications write:/replications] 이런 권한을 가진 토큰이 바로 저 관리자초기토큰인데 나의 경우에는 이게 없었다.

없는 경우 복원기능으로 복원해야 백업이 가능하다.(는 걸 뒤지고 뒤져서 포럼 어딘가의 답변에서 겨우 찾았다. 공식문서끼리 링크 좀 걸어줬으면 좋겠다.)

관리자초기토큰 복원 공식문서 https://docs.influxdata.com/influxdb/v2/reference/cli/influxd/recovery/auth/create-operator/

influxd recovery auth create-operator --bolt-path [bolt파일경로]
influxd recovery auth create-operator --org [organization이름] --username [관리자아이디] --bolt-path [bolt파일경로]

이 방식으로 관리자초기토큰을 복원하는데 성공했다. 이름은 [관리자아이디]'s Recovery Token
사실 간밤에 복원했을때는 recovery token 뭐 이런거랑 이거랑 2개였는데 오늘 출근하니까 한개는 없어지고 이거만 남았다ㅋㅋㅋ

bolt파일경로는 config.toml 파일에서 확인할 수 있다. config.toml 파일은 centos기준 /etc/influxdb/ 에 있다.

추후에 또 이 문제로 헤맬 수 있을 듯 하여 문서로 남겨놓는다.

 

돌고 돌아서 현재는 이 정보가 맞다.

출처: https://learn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/set-up-environment?tabs=dockerce#windows-server-1

Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/microsoft/Windows-Containers/Main/helpful_tools/Install-DockerCE/install-docker-ce.ps1" -o install-docker-ce.ps1
.\install-docker-ce.ps1

원격데스크톱으로 작업중이라 연결이 끊어졌는데 다시 재접속하니 마저 진행하고 잘 완료됨

 

json.asp 

<script language="JScript" runat="server">
    var JSON;
    if (!JSON) {
        JSON = {};
    }
    (function () {
        'use strict';
        function f(n) {
            // Format integers to have at least two digits.
            return n < 10 ? '0' + n : n;
        }
        if (typeof Date.prototype.toJSON !== 'function') {
            Date.prototype.toJSON = function (key) {
                return isFinite(this.valueOf())
                    ? this.getUTCFullYear() + '-' +
                        f(this.getUTCMonth() + 1) + '-' +
                        f(this.getUTCDate()) + 'T' +
                        f(this.getUTCHours()) + ':' +
                        f(this.getUTCMinutes()) + ':' +
                        f(this.getUTCSeconds()) + 'Z'
                    : null;
            };
            String.prototype.toJSON =
                Number.prototype.toJSON =
                Boolean.prototype.toJSON = function (key) {
                    return this.valueOf();
                };
        }
        var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
            escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
            gap,
            indent,
            meta = { // table of character substitutions
                '\b': '\\b',
                '\t': '\\t',
                '\n': '\\n',
                '\f': '\\f',
                '\r': '\\r',
                '"' : '\\"',
                '\\': '\\\\'
            },
            rep;
        function quote(string) {
    // If the string contains no control characters, no quote characters, and no
    // backslash characters, then we can safely slap some quotes around it.
    // Otherwise we must also replace the offending characters with safe escape
    // sequences.
            escapable.lastIndex = 0;
            return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                return typeof c === 'string'
                    ? c
                    : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' : '"' + string + '"';
        }
        function str(key, holder) {
    // Produce a string from holder[key].
            var i, // The loop counter.
                k, // The member key.
                v, // The member value.
                length,
                mind = gap,
                partial,
                value = holder[key];
    // If the value has a toJSON method, call it to obtain a replacement value.
            if (value && typeof value === 'object' &&
                    typeof value.toJSON === 'function') {
                value = value.toJSON(key);
            }
    // If we were called with a replacer function, then call the replacer to
    // obtain a replacement value.
            if (typeof rep === 'function') {
                value = rep.call(holder, key, value);
            }
    // What happens next depends on the value's type.
            switch (typeof value) {
            case 'string':
                return quote(value);
            case 'number':
    // JSON numbers must be finite. Encode non-finite numbers as null.
                return isFinite(value) ? String(value) : 'null';
            case 'boolean':
            case 'null':
    // If the value is a boolean or null, convert it to a string. Note:
    // typeof null does not produce 'null'. The case is included here in
    // the remote chance that this gets fixed someday.
                return String(value);
    // If the type is 'object', we might be dealing with an object or an array or
    // null.
            case 'object':
    // Due to a specification blunder in ECMAScript, typeof null is 'object',
    // so watch out for that case.
                if (!value) {
                    return 'null';
                }
    // Make an array to hold the partial results of stringifying this object value.
                gap += indent;
                partial = [];
    // Is the value an array?
                if (Object.prototype.toString.apply(value) === '[object Array]') {
    // The value is an array. Stringify every element. Use null as a placeholder
    // for non-JSON values.
                    length = value.length;
                    for (i = 0; i < length; i += 1) {
                        partial[i] = str(i, value) || 'null';
                    }
    // Join all of the elements together, separated with commas, and wrap them in
    // brackets.
                    v = partial.length === 0
                        ? '[]'
                        : gap
                        ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
                        : '[' + partial.join(',') + ']';
                    gap = mind;
                    return v;
                }
    // If the replacer is an array, use it to select the members to be stringified.
                if (rep && typeof rep === 'object') {
                    length = rep.length;
                    for (i = 0; i < length; i += 1) {
                        if (typeof rep[i] === 'string') {
                            k = rep[i];
                            v = str(k, value);
                            if (v) {
                                partial.push(quote(k) + (gap ? ': ' : ':') + v);
                            }
                        }
                    }
                } else {
    // Otherwise, iterate through all of the keys in the object.
                    for (k in value) {
                        if (Object.prototype.hasOwnProperty.call(value, k)) {
                            v = str(k, value);
                            if (v) {
                                partial.push(quote(k) + (gap ? ': ' : ':') + v);
                            }
                        }
                    }
                }
    // Join all of the member texts together, separated with commas,
    // and wrap them in braces.
                v = partial.length === 0
                    ? '{}'
                    : gap
                    ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
                    : '{' + partial.join(',') + '}';
                gap = mind;
                return v;
            }
        }
    // If the JSON object does not yet have a stringify method, give it one.
        if (typeof JSON.stringify !== 'function') {
            JSON.stringify = function (value, replacer, space) {
    // The stringify method takes a value and an optional replacer, and an optional
    // space parameter, and returns a JSON text. The replacer can be a function
    // that can replace values, or an array of strings that will select the keys.
    // A default replacer method can be provided. Use of the space parameter can
    // produce text that is more easily readable.
                var i;
                gap = '';
                indent = '';
    // If the space parameter is a number, make an indent string containing that
    // many spaces.
                if (typeof space === 'number') {
                    for (i = 0; i < space; i += 1) {
                        indent += ' ';
                    }
    // If the space parameter is a string, it will be used as the indent string.
                } else if (typeof space === 'string') {
                    indent = space;
                }
    // If there is a replacer, it must be a function or an array.
    // Otherwise, throw an error.
                rep = replacer;
                if (replacer && typeof replacer !== 'function' &&
                        (typeof replacer !== 'object' ||
                        typeof replacer.length !== 'number')) {
                    throw new Error('JSON.stringify');
                }
    // Make a fake root object containing our value under the key of ''.
    // Return the result of stringifying the value.
                return str('', {'': value});
            };
        }
    // If the JSON object does not yet have a parse method, give it one.
        if (typeof JSON.parse !== 'function') {
            JSON.parse = function (text, reviver) {
    // The parse method takes a text and an optional reviver function, and returns
    // a JavaScript value if the text is a valid JSON text.
                var j;
                function walk(holder, key) {
    // The walk method is used to recursively walk the resulting structure so
    // that modifications can be made.
                    var k, v, value = holder[key];
                    if (value && typeof value === 'object') {
                        for (k in value) {
                            if (Object.prototype.hasOwnProperty.call(value, k)) {
                                v = walk(value, k);
                                if (v !== undefined) {
                                    value[k] = v;
                                } else {
                                    delete value[k];
                                }
                            }
                        }
                    }
                    return reviver.call(holder, key, value);
                }
    // Parsing happens in four stages. In the first stage, we replace certain
    // Unicode characters with escape sequences. JavaScript handles many characters
    // incorrectly, either silently deleting them, or treating them as line endings.
                text = String(text);
                cx.lastIndex = 0;
                if (cx.test(text)) {
                    text = text.replace(cx, function (a) {
                        return '\\u' +
                            ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                    });
                }
    // In the second stage, we run the text against regular expressions that look
    // for non-JSON patterns. We are especially concerned with '()' and 'new'
    // because they can cause invocation, and '=' because it can cause mutation.
    // But just to be safe, we want to reject all unexpected forms.
    // We split the second stage into 4 regexp operations in order to work around
    // crippling inefficiencies in IE's and Safari's regexp engines. First we
    // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
    // replace all simple value tokens with ']' characters. Third, we delete all
    // open brackets that follow a colon or comma or that begin the text. Finally,
    // we look to see that the remaining characters are only whitespace or ']' or
    // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
                if (/^[\],:{}\s]*$/
                        .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
                            .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
                            .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
    // In the third stage we use the eval function to compile the text into a
    // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
    // in JavaScript: it can begin a block or an object literal. We wrap the text
    // in parens to eliminate the ambiguity.
                    j = eval('(' + text + ')');
    // In the optional fourth stage, we recursively walk the new structure, passing
    // each name/value pair to a reviver function for possible transformation.
                    return typeof reviver === 'function'
                        ? walk({'': j}, '')
                        : j;
                }
    // If the text is not JSON parseable, then a SyntaxError is thrown.
                throw new SyntaxError('JSON.parse');
            };
        }
    }());
</script>

출처: 쿠팡 https://developers.coupangcorp.com/hc/en-us/articles/360033396834-Classic-ASP-Example 최하단 제공하는 소스압축 파일내 존재함

 

<!--#include file="json.asp"-->
<%
	Call Response.AddHeader("Access-Control-Allow-Origin", "*") ' cors
	Response.ContentType = "application/json"

	dim bytes,binary,reqbody
	bytes=Request.TotalBytes
	binary=Request.BinaryRead(bytes)

	set stream = Server.CreateObject("Adodb.Stream")
	stream.type = 1
	stream.open
	stream.write(binary)
	stream.position = 0
	stream.type = 2
	stream.charset = "utf-8"
	reqbody=stream.readtext()
	stream.close
	set stream = nothing

	dim params
	set params = JSON.parse(reqbody)
%>
	json이 { "id": "test", "age": 100, "group": { "group_name": "dev" } }라고 치면
    params.id
    params.age
    params.group.group_name
    으로 접근가능함
<%   
	set params = nothing	
%>

'Classic ASP' 카테고리의 다른 글

Baisc Auth  (0) 2023.11.01
엑셀 xlsx 읽어들여 DB에 저장하기  (0) 2016.12.05
파일 복사  (0) 2016.11.08
스케쥴러  (0) 2016.09.23
숫자 관련 추가 함수  (0) 2016.06.27
<%
' Decodes a base-64 encoded string (BSTR type).
' 1999 - 2004 Antonin Foller, http://www.motobit.com
' 1.01 - solves problem with Access And 'Compare Database' (InStr)
Function Base64Decode(ByVal base64String)
  'rfc1521
  '1999 Antonin Foller, Motobit Software, http://Motobit.cz
  Const Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  Dim dataLength, sOut, groupBegin
  
  'remove white spaces, If any
  base64String = Replace(base64String, vbCrLf, "")
  base64String = Replace(base64String, vbTab, "")
  base64String = Replace(base64String, " ", "")
  
  'The source must consists from groups with Len of 4 chars
  dataLength = Len(base64String)
  If dataLength Mod 4 <> 0 Then
    Err.Raise 1, "Base64Decode", "Bad Base64 string."
    Exit Function
  End If

  
  ' Now decode each group:
  For groupBegin = 1 To dataLength Step 4
    Dim numDataBytes, CharCounter, thisChar, thisData, nGroup, pOut
    ' Each data group encodes up To 3 actual bytes.
    numDataBytes = 3
    nGroup = 0

    For CharCounter = 0 To 3
      ' Convert each character into 6 bits of data, And add it To
      ' an integer For temporary storage.  If a character is a '=', there
      ' is one fewer data byte.  (There can only be a maximum of 2 '=' In
      ' the whole string.)

      thisChar = Mid(base64String, groupBegin + CharCounter, 1)

      If thisChar = "=" Then
        numDataBytes = numDataBytes - 1
        thisData = 0
      Else
        thisData = InStr(1, Base64, thisChar, vbBinaryCompare) - 1
      End If
      If thisData = -1 Then
        Err.Raise 2, "Base64Decode", "Bad character In Base64 string."
        Exit Function
      End If

      nGroup = 64 * nGroup + thisData
    Next
    
    'Hex splits the long To 6 groups with 4 bits
    nGroup = Hex(nGroup)
    
    'Add leading zeros
    nGroup = String(6 - Len(nGroup), "0") & nGroup
    
    'Convert the 3 byte hex integer (6 chars) To 3 characters
    pOut = Chr(CByte("&H" & Mid(nGroup, 1, 2))) + _
      Chr(CByte("&H" & Mid(nGroup, 3, 2))) + _
      Chr(CByte("&H" & Mid(nGroup, 5, 2)))
    
    'add numDataBytes characters To out string
    sOut = sOut & Left(pOut, numDataBytes)
  Next

  Base64Decode = sOut
End Function
%>

<%
Dim UID, PWD
GetUser UID, PWD
If UID <> "아이디" or PWD <> "비밀번호" Then
  Response.Status = "401 Access Denied"
End If

Sub GetUser(LOGON_USER, LOGON_PASSWORD)
  Dim UP, Pos, Auth
  Auth = Request.ServerVariables("HTTP_AUTHORIZATION")
  LOGON_USER = ""
  LOGON_PASSWORD = ""
  If LCase(Left(Auth, 5)) = "basic" Then
    UP = Base64Decode(Mid(Auth, 7))
    Pos = InStr(UP, ":")
    If Pos > 1 Then
      LOGON_USER = Left(UP, Pos - 1)
      LOGON_PASSWORD = Mid(UP, Pos + 1)
    End If
  End If
End Sub
%>

 

 

출처: https://www.motobit.com/tips/detpg_base64/

'Classic ASP' 카테고리의 다른 글

request body json data parsing  (1) 2023.11.01
엑셀 xlsx 읽어들여 DB에 저장하기  (0) 2016.12.05
파일 복사  (0) 2016.11.08
스케쥴러  (0) 2016.09.23
숫자 관련 추가 함수  (0) 2016.06.27

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

- 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

+ Recent posts