Markdown의 작은 하위 집합을 React 구성 요소로 구문 분석하는 방법은 무엇입니까?


9

React 구성 요소로 구문 분석하려는 일부 사용자 정의 HTML과 함께 Markdown의 매우 작은 하위 집합이 있습니다. 예를 들어 다음 문자열을 설정하고 싶습니다.

hello *asdf* *how* _are_ you !doing! today

다음 배열로 :

[ "hello ", <strong>asdf</strong>, " ", <strong>how</strong>, " ", <em>are</em>, " you ", <MyComponent onClick={this.action}>doing</MyComponent>, " today" ]

그런 다음 React render 함수에서 반환하십시오 (React는 배열을 형식화 된 HTML로 올바르게 렌더링합니다)

기본적으로 사용자에게 매우 제한된 Markdown 집합을 사용하여 텍스트를 스타일이 지정된 구성 요소 (일부 경우 내 구성 요소)로 전환하는 옵션을 제공하려고합니다.

위험하게 SetInnerHTML을 사용하는 것은 현명하지 않으며 외부 종속성을 가져오고 싶지 않습니다. 모두 매우 무겁기 때문에 매우 기본적인 기능 만 필요하기 때문입니다.

나는 현재 이와 같은 일을하고 있지만 매우 부서지기 쉽고 모든 경우에 작동하지는 않습니다. 더 좋은 방법이 있는지 궁금합니다.

function matchStrong(result, i) {
  let match = result[i].match(/(^|[^\\])\*(.*)\*/);
  if (match) { result[i] = <strong key={"ms" + i}>{match[2]}</strong>; }
  return match;
}

function matchItalics(result, i) {
  let match = result[i].match(/(^|[^\\])_(.*)_/); // Ignores \_asdf_ but not _asdf_
  if (match) { result[i] = <em key={"mi" + i}>{match[2]}</em>; }
  return match;
}

function matchCode(result, i) {
  let match = result[i].match(/(^|[^\\])```\n?([\s\S]+)\n?```/);
  if (match) { result[i] = <code key={"mc" + i}>{match[2]}</code>; }
  return match;
}

// Very brittle and inefficient
export function convertMarkdownToComponents(message) {
  let result = message.match(/(\\?([!*_`+-]{1,3})([\s\S]+?)\2)|\s|([^\\!*_`+-]+)/g);

  if (result == null) { return message; }

  for (let i = 0; i < result.length; i++) {
    if (matchCode(result, i)) { continue; }
    if (matchStrong(result, i)) { continue; }
    if (matchItalics(result, i)) { continue; }
  }

  return result;
}

여기에 이전 질문 이 있습니다.


1
입력에 font _italic *and bold* then only italic_ and normal? 와 같이 중첩 된 항목이 있으면 어떻게 됩니까? 예상되는 결과는 무엇입니까? 아니면 절대 중첩되지 않습니까?
trincot

1
중첩에 대해 걱정할 필요가 없습니다. 사용자가 사용하는 것은 매우 기본적인 마크 다운입니다. 구현하기 가장 쉬운 것은 나와 괜찮습니다. 귀하의 예에서 내부 굵게 표시가 작동하지 않으면 완전히 괜찮을 것입니다. 그러나 중첩을 구현하지 않는 것보다 중첩을 구현하는 것이 더 쉬운 경우에도 마찬가지입니다.
Ryan Peschel


1
그래도 마크 다운을 사용하지 않습니다. 그것은 매우 유사하고 작은 하위 집합입니다 (중첩되지 않은 굵은 기울임 꼴, 이탤릭체, 코드, 밑줄과 함께 몇 가지 사용자 정의 구성 요소를 지원합니다). 내가 약간의 작업을 게시 한 스 니펫은 매우 이상적이지 않으며 사소한 경우에는 실패합니다 (예 :이 별표를 입력 할 수없는 것처럼 : asdf*사라지지 않고)
Ryan Peschel

1
글쎄 ... markdown 또는 markdown과 같은 것을 파싱하는 것은 쉬운 일이 아닙니다 ...
정규 표현식

답변:


1

어떻게 작동합니까?

그것은 청크별로 문자열 청크를 읽음으로써 작동합니다. 이것은 실제로 긴 문자열에 대한 최상의 솔루션이 아닐 수도 있습니다.

파서는 임계 청크 (즉 '*', 다른 마크 다운 태그)를 읽고 있음을 감지 할 때마다 파서가 닫는 태그를 찾을 때까지이 요소의 청크 파싱을 시작합니다.

여러 줄 문자열에서 작동합니다. 예를 들어 코드를 참조하십시오.

경고

굵은 체와 기울임 꼴로 된 태그를 구문 분석해야하는 경우 , 현재 솔루션이 작동하지 않을 수 있습니다.

그러나 위의 조건으로 작업하려면 여기에 주석을 달고 코드를 조정하십시오.

첫 번째 업데이트 : 마크 다운 태그 처리 방법을 조정합니다.

태그는 더 이상 하드 코딩되지 않으며, 필요에 따라 쉽게 확장 할 수있는 맵입니다.

이 문제를 지적 해 주셔서 감사합니다. = p에서 언급 한 버그를 수정했습니다.

두 번째 업데이트 : 다중 길이 마크 다운 태그

가장 쉬운 방법 : 다중 길이 문자를 거의 사용하지 않는 유니 코드로 대체

이 방법 parseMarkdown은 아직 다중 길이 태그를 지원하지 않지만 소품을 string.replace 보낼 때 이러한 다중 길이 태그를 간단한 것으로 쉽게 바꿀 수 있습니다 rawMarkdown.

실제로 이에 대한 예제를 보려면 ReactDOM.render코드 끝에있는를보십시오.

응용 프로그램 여러 언어를 지원 하더라도 JavaScript가 여전히 감지하는 잘못된 유니 코드 문자가 있습니다. 예 : "\uFFFF"올바르게 기억하면 유효한 유니 코드가 아니지만 JS는 여전히 비교 할 수 있습니다 ( "\uFFFF" === "\uFFFF" = true)

처음에는 해킹처럼 보일 수 있지만 사용 사례에 따라이 경로를 사용하여 큰 문제는 보이지 않습니다.

이것을 달성하는 또 다른 방법

글쎄, 우리는 마지막 N( N가장 긴 다중 길이 태그의 길이에 해당하는) 청크를 쉽게 추적 할 수 있습니다 .

메소드 내부 루프가 parseMarkdown동작 하는 방식 , 즉 현재 청크가 다중 길이 태그의 일부인지, 태그로 사용되는지 확인하는 방법 이 약간 수정 될 것입니다. 그렇지 않은 경우,와 같은 경우 또는 이와 유사한 ``k것으로 표시하고 notMultiLength해당 청크를 콘텐츠로 푸시해야합니다.

암호

// Instead of creating hardcoded variables, we can make the code more extendable
// by storing all the possible tags we'll work with in a Map. Thus, creating
// more tags will not require additional logic in our code.
const tags = new Map(Object.entries({
  "*": "strong", // bold
  "!": "button", // action
  "_": "em", // emphasis
  "\uFFFF": "pre", // Just use a very unlikely to happen unicode character,
                   // We'll replace our multi-length symbols with that one.
}));
// Might be useful if we need to discover the symbol of a tag
const tagSymbols = new Map();
tags.forEach((v, k) => { tagSymbols.set(v, k ); })

const rawMarkdown = `
  This must be *bold*,

  This also must be *bo_ld*,

  this _entire block must be
  emphasized even if it's comprised of multiple lines_,

  This is an !action! it should be a button,

  \`\`\`
beep, boop, this is code
  \`\`\`

  This is an asterisk\\*
`;

class App extends React.Component {
  parseMarkdown(source) {
    let currentTag = "";
    let currentContent = "";

    const parsedMarkdown = [];

    // We create this variable to track possible escape characters, eg. "\"
    let before = "";

    const pushContent = (
      content,
      tagValue,
      props,
    ) => {
      let children = undefined;

      // There's the need to parse for empty lines
      if (content.indexOf("\n\n") >= 0) {
        let before = "";
        const contentJSX = [];

        let chunk = "";
        for (let i = 0; i < content.length; i++) {
          if (i !== 0) before = content[i - 1];

          chunk += content[i];

          if (before === "\n" && content[i] === "\n") {
            contentJSX.push(chunk);
            contentJSX.push(<br />);
            chunk = "";
          }

          if (chunk !== "" && i === content.length - 1) {
            contentJSX.push(chunk);
          }
        }

        children = contentJSX;
      } else {
        children = [content];
      }
      parsedMarkdown.push(React.createElement(tagValue, props, children))
    };

    for (let i = 0; i < source.length; i++) {
      const chunk = source[i];
      if (i !== 0) {
        before = source[i - 1];
      }

      // Does our current chunk needs to be treated as a escaped char?
      const escaped = before === "\\";

      // Detect if we need to start/finish parsing our tags

      // We are not parsing anything, however, that could change at current
      // chunk
      if (currentTag === "" && escaped === false) {
        // If our tags array has the chunk, this means a markdown tag has
        // just been found. We'll change our current state to reflect this.
        if (tags.has(chunk)) {
          currentTag = tags.get(chunk);

          // We have simple content to push
          if (currentContent !== "") {
            pushContent(currentContent, "span");
          }

          currentContent = "";
        }
      } else if (currentTag !== "" && escaped === false) {
        // We'll look if we can finish parsing our tag
        if (tags.has(chunk)) {
          const symbolValue = tags.get(chunk);

          // Just because the current chunk is a symbol it doesn't mean we
          // can already finish our currentTag.
          //
          // We'll need to see if the symbol's value corresponds to the
          // value of our currentTag. In case it does, we'll finish parsing it.
          if (symbolValue === currentTag) {
            pushContent(
              currentContent,
              currentTag,
              undefined, // you could pass props here
            );

            currentTag = "";
            currentContent = "";
          }
        }
      }

      // Increment our currentContent
      //
      // Ideally, we don't want our rendered markdown to contain any '\'
      // or undesired '*' or '_' or '!'.
      //
      // Users can still escape '*', '_', '!' by prefixing them with '\'
      if (tags.has(chunk) === false || escaped) {
        if (chunk !== "\\" || escaped) {
          currentContent += chunk;
        }
      }

      // In case an erroneous, i.e. unfinished tag, is present and the we've
      // reached the end of our source (rawMarkdown), we want to make sure
      // all our currentContent is pushed as a simple string
      if (currentContent !== "" && i === source.length - 1) {
        pushContent(
          currentContent,
          "span",
          undefined,
        );
      }
    }

    return parsedMarkdown;
  }

  render() {
    return (
      <div className="App">
        <div>{this.parseMarkdown(this.props.rawMarkdown)}</div>
      </div>
    );
  }
}

ReactDOM.render(<App rawMarkdown={rawMarkdown.replace(/```/g, "\uFFFF")} />, document.getElementById('app'));

코드 링크 (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

코드 링크 (vanilla / babel) https://codepen.io/ludanin/pen/eYmBvXw


이 솔루션이 올바르게 진행되고 있다고 생각하지만 다른 마크 다운 문자를 다른 문자 안에 넣는 데 문제가있는 것 같습니다. 예를 들어, 교체 시도 This must be *bold*와 함께 This must be *bo_ld*. 이 결과 HTML을, 부정되도록
라이언 Peschel에게

적절한 테스트가 이루어지지 않아서이 = p, 나쁘게 만들었습니다. 나는 이미 그것을 고치고 여기에 결과를 게시하려고합니다. 고치는 간단한 문제처럼 보입니다.
루카스 다닌

네, 고마워요. 나는 정말로이 솔루션을 좋아한다. 매우 강력하고 깨끗해 보입니다. 좀 더 우아하게 리팩토링 할 수 있다고 생각합니다. 나는 그것을 조금 어지럽 힐 수도 있습니다.
Ryan Peschel 2014

그런데 마크 다운 태그와 해당 JSX 값을 정의하는 훨씬 유연한 방법을 지원하도록 코드를 조정했습니다.
루카스 다닌

고마워요. 마지막 한 가지만 완벽하다고 생각합니다. 원래 게시물에는 코드 스 니펫 기능도 있습니다 (트리플 백틱 포함). 그것도 지원할 수 있습니까? 태그가 선택적으로 여러 문자가 될 수 있습니까? 또 다른 응답은```의 인스턴스를 거의 사용하지 않는 문자로 대체하여 지원을 추가했습니다. 그것은 쉬운 방법이지만, 그것이 이상적인지 확실하지 않습니다.
Ryan Peschel

4

아주 기본적인 솔루션을 찾고있는 것 같습니다. "슈퍼 몬스터"가 아닌 react-markdown-it:)

나는 가볍고 멋지게 보이는 https://github.com/developit/snarkdown 을 추천하고 싶습니다 ! 단 1kb이고 매우 간단합니다. 다른 구문 기능이 필요한 경우이를 사용하고 확장 할 수 있습니다.

지원되는 태그 목록 https://github.com/developit/snarkdown/blob/master/src/index.js#L1

최신 정보

반응 구성 요소에 대해 방금 인식했지만 처음에는 놓쳤습니다. 따라서 라이브러리를 예로 들어 HTML을 위험하게 설정하지 않고 사용자 정의 필수 구성 요소를 구현하는 것이 좋습니다. 도서관은 꽤 작고 깨끗합니다. 재미있게 보내세요! :)


3
var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

var myMarkdown = "hello *asdf* *how* _are_ you !doing! today";
var tagFinder = /(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/gm;

//Use case 1: direct string replacement
var replaced = myMarkdown.replace(tagFinder, replacer);
function replacer(match, whole, tag_begin, content, tag_end, offset, string) {
  return table[tag_begin]["begin"] + content + table[tag_begin]["end"];
}
alert(replaced);

//Use case 2: React components
var pieces = [];
var lastMatchedPosition = 0;
myMarkdown.replace(tagFinder, breaker);
function breaker(match, whole, tag_begin, content, tag_end, offset, string) {
  var piece;
  if (lastMatchedPosition < offset)
  {
    piece = string.substring(lastMatchedPosition, offset);
    pieces.push("\"" + piece + "\"");
  }
  piece = table[tag_begin]["begin"] + content + table[tag_begin]["end"];
  pieces.push(piece);
  lastMatchedPosition = offset + match.length;

}
alert(pieces);

결과: 실행 결과

정규식 테스트 결과

설명:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • 이 섹션에서 태그를 정의 할 수 있습니다. [*|!|_], 태그 중 하나가 일치하면 그룹으로 캡처되어 "tag_begin"으로 이름이 지정됩니다.

  • 그런 다음 (?<content>\w+)태그로 묶인 내용 을 캡처합니다.

  • 끝 태그는 이전에 일치 한 태그와 같아야하므로 여기에서를 사용 \k<tag_begin>하고 테스트를 통과 한 경우 그룹으로 캡처하여 "tag_end"라는 이름을 지정하면 (?<tag_end>\k<tag_begin>))됩니다.

JS에서는 다음과 같은 테이블을 설정했습니다.

var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

일치하는 태그를 바꾸려면이 표를 사용하십시오.

Sting.replace 에는 과부하 String.replace (regexp, function)가 있습니다. 매개 변수로 캡처 된 그룹을 취할 수 가 있습니다.이 캡처 된 항목을 사용하여 테이블을 찾고 교체 문자열을 생성합니다.

[업데이트]
코드를 업데이트했습니다. 다른 사람이 반응 구성 요소를 필요로하지 않을 경우를 대비하여 첫 번째 코드를 유지했는데 그 차이가 거의 없다는 것을 알 수 있습니다. 반응 성분


불행히도 이것이 작동하는지 확실하지 않습니다. 실제 React 구성 요소 및 요소 자체가 필요하기 때문에 문자열이 아닙니다. 원래 게시물을 보면 실제 요소 자체가 문자열이 아닌 배열에 추가되고 있음을 알 수 있습니다. 그리고 사용자가 악성 문자열을 입력 할 수 있으므로 dangerouslySetInnerHTML을 사용하는 것은 위험합니다.
Ryan Peschel

다행히도 문자열 대체를 React 구성 요소로 변환하는 것은 매우 간단합니다. 코드를 업데이트했습니다.
Simon

흠? 그들은 여전히 ​​내 문자열이기 때문에 뭔가를 놓치고 있어야합니다. 나는 심지어 당신의 코드로 바이올린을 만들었습니다. 당신이 읽는다면 console.log당신은 배열을 볼 수 있습니다 출력 문자열이 가득, 실제하지 구성 요소 반응 : jsfiddle.net/xftswh41
라이언 Peschel

솔직히 나는 React를 모른다. 그래서 모든 것을 완벽하게 따라갈 수는 없지만, 문제를 해결하는 방법에 대한 정보는 충분하다고 생각합니다 .React 기계에 넣어야하며 갈 수 있습니다.
시몬

이 스레드가 존재하는 이유는 스레드를 React 구성 요소로 구문 분석하기가 훨씬 더 어려워서 (실제로 정확한 요구를 지정하는 스레드 제목) 때문입니다. 문자열로 파싱하는 것은 매우 간단하며 문자열 바꾸기 기능을 사용할 수 있습니다. 문자열은 이상적인 솔루션 수없는 때문에있는 거 때문에 dangerouslySetInnerHTML 호출하는 데에 느리고 XSS에 취약
라이언 Peschel

0

당신은 이렇게 할 수 있습니다 :

//inside your compoenet

   mapData(myMarkdown){
    return myMarkdown.split(' ').map((w)=>{

        if(w.startsWith('*') && w.endsWith('*') && w.length>=3){
           w=w.substr(1,w.length-2);
           w=<strong>{w}</strong>;
         }else{
             if(w.startsWith('_') && w.endsWith('_') && w.length>=3){
                w=w.substr(1,w.length-2);
                w=<em>{w}</em>;
              }else{
                if(w.startsWith('!') && w.endsWith('!') && w.length>=3){
                w=w.substr(1,w.length-2);
                w=<YourComponent onClick={this.action}>{w}</YourComponent>;
                }
            }
         }
       return w;
    })

}


 render(){
   let content=this.mapData('hello *asdf* *how* _are_ you !doing! today');
    return {content};
  }

0

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

접근하다

문자 별 마크 다운 요소를 검색합니다. 하나가 발생하자마자 끝 태그를 찾아 html로 변환하십시오.

스 니펫에서 지원되는 태그

  • 굵게
  • 이탤릭체
  • 여자 이름
  • 사전

스 니펫의 입력 및 출력 :

JSFiddle : https://jsfiddle.net/sunil12738/wg7emcz1/58/

암호:

const preTag = "đ"
const map = {
      "*": "b",
      "!": "i",
      "_": "em",
      [preTag]: "pre"
    }

class App extends React.Component {
    constructor(){
      super()
      this.getData = this.getData.bind(this)
    }

    state = {
      data: []
    }
    getData() {
      let str = document.getElementById("ta1").value
      //If any tag contains more than one char, replace it with some char which is less frequently used and use it
      str = str.replace(/```/gi, preTag)
      const tempArr = []
      const tagsArr = Object.keys(map)
      let strIndexOf = 0;
      for (let i = 0; i < str.length; ++i) {
        strIndexOf = tagsArr.indexOf(str[i])
        if (strIndexOf >= 0 && str[i-1] !== "\\") {
          tempArr.push(str.substring(0, i).split("\\").join("").split(preTag).join(""))
          str = str.substr(i + 1);
          i = 0;
          for (let j = 0; j < str.length; ++j) {
            strIndexOf = tagsArr.indexOf(str[j])
            if (strIndexOf >= 0 && str[j-1] !== "\\") {
              const Tag = map[str[j]];
              tempArr.push(<Tag>{str.substring(0, j).split("\\").join("")}</Tag>)
              str = str.substr(j + 1);
              i = 0;
              break
             }
          }
        }
      }
      tempArr.push(str.split("\\").join(""))
      this.setState({
        data: tempArr,
      })
    }
    render() {
      return (
        <div>
          <textarea rows = "10"
            cols = "40"
           id = "ta1"
          /><br/>
          <button onClick={this.getData}>Render it</button><br/> 
          {this.state.data.map(x => x)} 
        </div>
      )
    }
  }

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
  <div id="root"></div>
</body>

자세한 설명 (예) :

문자열이 How are *you* doing? 태그에 대한 심볼의 매핑 유지를 가정 합니다

map = {
 "*": "b"
}
  • 첫 번째 *를 찾을 때까지 반복하십시오. 이전의 텍스트는 정상적인 문자열입니다
  • 배열 내부로 밀어 넣습니다. ["How are "]다음 *를 찾을 때까지 배열이되어 내부 루프를 시작합니다.
  • Now next between * and * needs to be bold, 우리는 텍스트로 html 요소로 변환하고 Tag = b 인 배열을지도에서 직접 밀어 넣습니다. 그렇게 <Tag>text</Tag>하면 반응은 내부적으로 텍스트 로 변환 하고 배열로 푸시합니다. 이제 배열은 [ "어떻게" 당신 ]. 내부 루프에서 끊기
  • 이제 외부 루프를 시작하고 태그를 찾을 수 없으므로 배열에 나머지를 밀어 넣으십시오. 배열은 ""어떻게 ", 당신은 ,"하고 있습니다 "].
  • UI에서 렌더링 How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

참고 : 중첩도 가능합니다. 재귀에서 위의 논리를 호출해야합니다.

새 태그 지원을 추가하려면

  • * 또는!와 같은 문자 인 경우 map키를 문자로, 값을 해당 태그로 사용 하여 객체에 추가하십시오.
  • ```와 같이 하나 이상의 문자 인 경우 자주 사용하지 않는 문자를 사용하여 일대일 맵을 만든 다음 삽입합니다 (이유 : 현재 문자 별 문자 기반 접근 방식으로 인해 둘 이상의 문자가 끊어짐). 로직을 개선하여 관리 할 수도 있습니다.)

중첩을 지원합니까? 아니요
OP에서 언급 한 모든 사용 사례를 지원합니까? 예

도움이 되길 바랍니다.


안녕, 지금 이걸 봐 트리플 백틱 지원과 함께 사용할 수 있습니까? 그렇다면``asdf ''는 코드 블록에서도 잘 작동합니까?
Ryan Peschel

그러나 약간의 수정이 필요할 수 있습니다. 현재 * 또는!에는 단일 문자 일치 만 있습니다. 약간 수정해야합니다. 코드 블록은 기본적으로 어두운 배경 asdf으로 렌더링 <pre>asdf</pre>됩니다. 알려 주시면하겠습니다. 당신도 지금 시도 할 수 있습니다. 간단한 해결책은 다음과 같습니다. 위의 솔루션에서 텍스트의```를 ^ 또는 ~와 같은 특수 문자로 바꾸고 프리 태그에 매핑하십시오. 그러면 잘 작동합니다. 다른 접근 방법은 좀 더 많은 작업을 필요로
선일 Chaudhary에게

예, 정확히``asdf''``를로 바꿉니다 <pre>asdf</pre>. 감사!
Ryan Peschel

@RyanPeschel 안녕! pre태그 지원도 추가했습니다 . 작동하는지 알려주세요
선일 Chaudhary에게

재미있는 해결책 (드문 문자 사용). 그래도 여전히 하나의 문제는 이스케이프에 대한 지원이 부족하다는 것입니다 (예 : \ * asdf *는 굵게 표시되지 않음). 원래 게시물의 코드에 대한 지원을 포함했습니다 (또한 마지막에 연결된 세부 사항에서 언급했습니다) 게시하다). 추가하기가 매우 어려울까요?
Ryan Peschel 2014
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.